1#!/usr/bin/env python3
2# Copyright 2016-2017 The Meson development team
3
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7
8#     http://www.apache.org/licenses/LICENSE-2.0
9
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import typing as T
17import stat
18import subprocess
19import re
20import json
21import tempfile
22import textwrap
23import os
24import shutil
25import sys
26import unittest
27import platform
28import pickle
29import functools
30import io
31import operator
32import threading
33import urllib.error
34import urllib.request
35import zipfile
36import hashlib
37from itertools import chain
38from unittest import mock
39from configparser import ConfigParser
40from contextlib import contextmanager
41from glob import glob
42from pathlib import (PurePath, Path)
43from distutils.dir_util import copy_tree
44import typing as T
45
46import mesonbuild.mlog
47import mesonbuild.depfile
48import mesonbuild.dependencies.base
49import mesonbuild.compilers
50import mesonbuild.envconfig
51import mesonbuild.environment
52import mesonbuild.mesonlib
53import mesonbuild.coredata
54import mesonbuild.modules.gnome
55from mesonbuild.interpreter import Interpreter, ObjectHolder
56from mesonbuild.ast import AstInterpreter
57from mesonbuild.mesonlib import (
58    BuildDirLock, LibType, MachineChoice, PerMachine, Version, is_windows,
59    is_osx, is_cygwin, is_dragonflybsd, is_openbsd, is_haiku, is_sunos,
60    windows_proof_rmtree, python_command, version_compare, split_args,
61    quote_arg, relpath, is_linux
62)
63from mesonbuild.environment import detect_ninja
64from mesonbuild.mesonlib import MesonException, EnvironmentException
65from mesonbuild.dependencies import PkgConfigDependency, ExternalProgram
66import mesonbuild.dependencies.base
67from mesonbuild.build import Target, ConfigurationData
68import mesonbuild.modules.pkgconfig
69
70from mesonbuild.mtest import TAPParser, TestResult
71
72from run_tests import (
73    Backend, FakeBuild, FakeCompilerOptions,
74    ensure_backend_detects_changes, exe_suffix, get_backend_commands,
75    get_builddir_target_args, get_fake_env, get_fake_options, get_meson_script,
76    run_configure_inprocess, run_mtest_inprocess
77)
78
79
80URLOPEN_TIMEOUT = 5
81
82@contextmanager
83def chdir(path: str):
84    curdir = os.getcwd()
85    os.chdir(path)
86    yield
87    os.chdir(curdir)
88
89
90def get_dynamic_section_entry(fname, entry):
91    if is_cygwin() or is_osx():
92        raise unittest.SkipTest('Test only applicable to ELF platforms')
93
94    try:
95        raw_out = subprocess.check_output(['readelf', '-d', fname],
96                                          universal_newlines=True)
97    except FileNotFoundError:
98        # FIXME: Try using depfixer.py:Elf() as a fallback
99        raise unittest.SkipTest('readelf not found')
100    pattern = re.compile(entry + r': \[(.*?)\]')
101    for line in raw_out.split('\n'):
102        m = pattern.search(line)
103        if m is not None:
104            return m.group(1)
105    return None # The file did not contain the specified entry.
106
107def get_soname(fname):
108    return get_dynamic_section_entry(fname, 'soname')
109
110def get_rpath(fname):
111    return get_dynamic_section_entry(fname, r'(?:rpath|runpath)')
112
113def is_tarball():
114    if not os.path.isdir('docs'):
115        return True
116    return False
117
118def is_ci():
119    if 'CI' in os.environ:
120        return True
121    return False
122
123def is_pull():
124    # Travis
125    if os.environ.get('TRAVIS_PULL_REQUEST', 'false') != 'false':
126        return True
127    # Azure
128    if 'SYSTEM_PULLREQUEST_ISFORK' in os.environ:
129        return True
130    return False
131
132def _git_init(project_dir):
133    subprocess.check_call(['git', 'init'], cwd=project_dir, stdout=subprocess.DEVNULL)
134    subprocess.check_call(['git', 'config',
135                           'user.name', 'Author Person'], cwd=project_dir)
136    subprocess.check_call(['git', 'config',
137                           'user.email', 'teh_coderz@example.com'], cwd=project_dir)
138    subprocess.check_call('git add *', cwd=project_dir, shell=True,
139                          stdout=subprocess.DEVNULL)
140    subprocess.check_call(['git', 'commit', '-a', '-m', 'I am a project'], cwd=project_dir,
141                          stdout=subprocess.DEVNULL)
142
143@functools.lru_cache()
144def is_real_gnu_compiler(path):
145    '''
146    Check if the gcc we have is a real gcc and not a macOS wrapper around clang
147    '''
148    if not path:
149        return False
150    out = subprocess.check_output([path, '--version'], universal_newlines=True, stderr=subprocess.STDOUT)
151    return 'Free Software Foundation' in out
152
153def skipIfNoExecutable(exename):
154    '''
155    Skip this test if the given executable is not found.
156    '''
157    def wrapper(func):
158        @functools.wraps(func)
159        def wrapped(*args, **kwargs):
160            if shutil.which(exename) is None:
161                raise unittest.SkipTest(exename + ' not found')
162            return func(*args, **kwargs)
163        return wrapped
164    return wrapper
165
166def skipIfNoPkgconfig(f):
167    '''
168    Skip this test if no pkg-config is found, unless we're on CI.
169    This allows users to run our test suite without having
170    pkg-config installed on, f.ex., macOS, while ensuring that our CI does not
171    silently skip the test because of misconfiguration.
172
173    Note: Yes, we provide pkg-config even while running Windows CI
174    '''
175    @functools.wraps(f)
176    def wrapped(*args, **kwargs):
177        if not is_ci() and shutil.which('pkg-config') is None:
178            raise unittest.SkipTest('pkg-config not found')
179        return f(*args, **kwargs)
180    return wrapped
181
182def skipIfNoPkgconfigDep(depname):
183    '''
184    Skip this test if the given pkg-config dep is not found, unless we're on CI.
185    '''
186    def wrapper(func):
187        @functools.wraps(func)
188        def wrapped(*args, **kwargs):
189            if not is_ci() and shutil.which('pkg-config') is None:
190                raise unittest.SkipTest('pkg-config not found')
191            if not is_ci() and subprocess.call(['pkg-config', '--exists', depname]) != 0:
192                raise unittest.SkipTest('pkg-config dependency {} not found.'.format(depname))
193            return func(*args, **kwargs)
194        return wrapped
195    return wrapper
196
197def skip_if_no_cmake(f):
198    '''
199    Skip this test if no cmake is found, unless we're on CI.
200    This allows users to run our test suite without having
201    cmake installed on, f.ex., macOS, while ensuring that our CI does not
202    silently skip the test because of misconfiguration.
203    '''
204    @functools.wraps(f)
205    def wrapped(*args, **kwargs):
206        if not is_ci() and shutil.which('cmake') is None:
207            raise unittest.SkipTest('cmake not found')
208        return f(*args, **kwargs)
209    return wrapped
210
211def skip_if_not_language(lang):
212    def wrapper(func):
213        @functools.wraps(func)
214        def wrapped(*args, **kwargs):
215            try:
216                env = get_fake_env()
217                f = getattr(env, 'detect_{}_compiler'.format(lang))
218                f(MachineChoice.HOST)
219            except EnvironmentException:
220                raise unittest.SkipTest('No {} compiler found.'.format(lang))
221            return func(*args, **kwargs)
222        return wrapped
223    return wrapper
224
225def skip_if_env_set(key):
226    '''
227    Skip a test if a particular env is set, except when running under CI
228    '''
229    def wrapper(func):
230        @functools.wraps(func)
231        def wrapped(*args, **kwargs):
232            old = None
233            if key in os.environ:
234                if not is_ci():
235                    raise unittest.SkipTest('Env var {!r} set, skipping'.format(key))
236                old = os.environ.pop(key)
237            try:
238                return func(*args, **kwargs)
239            finally:
240                if old is not None:
241                    os.environ[key] = old
242        return wrapped
243    return wrapper
244
245def skip_if_not_base_option(feature):
246    """Skip tests if The compiler does not support a given base option.
247
248    for example, ICC doesn't currently support b_sanitize.
249    """
250    def actual(f):
251        @functools.wraps(f)
252        def wrapped(*args, **kwargs):
253            env = get_fake_env()
254            cc = env.detect_c_compiler(MachineChoice.HOST)
255            if feature not in cc.base_options:
256                raise unittest.SkipTest(
257                    '{} not available with {}'.format(feature, cc.id))
258            return f(*args, **kwargs)
259        return wrapped
260    return actual
261
262
263@contextmanager
264def temp_filename():
265    '''A context manager which provides a filename to an empty temporary file.
266
267    On exit the file will be deleted.
268    '''
269
270    fd, filename = tempfile.mkstemp()
271    os.close(fd)
272    try:
273        yield filename
274    finally:
275        try:
276            os.remove(filename)
277        except OSError:
278            pass
279
280@contextmanager
281def no_pkgconfig():
282    '''
283    A context manager that overrides shutil.which and ExternalProgram to force
284    them to return None for pkg-config to simulate it not existing.
285    '''
286    old_which = shutil.which
287    old_search = ExternalProgram._search
288
289    def new_search(self, name, search_dir):
290        if name == 'pkg-config':
291            return [None]
292        return old_search(self, name, search_dir)
293
294    def new_which(cmd, *kwargs):
295        if cmd == 'pkg-config':
296            return None
297        return old_which(cmd, *kwargs)
298
299    shutil.which = new_which
300    ExternalProgram._search = new_search
301    try:
302        yield
303    finally:
304        shutil.which = old_which
305        ExternalProgram._search = old_search
306
307
308class InternalTests(unittest.TestCase):
309
310    def test_version_number(self):
311        searchfunc = mesonbuild.environment.search_version
312        self.assertEqual(searchfunc('foobar 1.2.3'), '1.2.3')
313        self.assertEqual(searchfunc('1.2.3'), '1.2.3')
314        self.assertEqual(searchfunc('foobar 2016.10.28 1.2.3'), '1.2.3')
315        self.assertEqual(searchfunc('2016.10.28 1.2.3'), '1.2.3')
316        self.assertEqual(searchfunc('foobar 2016.10.128'), '2016.10.128')
317        self.assertEqual(searchfunc('2016.10.128'), '2016.10.128')
318        self.assertEqual(searchfunc('2016.10'), '2016.10')
319        self.assertEqual(searchfunc('2016.10 1.2.3'), '1.2.3')
320        self.assertEqual(searchfunc('oops v1.2.3'), '1.2.3')
321        self.assertEqual(searchfunc('2016.oops 1.2.3'), '1.2.3')
322        self.assertEqual(searchfunc('2016.x'), 'unknown version')
323
324
325    def test_mode_symbolic_to_bits(self):
326        modefunc = mesonbuild.mesonlib.FileMode.perms_s_to_bits
327        self.assertEqual(modefunc('---------'), 0)
328        self.assertEqual(modefunc('r--------'), stat.S_IRUSR)
329        self.assertEqual(modefunc('---r-----'), stat.S_IRGRP)
330        self.assertEqual(modefunc('------r--'), stat.S_IROTH)
331        self.assertEqual(modefunc('-w-------'), stat.S_IWUSR)
332        self.assertEqual(modefunc('----w----'), stat.S_IWGRP)
333        self.assertEqual(modefunc('-------w-'), stat.S_IWOTH)
334        self.assertEqual(modefunc('--x------'), stat.S_IXUSR)
335        self.assertEqual(modefunc('-----x---'), stat.S_IXGRP)
336        self.assertEqual(modefunc('--------x'), stat.S_IXOTH)
337        self.assertEqual(modefunc('--S------'), stat.S_ISUID)
338        self.assertEqual(modefunc('-----S---'), stat.S_ISGID)
339        self.assertEqual(modefunc('--------T'), stat.S_ISVTX)
340        self.assertEqual(modefunc('--s------'), stat.S_ISUID | stat.S_IXUSR)
341        self.assertEqual(modefunc('-----s---'), stat.S_ISGID | stat.S_IXGRP)
342        self.assertEqual(modefunc('--------t'), stat.S_ISVTX | stat.S_IXOTH)
343        self.assertEqual(modefunc('rwx------'), stat.S_IRWXU)
344        self.assertEqual(modefunc('---rwx---'), stat.S_IRWXG)
345        self.assertEqual(modefunc('------rwx'), stat.S_IRWXO)
346        # We could keep listing combinations exhaustively but that seems
347        # tedious and pointless. Just test a few more.
348        self.assertEqual(modefunc('rwxr-xr-x'),
349                         stat.S_IRWXU |
350                         stat.S_IRGRP | stat.S_IXGRP |
351                         stat.S_IROTH | stat.S_IXOTH)
352        self.assertEqual(modefunc('rw-r--r--'),
353                         stat.S_IRUSR | stat.S_IWUSR |
354                         stat.S_IRGRP |
355                         stat.S_IROTH)
356        self.assertEqual(modefunc('rwsr-x---'),
357                         stat.S_IRWXU | stat.S_ISUID |
358                         stat.S_IRGRP | stat.S_IXGRP)
359
360    def test_compiler_args_class_none_flush(self):
361        cc = mesonbuild.compilers.CCompiler([], 'fake', False, MachineChoice.HOST, mock.Mock())
362        a = cc.compiler_args(['-I.'])
363        #first we are checking if the tree construction deduplicates the correct -I argument
364        a += ['-I..']
365        a += ['-I./tests/']
366        a += ['-I./tests2/']
367        #think this here as assertion, we cannot apply it, otherwise the CompilerArgs would already flush the changes:
368        # assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..', '-I.'])
369        a += ['-I.']
370        a += ['-I.', '-I./tests/']
371        self.assertEqual(a, ['-I.', '-I./tests/', '-I./tests2/', '-I..'])
372
373        #then we are checking that when CompilerArgs already have a build container list, that the deduplication is taking the correct one
374        a += ['-I.', '-I./tests2/']
375        self.assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..'])
376
377    def test_compiler_args_class_d(self):
378        d = mesonbuild.compilers.DCompiler([], 'fake', MachineChoice.HOST, 'info', 'arch', False, None)
379        # check include order is kept when deduplicating
380        a = d.compiler_args(['-Ifirst', '-Isecond', '-Ithird'])
381        a += ['-Ifirst']
382        self.assertEqual(a, ['-Ifirst', '-Isecond', '-Ithird'])
383
384    def test_compiler_args_class(self):
385        cc = mesonbuild.compilers.CCompiler([], 'fake', False, MachineChoice.HOST, mock.Mock())
386        # Test that empty initialization works
387        a = cc.compiler_args()
388        self.assertEqual(a, [])
389        # Test that list initialization works
390        a = cc.compiler_args(['-I.', '-I..'])
391        self.assertEqual(a, ['-I.', '-I..'])
392        # Test that there is no de-dup on initialization
393        self.assertEqual(cc.compiler_args(['-I.', '-I.']), ['-I.', '-I.'])
394
395        ## Test that appending works
396        a.append('-I..')
397        self.assertEqual(a, ['-I..', '-I.'])
398        a.append('-O3')
399        self.assertEqual(a, ['-I..', '-I.', '-O3'])
400
401        ## Test that in-place addition works
402        a += ['-O2', '-O2']
403        self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2', '-O2'])
404        # Test that removal works
405        a.remove('-O2')
406        self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2'])
407        # Test that de-dup happens on addition
408        a += ['-Ifoo', '-Ifoo']
409        self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2'])
410
411        # .extend() is just +=, so we don't test it
412
413        ## Test that addition works
414        # Test that adding a list with just one old arg works and yields the same array
415        a = a + ['-Ifoo']
416        self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2'])
417        # Test that adding a list with one arg new and one old works
418        a = a + ['-Ifoo', '-Ibaz']
419        self.assertEqual(a, ['-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2'])
420        # Test that adding args that must be prepended and appended works
421        a = a + ['-Ibar', '-Wall']
422        self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall'])
423
424        ## Test that reflected addition works
425        # Test that adding to a list with just one old arg works and yields the same array
426        a = ['-Ifoo'] + a
427        self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall'])
428        # Test that adding to a list with just one new arg that is not pre-pended works
429        a = ['-Werror'] + a
430        self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Werror', '-O3', '-O2', '-Wall'])
431        # Test that adding to a list with two new args preserves the order
432        a = ['-Ldir', '-Lbah'] + a
433        self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall'])
434        # Test that adding to a list with old args does nothing
435        a = ['-Ibar', '-Ibaz', '-Ifoo'] + a
436        self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall'])
437
438        ## Test that adding libraries works
439        l = cc.compiler_args(['-Lfoodir', '-lfoo'])
440        self.assertEqual(l, ['-Lfoodir', '-lfoo'])
441        # Adding a library and a libpath appends both correctly
442        l += ['-Lbardir', '-lbar']
443        self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar'])
444        # Adding the same library again does nothing
445        l += ['-lbar']
446        self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar'])
447
448        ## Test that 'direct' append and extend works
449        l = cc.compiler_args(['-Lfoodir', '-lfoo'])
450        self.assertEqual(l, ['-Lfoodir', '-lfoo'])
451        # Direct-adding a library and a libpath appends both correctly
452        l.extend_direct(['-Lbardir', '-lbar'])
453        self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar'])
454        # Direct-adding the same library again still adds it
455        l.append_direct('-lbar')
456        self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar'])
457        # Direct-adding with absolute path deduplicates
458        l.append_direct('/libbaz.a')
459        self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a'])
460        # Adding libbaz again does nothing
461        l.append_direct('/libbaz.a')
462        self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a'])
463
464    def test_compiler_args_class_gnuld(self):
465        ## Test --start/end-group
466        linker = mesonbuild.linkers.GnuDynamicLinker([], MachineChoice.HOST, 'fake', '-Wl,', [])
467        gcc = mesonbuild.compilers.GnuCCompiler([], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker)
468        ## Ensure that the fake compiler is never called by overriding the relevant function
469        gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include']
470        ## Test that 'direct' append and extend works
471        l = gcc.compiler_args(['-Lfoodir', '-lfoo'])
472        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group'])
473        # Direct-adding a library and a libpath appends both correctly
474        l.extend_direct(['-Lbardir', '-lbar'])
475        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-Wl,--end-group'])
476        # Direct-adding the same library again still adds it
477        l.append_direct('-lbar')
478        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '-Wl,--end-group'])
479        # Direct-adding with absolute path deduplicates
480        l.append_direct('/libbaz.a')
481        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group'])
482        # Adding libbaz again does nothing
483        l.append_direct('/libbaz.a')
484        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group'])
485        # Adding a non-library argument doesn't include it in the group
486        l += ['-Lfoo', '-Wl,--export-dynamic']
487        self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--end-group', '-Wl,--export-dynamic'])
488        # -Wl,-lfoo is detected as a library and gets added to the group
489        l.append('-Wl,-ldl')
490        self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '/libbaz.a', '-Wl,--export-dynamic', '-Wl,-ldl', '-Wl,--end-group'])
491
492    def test_compiler_args_remove_system(self):
493        ## Test --start/end-group
494        linker = mesonbuild.linkers.GnuDynamicLinker([], MachineChoice.HOST, 'fake', '-Wl,', [])
495        gcc = mesonbuild.compilers.GnuCCompiler([], 'fake', False, MachineChoice.HOST, mock.Mock(), linker=linker)
496        ## Ensure that the fake compiler is never called by overriding the relevant function
497        gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include']
498        ## Test that 'direct' append and extend works
499        l = gcc.compiler_args(['-Lfoodir', '-lfoo'])
500        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group'])
501        ## Test that to_native removes all system includes
502        l += ['-isystem/usr/include', '-isystem=/usr/share/include', '-DSOMETHING_IMPORTANT=1', '-isystem', '/usr/local/include']
503        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Wl,--end-group', '-DSOMETHING_IMPORTANT=1'])
504
505    def test_string_templates_substitution(self):
506        dictfunc = mesonbuild.mesonlib.get_filenames_templates_dict
507        substfunc = mesonbuild.mesonlib.substitute_values
508        ME = mesonbuild.mesonlib.MesonException
509
510        # Identity
511        self.assertEqual(dictfunc([], []), {})
512
513        # One input, no outputs
514        inputs = ['bar/foo.c.in']
515        outputs = []
516        ret = dictfunc(inputs, outputs)
517        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
518             '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c'}
519        # Check dictionary
520        self.assertEqual(ret, d)
521        # Check substitutions
522        cmd = ['some', 'ordinary', 'strings']
523        self.assertEqual(substfunc(cmd, d), cmd)
524        cmd = ['@INPUT@.out', 'ordinary', 'strings']
525        self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:])
526        cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', 'strings']
527        self.assertEqual(substfunc(cmd, d),
528                         [inputs[0] + '.out'] + [d['@PLAINNAME@'] + '.ok'] + cmd[2:])
529        cmd = ['@INPUT@', '@BASENAME@.hah', 'strings']
530        self.assertEqual(substfunc(cmd, d),
531                         inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])
532        cmd = ['@OUTPUT@']
533        self.assertRaises(ME, substfunc, cmd, d)
534
535        # One input, one output
536        inputs = ['bar/foo.c.in']
537        outputs = ['out.c']
538        ret = dictfunc(inputs, outputs)
539        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
540             '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
541             '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': '.'}
542        # Check dictionary
543        self.assertEqual(ret, d)
544        # Check substitutions
545        cmd = ['some', 'ordinary', 'strings']
546        self.assertEqual(substfunc(cmd, d), cmd)
547        cmd = ['@INPUT@.out', '@OUTPUT@', 'strings']
548        self.assertEqual(substfunc(cmd, d),
549                         [inputs[0] + '.out'] + outputs + cmd[2:])
550        cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', '@OUTPUT0@']
551        self.assertEqual(substfunc(cmd, d),
552                         [inputs[0] + '.out', d['@PLAINNAME@'] + '.ok'] + outputs)
553        cmd = ['@INPUT@', '@BASENAME@.hah', 'strings']
554        self.assertEqual(substfunc(cmd, d),
555                         inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])
556
557        # One input, one output with a subdir
558        outputs = ['dir/out.c']
559        ret = dictfunc(inputs, outputs)
560        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
561             '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
562             '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
563        # Check dictionary
564        self.assertEqual(ret, d)
565
566        # Two inputs, no outputs
567        inputs = ['bar/foo.c.in', 'baz/foo.c.in']
568        outputs = []
569        ret = dictfunc(inputs, outputs)
570        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1]}
571        # Check dictionary
572        self.assertEqual(ret, d)
573        # Check substitutions
574        cmd = ['some', 'ordinary', 'strings']
575        self.assertEqual(substfunc(cmd, d), cmd)
576        cmd = ['@INPUT@', 'ordinary', 'strings']
577        self.assertEqual(substfunc(cmd, d), inputs + cmd[1:])
578        cmd = ['@INPUT0@.out', 'ordinary', 'strings']
579        self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:])
580        cmd = ['@INPUT0@.out', '@INPUT1@.ok', 'strings']
581        self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:])
582        cmd = ['@INPUT0@', '@INPUT1@', 'strings']
583        self.assertEqual(substfunc(cmd, d), inputs + cmd[2:])
584        # Many inputs, can't use @INPUT@ like this
585        cmd = ['@INPUT@.out', 'ordinary', 'strings']
586        self.assertRaises(ME, substfunc, cmd, d)
587        # Not enough inputs
588        cmd = ['@INPUT2@.out', 'ordinary', 'strings']
589        self.assertRaises(ME, substfunc, cmd, d)
590        # Too many inputs
591        cmd = ['@PLAINNAME@']
592        self.assertRaises(ME, substfunc, cmd, d)
593        cmd = ['@BASENAME@']
594        self.assertRaises(ME, substfunc, cmd, d)
595        # No outputs
596        cmd = ['@OUTPUT@']
597        self.assertRaises(ME, substfunc, cmd, d)
598        cmd = ['@OUTPUT0@']
599        self.assertRaises(ME, substfunc, cmd, d)
600        cmd = ['@OUTDIR@']
601        self.assertRaises(ME, substfunc, cmd, d)
602
603        # Two inputs, one output
604        outputs = ['dir/out.c']
605        ret = dictfunc(inputs, outputs)
606        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
607             '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
608        # Check dictionary
609        self.assertEqual(ret, d)
610        # Check substitutions
611        cmd = ['some', 'ordinary', 'strings']
612        self.assertEqual(substfunc(cmd, d), cmd)
613        cmd = ['@OUTPUT@', 'ordinary', 'strings']
614        self.assertEqual(substfunc(cmd, d), outputs + cmd[1:])
615        cmd = ['@OUTPUT@.out', 'ordinary', 'strings']
616        self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out'] + cmd[1:])
617        cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', 'strings']
618        self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:])
619        # Many inputs, can't use @INPUT@ like this
620        cmd = ['@INPUT@.out', 'ordinary', 'strings']
621        self.assertRaises(ME, substfunc, cmd, d)
622        # Not enough inputs
623        cmd = ['@INPUT2@.out', 'ordinary', 'strings']
624        self.assertRaises(ME, substfunc, cmd, d)
625        # Not enough outputs
626        cmd = ['@OUTPUT2@.out', 'ordinary', 'strings']
627        self.assertRaises(ME, substfunc, cmd, d)
628
629        # Two inputs, two outputs
630        outputs = ['dir/out.c', 'dir/out2.c']
631        ret = dictfunc(inputs, outputs)
632        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
633             '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTPUT1@': outputs[1],
634             '@OUTDIR@': 'dir'}
635        # Check dictionary
636        self.assertEqual(ret, d)
637        # Check substitutions
638        cmd = ['some', 'ordinary', 'strings']
639        self.assertEqual(substfunc(cmd, d), cmd)
640        cmd = ['@OUTPUT@', 'ordinary', 'strings']
641        self.assertEqual(substfunc(cmd, d), outputs + cmd[1:])
642        cmd = ['@OUTPUT0@', '@OUTPUT1@', 'strings']
643        self.assertEqual(substfunc(cmd, d), outputs + cmd[2:])
644        cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', '@OUTDIR@']
645        self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok', 'dir'])
646        # Many inputs, can't use @INPUT@ like this
647        cmd = ['@INPUT@.out', 'ordinary', 'strings']
648        self.assertRaises(ME, substfunc, cmd, d)
649        # Not enough inputs
650        cmd = ['@INPUT2@.out', 'ordinary', 'strings']
651        self.assertRaises(ME, substfunc, cmd, d)
652        # Not enough outputs
653        cmd = ['@OUTPUT2@.out', 'ordinary', 'strings']
654        self.assertRaises(ME, substfunc, cmd, d)
655        # Many outputs, can't use @OUTPUT@ like this
656        cmd = ['@OUTPUT@.out', 'ordinary', 'strings']
657        self.assertRaises(ME, substfunc, cmd, d)
658
659    def test_needs_exe_wrapper_override(self):
660        config = ConfigParser()
661        config['binaries'] = {
662            'c': '\'/usr/bin/gcc\'',
663        }
664        config['host_machine'] = {
665            'system': '\'linux\'',
666            'cpu_family': '\'arm\'',
667            'cpu': '\'armv7\'',
668            'endian': '\'little\'',
669        }
670        # Can not be used as context manager because we need to
671        # open it a second time and this is not possible on
672        # Windows.
673        configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False)
674        configfilename = configfile.name
675        config.write(configfile)
676        configfile.flush()
677        configfile.close()
678        opts = get_fake_options()
679        opts.cross_file = (configfilename,)
680        env = get_fake_env(opts=opts)
681        detected_value = env.need_exe_wrapper()
682        os.unlink(configfilename)
683
684        desired_value = not detected_value
685        config['properties'] = {
686            'needs_exe_wrapper': 'true' if desired_value else 'false'
687        }
688
689        configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False)
690        configfilename = configfile.name
691        config.write(configfile)
692        configfile.close()
693        opts = get_fake_options()
694        opts.cross_file = (configfilename,)
695        env = get_fake_env(opts=opts)
696        forced_value = env.need_exe_wrapper()
697        os.unlink(configfilename)
698
699        self.assertEqual(forced_value, desired_value)
700
701    def test_listify(self):
702        listify = mesonbuild.mesonlib.listify
703        # Test sanity
704        self.assertEqual([1], listify(1))
705        self.assertEqual([], listify([]))
706        self.assertEqual([1], listify([1]))
707        # Test flattening
708        self.assertEqual([1, 2, 3], listify([1, [2, 3]]))
709        self.assertEqual([1, 2, 3], listify([1, [2, [3]]]))
710        self.assertEqual([1, [2, [3]]], listify([1, [2, [3]]], flatten=False))
711        # Test flattening and unholdering
712        holder1 = ObjectHolder(1)
713        self.assertEqual([holder1], listify(holder1))
714        self.assertEqual([holder1], listify([holder1]))
715        self.assertEqual([holder1, 2], listify([holder1, 2]))
716        self.assertEqual([holder1, 2, 3], listify([holder1, 2, [3]]))
717
718    def test_unholder(self):
719        unholder = mesonbuild.mesonlib.unholder
720
721        holder1 = ObjectHolder(1)
722        holder3 = ObjectHolder(3)
723        holders = [holder1, holder3]
724
725        self.assertEqual(1, unholder(holder1))
726        self.assertEqual([1], unholder([holder1]))
727        self.assertEqual([1, 3], unholder(holders))
728
729    def test_extract_as_list(self):
730        extract = mesonbuild.mesonlib.extract_as_list
731        # Test sanity
732        kwargs = {'sources': [1, 2, 3]}
733        self.assertEqual([1, 2, 3], extract(kwargs, 'sources'))
734        self.assertEqual(kwargs, {'sources': [1, 2, 3]})
735        self.assertEqual([1, 2, 3], extract(kwargs, 'sources', pop=True))
736        self.assertEqual(kwargs, {})
737
738        # Test unholding
739        holder3 = ObjectHolder(3)
740        kwargs = {'sources': [1, 2, holder3]}
741        self.assertEqual(kwargs, {'sources': [1, 2, holder3]})
742
743        # flatten nested lists
744        kwargs = {'sources': [1, [2, [3]]]}
745        self.assertEqual([1, 2, 3], extract(kwargs, 'sources'))
746
747    def test_pkgconfig_module(self):
748        dummystate = mock.Mock()
749        dummystate.subproject = 'dummy'
750        _mock = mock.Mock(spec=mesonbuild.dependencies.ExternalDependency)
751        _mock.pcdep = mock.Mock()
752        _mock.pcdep.name = "some_name"
753        _mock.version_reqs = []
754        _mock = mock.Mock(held_object=_mock)
755
756        # pkgconfig dependency as lib
757        deps = mesonbuild.modules.pkgconfig.DependenciesHelper(dummystate, "thislib")
758        deps.add_pub_libs([_mock])
759        self.assertEqual(deps.format_reqs(deps.pub_reqs), "some_name")
760
761        # pkgconfig dependency as requires
762        deps = mesonbuild.modules.pkgconfig.DependenciesHelper(dummystate, "thislib")
763        deps.add_pub_reqs([_mock])
764        self.assertEqual(deps.format_reqs(deps.pub_reqs), "some_name")
765
766    def _test_all_naming(self, cc, env, patterns, platform):
767        shr = patterns[platform]['shared']
768        stc = patterns[platform]['static']
769        shrstc = shr + tuple([x for x in stc if x not in shr])
770        stcshr = stc + tuple([x for x in shr if x not in stc])
771        p = cc.get_library_naming(env, LibType.SHARED)
772        self.assertEqual(p, shr)
773        p = cc.get_library_naming(env, LibType.STATIC)
774        self.assertEqual(p, stc)
775        p = cc.get_library_naming(env, LibType.PREFER_STATIC)
776        self.assertEqual(p, stcshr)
777        p = cc.get_library_naming(env, LibType.PREFER_SHARED)
778        self.assertEqual(p, shrstc)
779        # Test find library by mocking up openbsd
780        if platform != 'openbsd':
781            return
782        with tempfile.TemporaryDirectory() as tmpdir:
783            with open(os.path.join(tmpdir, 'libfoo.so.6.0'), 'w') as f:
784                f.write('')
785            with open(os.path.join(tmpdir, 'libfoo.so.5.0'), 'w') as f:
786                f.write('')
787            with open(os.path.join(tmpdir, 'libfoo.so.54.0'), 'w') as f:
788                f.write('')
789            with open(os.path.join(tmpdir, 'libfoo.so.66a.0b'), 'w') as f:
790                f.write('')
791            with open(os.path.join(tmpdir, 'libfoo.so.70.0.so.1'), 'w') as f:
792                f.write('')
793            found = cc.find_library_real('foo', env, [tmpdir], '', LibType.PREFER_SHARED)
794            self.assertEqual(os.path.basename(found[0]), 'libfoo.so.54.0')
795
796    def test_find_library_patterns(self):
797        '''
798        Unit test for the library search patterns used by find_library()
799        '''
800        unix_static = ('lib{}.a', '{}.a')
801        msvc_static = ('lib{}.a', 'lib{}.lib', '{}.a', '{}.lib')
802        # This is the priority list of pattern matching for library searching
803        patterns = {'openbsd': {'shared': ('lib{}.so', '{}.so', 'lib{}.so.[0-9]*.[0-9]*', '{}.so.[0-9]*.[0-9]*'),
804                                'static': unix_static},
805                    'linux': {'shared': ('lib{}.so', '{}.so'),
806                              'static': unix_static},
807                    'darwin': {'shared': ('lib{}.dylib', 'lib{}.so', '{}.dylib', '{}.so'),
808                               'static': unix_static},
809                    'cygwin': {'shared': ('cyg{}.dll', 'cyg{}.dll.a', 'lib{}.dll',
810                                          'lib{}.dll.a', '{}.dll', '{}.dll.a'),
811                               'static': ('cyg{}.a',) + unix_static},
812                    'windows-msvc': {'shared': ('lib{}.lib', '{}.lib'),
813                                     'static': msvc_static},
814                    'windows-mingw': {'shared': ('lib{}.dll.a', 'lib{}.lib', 'lib{}.dll',
815                                                 '{}.dll.a', '{}.lib', '{}.dll'),
816                                      'static': msvc_static}}
817        env = get_fake_env()
818        cc = env.detect_c_compiler(MachineChoice.HOST)
819        if is_osx():
820            self._test_all_naming(cc, env, patterns, 'darwin')
821        elif is_cygwin():
822            self._test_all_naming(cc, env, patterns, 'cygwin')
823        elif is_windows():
824            if cc.get_argument_syntax() == 'msvc':
825                self._test_all_naming(cc, env, patterns, 'windows-msvc')
826            else:
827                self._test_all_naming(cc, env, patterns, 'windows-mingw')
828        elif is_openbsd():
829            self._test_all_naming(cc, env, patterns, 'openbsd')
830        else:
831            self._test_all_naming(cc, env, patterns, 'linux')
832            env.machines.host.system = 'openbsd'
833            self._test_all_naming(cc, env, patterns, 'openbsd')
834            env.machines.host.system = 'darwin'
835            self._test_all_naming(cc, env, patterns, 'darwin')
836            env.machines.host.system = 'cygwin'
837            self._test_all_naming(cc, env, patterns, 'cygwin')
838            env.machines.host.system = 'windows'
839            self._test_all_naming(cc, env, patterns, 'windows-mingw')
840
841    @skipIfNoPkgconfig
842    def test_pkgconfig_parse_libs(self):
843        '''
844        Unit test for parsing of pkg-config output to search for libraries
845
846        https://github.com/mesonbuild/meson/issues/3951
847        '''
848        def create_static_lib(name):
849            if not is_osx():
850                name.open('w').close()
851                return
852            src = name.with_suffix('.c')
853            out = name.with_suffix('.o')
854            with src.open('w') as f:
855                f.write('int meson_foobar (void) { return 0; }')
856            subprocess.check_call(['clang', '-c', str(src), '-o', str(out)])
857            subprocess.check_call(['ar', 'csr', str(name), str(out)])
858
859        with tempfile.TemporaryDirectory() as tmpdir:
860            pkgbin = ExternalProgram('pkg-config', command=['pkg-config'], silent=True)
861            env = get_fake_env()
862            compiler = env.detect_c_compiler(MachineChoice.HOST)
863            env.coredata.compilers.host = {'c': compiler}
864            env.coredata.compiler_options.host['c']['link_args'] = FakeCompilerOptions()
865            p1 = Path(tmpdir) / '1'
866            p2 = Path(tmpdir) / '2'
867            p1.mkdir()
868            p2.mkdir()
869            # libfoo.a is in one prefix
870            create_static_lib(p1 / 'libfoo.a')
871            # libbar.a is in both prefixes
872            create_static_lib(p1 / 'libbar.a')
873            create_static_lib(p2 / 'libbar.a')
874            # Ensure that we never statically link to these
875            create_static_lib(p1 / 'libpthread.a')
876            create_static_lib(p1 / 'libm.a')
877            create_static_lib(p1 / 'libc.a')
878            create_static_lib(p1 / 'libdl.a')
879            create_static_lib(p1 / 'librt.a')
880
881            def fake_call_pkgbin(self, args, env=None):
882                if '--libs' not in args:
883                    return 0, '', ''
884                if args[0] == 'foo':
885                    return 0, '-L{} -lfoo -L{} -lbar'.format(p2.as_posix(), p1.as_posix()), ''
886                if args[0] == 'bar':
887                    return 0, '-L{} -lbar'.format(p2.as_posix()), ''
888                if args[0] == 'internal':
889                    return 0, '-L{} -lpthread -lm -lc -lrt -ldl'.format(p1.as_posix()), ''
890
891            old_call = PkgConfigDependency._call_pkgbin
892            old_check = PkgConfigDependency.check_pkgconfig
893            PkgConfigDependency._call_pkgbin = fake_call_pkgbin
894            PkgConfigDependency.check_pkgconfig = lambda x, _: pkgbin
895            # Test begins
896            try:
897                kwargs = {'required': True, 'silent': True}
898                foo_dep = PkgConfigDependency('foo', env, kwargs)
899                self.assertEqual(foo_dep.get_link_args(),
900                                 [(p1 / 'libfoo.a').as_posix(), (p2 / 'libbar.a').as_posix()])
901                bar_dep = PkgConfigDependency('bar', env, kwargs)
902                self.assertEqual(bar_dep.get_link_args(), [(p2 / 'libbar.a').as_posix()])
903                internal_dep = PkgConfigDependency('internal', env, kwargs)
904                if compiler.get_argument_syntax() == 'msvc':
905                    self.assertEqual(internal_dep.get_link_args(), [])
906                else:
907                    link_args = internal_dep.get_link_args()
908                    for link_arg in link_args:
909                        for lib in ('pthread', 'm', 'c', 'dl', 'rt'):
910                            self.assertNotIn('lib{}.a'.format(lib), link_arg, msg=link_args)
911            finally:
912                # Test ends
913                PkgConfigDependency._call_pkgbin = old_call
914                PkgConfigDependency.check_pkgconfig = old_check
915                # Reset dependency class to ensure that in-process configure doesn't mess up
916                PkgConfigDependency.pkgbin_cache = {}
917                PkgConfigDependency.class_pkgbin = PerMachine(None, None)
918
919    def test_version_compare(self):
920        comparefunc = mesonbuild.mesonlib.version_compare_many
921        for (a, b, result) in [
922                ('0.99.beta19', '>= 0.99.beta14', True),
923        ]:
924            self.assertEqual(comparefunc(a, b)[0], result)
925
926        for (a, b, op) in [
927                # examples from https://fedoraproject.org/wiki/Archive:Tools/RPM/VersionComparison
928                ("1.0010", "1.9", operator.gt),
929                ("1.05", "1.5", operator.eq),
930                ("1.0", "1", operator.gt),
931                ("2.50", "2.5", operator.gt),
932                ("fc4", "fc.4", operator.eq),
933                ("FC5", "fc4", operator.lt),
934                ("2a", "2.0", operator.lt),
935                ("1.0", "1.fc4", operator.gt),
936                ("3.0.0_fc", "3.0.0.fc", operator.eq),
937                # from RPM tests
938                ("1.0", "1.0", operator.eq),
939                ("1.0", "2.0", operator.lt),
940                ("2.0", "1.0", operator.gt),
941                ("2.0.1", "2.0.1", operator.eq),
942                ("2.0", "2.0.1", operator.lt),
943                ("2.0.1", "2.0", operator.gt),
944                ("2.0.1a", "2.0.1a", operator.eq),
945                ("2.0.1a", "2.0.1", operator.gt),
946                ("2.0.1", "2.0.1a", operator.lt),
947                ("5.5p1", "5.5p1", operator.eq),
948                ("5.5p1", "5.5p2", operator.lt),
949                ("5.5p2", "5.5p1", operator.gt),
950                ("5.5p10", "5.5p10", operator.eq),
951                ("5.5p1", "5.5p10", operator.lt),
952                ("5.5p10", "5.5p1", operator.gt),
953                ("10xyz", "10.1xyz", operator.lt),
954                ("10.1xyz", "10xyz", operator.gt),
955                ("xyz10", "xyz10", operator.eq),
956                ("xyz10", "xyz10.1", operator.lt),
957                ("xyz10.1", "xyz10", operator.gt),
958                ("xyz.4", "xyz.4", operator.eq),
959                ("xyz.4", "8", operator.lt),
960                ("8", "xyz.4", operator.gt),
961                ("xyz.4", "2", operator.lt),
962                ("2", "xyz.4", operator.gt),
963                ("5.5p2", "5.6p1", operator.lt),
964                ("5.6p1", "5.5p2", operator.gt),
965                ("5.6p1", "6.5p1", operator.lt),
966                ("6.5p1", "5.6p1", operator.gt),
967                ("6.0.rc1", "6.0", operator.gt),
968                ("6.0", "6.0.rc1", operator.lt),
969                ("10b2", "10a1", operator.gt),
970                ("10a2", "10b2", operator.lt),
971                ("1.0aa", "1.0aa", operator.eq),
972                ("1.0a", "1.0aa", operator.lt),
973                ("1.0aa", "1.0a", operator.gt),
974                ("10.0001", "10.0001", operator.eq),
975                ("10.0001", "10.1", operator.eq),
976                ("10.1", "10.0001", operator.eq),
977                ("10.0001", "10.0039", operator.lt),
978                ("10.0039", "10.0001", operator.gt),
979                ("4.999.9", "5.0", operator.lt),
980                ("5.0", "4.999.9", operator.gt),
981                ("20101121", "20101121", operator.eq),
982                ("20101121", "20101122", operator.lt),
983                ("20101122", "20101121", operator.gt),
984                ("2_0", "2_0", operator.eq),
985                ("2.0", "2_0", operator.eq),
986                ("2_0", "2.0", operator.eq),
987                ("a", "a", operator.eq),
988                ("a+", "a+", operator.eq),
989                ("a+", "a_", operator.eq),
990                ("a_", "a+", operator.eq),
991                ("+a", "+a", operator.eq),
992                ("+a", "_a", operator.eq),
993                ("_a", "+a", operator.eq),
994                ("+_", "+_", operator.eq),
995                ("_+", "+_", operator.eq),
996                ("_+", "_+", operator.eq),
997                ("+", "_", operator.eq),
998                ("_", "+", operator.eq),
999                # other tests
1000                ('0.99.beta19', '0.99.beta14', operator.gt),
1001                ("1.0.0", "2.0.0", operator.lt),
1002                (".0.0", "2.0.0", operator.lt),
1003                ("alpha", "beta", operator.lt),
1004                ("1.0", "1.0.0", operator.lt),
1005                ("2.456", "2.1000", operator.lt),
1006                ("2.1000", "3.111", operator.lt),
1007                ("2.001", "2.1", operator.eq),
1008                ("2.34", "2.34", operator.eq),
1009                ("6.1.2", "6.3.8", operator.lt),
1010                ("1.7.3.0", "2.0.0", operator.lt),
1011                ("2.24.51", "2.25", operator.lt),
1012                ("2.1.5+20120813+gitdcbe778", "2.1.5", operator.gt),
1013                ("3.4.1", "3.4b1", operator.gt),
1014                ("041206", "200090325", operator.lt),
1015                ("0.6.2+git20130413", "0.6.2", operator.gt),
1016                ("2.6.0+bzr6602", "2.6.0", operator.gt),
1017                ("2.6.0", "2.6b2", operator.gt),
1018                ("2.6.0+bzr6602", "2.6b2x", operator.gt),
1019                ("0.6.7+20150214+git3a710f9", "0.6.7", operator.gt),
1020                ("15.8b", "15.8.0.1", operator.lt),
1021                ("1.2rc1", "1.2.0", operator.lt),
1022        ]:
1023            ver_a = Version(a)
1024            ver_b = Version(b)
1025            if op is operator.eq:
1026                for o, name in [(op, 'eq'), (operator.ge, 'ge'), (operator.le, 'le')]:
1027                    self.assertTrue(o(ver_a, ver_b), '{} {} {}'.format(ver_a, name, ver_b))
1028            if op is operator.lt:
1029                for o, name in [(op, 'lt'), (operator.le, 'le'), (operator.ne, 'ne')]:
1030                    self.assertTrue(o(ver_a, ver_b), '{} {} {}'.format(ver_a, name, ver_b))
1031                for o, name in [(operator.gt, 'gt'), (operator.ge, 'ge'), (operator.eq, 'eq')]:
1032                    self.assertFalse(o(ver_a, ver_b), '{} {} {}'.format(ver_a, name, ver_b))
1033            if op is operator.gt:
1034                for o, name in [(op, 'gt'), (operator.ge, 'ge'), (operator.ne, 'ne')]:
1035                    self.assertTrue(o(ver_a, ver_b), '{} {} {}'.format(ver_a, name, ver_b))
1036                for o, name in [(operator.lt, 'lt'), (operator.le, 'le'), (operator.eq, 'eq')]:
1037                    self.assertFalse(o(ver_a, ver_b), '{} {} {}'.format(ver_a, name, ver_b))
1038
1039    def test_msvc_toolset_version(self):
1040        '''
1041        Ensure that the toolset version returns the correct value for this MSVC
1042        '''
1043        env = get_fake_env()
1044        cc = env.detect_c_compiler(MachineChoice.HOST)
1045        if cc.get_argument_syntax() != 'msvc':
1046            raise unittest.SkipTest('Test only applies to MSVC-like compilers')
1047        toolset_ver = cc.get_toolset_version()
1048        self.assertIsNotNone(toolset_ver)
1049        # Visual Studio 2015 and older versions do not define VCToolsVersion
1050        # TODO: ICL doesn't set this in the VSC2015 profile either
1051        if cc.id == 'msvc' and int(''.join(cc.version.split('.')[0:2])) < 1910:
1052            return
1053        if 'VCToolsVersion' in os.environ:
1054            vctools_ver = os.environ['VCToolsVersion']
1055        else:
1056            self.assertIn('VCINSTALLDIR', os.environ)
1057            # See https://devblogs.microsoft.com/cppblog/finding-the-visual-c-compiler-tools-in-visual-studio-2017/
1058            vctools_ver = (Path(os.environ['VCINSTALLDIR']) / 'Auxiliary' / 'Build' / 'Microsoft.VCToolsVersion.default.txt').read_text()
1059        self.assertTrue(vctools_ver.startswith(toolset_ver),
1060                        msg='{!r} does not start with {!r}'.format(vctools_ver, toolset_ver))
1061
1062    def test_split_args(self):
1063        split_args = mesonbuild.mesonlib.split_args
1064        join_args = mesonbuild.mesonlib.join_args
1065        if is_windows():
1066            test_data = [
1067                # examples from https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments
1068                (r'"a b c" d e', ['a b c', 'd', 'e'], True),
1069                (r'"ab\"c" "\\" d', ['ab"c', '\\', 'd'], False),
1070                (r'a\\\b d"e f"g h', [r'a\\\b', 'de fg', 'h'], False),
1071                (r'a\\\"b c d', [r'a\"b', 'c', 'd'], False),
1072                (r'a\\\\"b c" d e', [r'a\\b c', 'd', 'e'], False),
1073                # other basics
1074                (r'""', [''], True),
1075                (r'a b c d "" e', ['a', 'b', 'c', 'd', '', 'e'], True),
1076                (r"'a b c' d e", ["'a", 'b', "c'", 'd', 'e'], True),
1077                (r"'a&b&c' d e", ["'a&b&c'", 'd', 'e'], True),
1078                (r"a & b & c d e", ['a', '&', 'b', '&', 'c', 'd', 'e'], True),
1079                (r"'a & b & c d e'", ["'a", '&', 'b', '&', 'c', 'd', "e'"], True),
1080                ('a  b\nc\rd \n\re', ['a', 'b', 'c', 'd', 'e'], False),
1081                # more illustrative tests
1082                (r'cl test.cpp /O1 /Fe:test.exe', ['cl', 'test.cpp', '/O1', '/Fe:test.exe'], True),
1083                (r'cl "test.cpp /O1 /Fe:test.exe"', ['cl', 'test.cpp /O1 /Fe:test.exe'], True),
1084                (r'cl /DNAME=\"Bob\" test.cpp', ['cl', '/DNAME="Bob"', 'test.cpp'], False),
1085                (r'cl "/DNAME=\"Bob\"" test.cpp', ['cl', '/DNAME="Bob"', 'test.cpp'], True),
1086                (r'cl /DNAME=\"Bob, Alice\" test.cpp', ['cl', '/DNAME="Bob,', 'Alice"', 'test.cpp'], False),
1087                (r'cl "/DNAME=\"Bob, Alice\"" test.cpp', ['cl', '/DNAME="Bob, Alice"', 'test.cpp'], True),
1088                (r'cl C:\path\with\backslashes.cpp', ['cl', r'C:\path\with\backslashes.cpp'], True),
1089                (r'cl C:\\path\\with\\double\\backslashes.cpp', ['cl', r'C:\\path\\with\\double\\backslashes.cpp'], True),
1090                (r'cl "C:\\path\\with\\double\\backslashes.cpp"', ['cl', r'C:\\path\\with\\double\\backslashes.cpp'], False),
1091                (r'cl C:\path with spaces\test.cpp', ['cl', r'C:\path', 'with', r'spaces\test.cpp'], False),
1092                (r'cl "C:\path with spaces\test.cpp"', ['cl', r'C:\path with spaces\test.cpp'], True),
1093                (r'cl /DPATH="C:\path\with\backslashes test.cpp', ['cl', r'/DPATH=C:\path\with\backslashes test.cpp'], False),
1094                (r'cl /DPATH=\"C:\\ends\\with\\backslashes\\\" test.cpp', ['cl', r'/DPATH="C:\\ends\\with\\backslashes\"', 'test.cpp'], False),
1095                (r'cl /DPATH="C:\\ends\\with\\backslashes\\" test.cpp', ['cl', '/DPATH=C:\\\\ends\\\\with\\\\backslashes\\', 'test.cpp'], False),
1096                (r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\"', 'test.cpp'], True),
1097                (r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\\ test.cpp'], False),
1098                (r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\\"', 'test.cpp'], True),
1099            ]
1100        else:
1101            test_data = [
1102                (r"'a b c' d e", ['a b c', 'd', 'e'], True),
1103                (r"a/b/c d e", ['a/b/c', 'd', 'e'], True),
1104                (r"a\b\c d e", [r'abc', 'd', 'e'], False),
1105                (r"a\\b\\c d e", [r'a\b\c', 'd', 'e'], False),
1106                (r'"a b c" d e', ['a b c', 'd', 'e'], False),
1107                (r'"a\\b\\c\\" d e', ['a\\b\\c\\', 'd', 'e'], False),
1108                (r"'a\b\c\' d e", ['a\\b\\c\\', 'd', 'e'], True),
1109                (r"'a&b&c' d e", ['a&b&c', 'd', 'e'], True),
1110                (r"a & b & c d e", ['a', '&', 'b', '&', 'c', 'd', 'e'], False),
1111                (r"'a & b & c d e'", ['a & b & c d e'], True),
1112                (r"abd'e f'g h", [r'abde fg', 'h'], False),
1113                ('a  b\nc\rd \n\re', ['a', 'b', 'c', 'd', 'e'], False),
1114
1115                ('g++ -DNAME="Bob" test.cpp', ['g++', '-DNAME=Bob', 'test.cpp'], False),
1116                ("g++ '-DNAME=\"Bob\"' test.cpp", ['g++', '-DNAME="Bob"', 'test.cpp'], True),
1117                ('g++ -DNAME="Bob, Alice" test.cpp', ['g++', '-DNAME=Bob, Alice', 'test.cpp'], False),
1118                ("g++ '-DNAME=\"Bob, Alice\"' test.cpp", ['g++', '-DNAME="Bob, Alice"', 'test.cpp'], True),
1119            ]
1120
1121        for (cmd, expected, roundtrip) in test_data:
1122            self.assertEqual(split_args(cmd), expected)
1123            if roundtrip:
1124                self.assertEqual(join_args(expected), cmd)
1125
1126    def test_quote_arg(self):
1127        split_args = mesonbuild.mesonlib.split_args
1128        quote_arg = mesonbuild.mesonlib.quote_arg
1129        if is_windows():
1130            test_data = [
1131                ('', '""'),
1132                ('arg1', 'arg1'),
1133                ('/option1', '/option1'),
1134                ('/Ovalue', '/Ovalue'),
1135                ('/OBob&Alice', '/OBob&Alice'),
1136                ('/Ovalue with spaces', r'"/Ovalue with spaces"'),
1137                (r'/O"value with spaces"', r'"/O\"value with spaces\""'),
1138                (r'/OC:\path with spaces\test.exe', r'"/OC:\path with spaces\test.exe"'),
1139                ('/LIBPATH:C:\\path with spaces\\ends\\with\\backslashes\\', r'"/LIBPATH:C:\path with spaces\ends\with\backslashes\\"'),
1140                ('/LIBPATH:"C:\\path with spaces\\ends\\with\\backslashes\\\\"', r'"/LIBPATH:\"C:\path with spaces\ends\with\backslashes\\\\\""'),
1141                (r'/DMSG="Alice said: \"Let\'s go\""', r'"/DMSG=\"Alice said: \\\"Let\'s go\\\"\""'),
1142            ]
1143        else:
1144            test_data = [
1145                ('arg1', 'arg1'),
1146                ('--option1', '--option1'),
1147                ('-O=value', '-O=value'),
1148                ('-O=Bob&Alice', "'-O=Bob&Alice'"),
1149                ('-O=value with spaces', "'-O=value with spaces'"),
1150                ('-O="value with spaces"', '\'-O=\"value with spaces\"\''),
1151                ('-O=/path with spaces/test', '\'-O=/path with spaces/test\''),
1152                ('-DMSG="Alice said: \\"Let\'s go\\""', "'-DMSG=\"Alice said: \\\"Let'\"'\"'s go\\\"\"'"),
1153            ]
1154
1155        for (arg, expected) in test_data:
1156            self.assertEqual(quote_arg(arg), expected)
1157            self.assertEqual(split_args(expected)[0], arg)
1158
1159    def test_depfile(self):
1160        for (f, target, expdeps) in [
1161                # empty, unknown target
1162                ([''], 'unknown', set()),
1163                # simple target & deps
1164                (['meson/foo.o  : foo.c   foo.h'], 'meson/foo.o', set({'foo.c', 'foo.h'})),
1165                (['meson/foo.o: foo.c foo.h'], 'foo.c', set()),
1166                # get all deps
1167                (['meson/foo.o: foo.c foo.h',
1168                  'foo.c: gen.py'], 'meson/foo.o', set({'foo.c', 'foo.h', 'gen.py'})),
1169                (['meson/foo.o: foo.c foo.h',
1170                  'foo.c: gen.py'], 'foo.c', set({'gen.py'})),
1171                # linue continuation, multiple targets
1172                (['foo.o \\', 'foo.h: bar'], 'foo.h', set({'bar'})),
1173                (['foo.o \\', 'foo.h: bar'], 'foo.o', set({'bar'})),
1174                # \\ handling
1175                (['foo: Program\\ F\\iles\\\\X'], 'foo', set({'Program Files\\X'})),
1176                # $ handling
1177                (['f$o.o: c/b'], 'f$o.o', set({'c/b'})),
1178                (['f$$o.o: c/b'], 'f$o.o', set({'c/b'})),
1179                # cycles
1180                (['a: b', 'b: a'], 'a', set({'a', 'b'})),
1181                (['a: b', 'b: a'], 'b', set({'a', 'b'})),
1182        ]:
1183            d = mesonbuild.depfile.DepFile(f)
1184            deps = d.get_all_dependencies(target)
1185            self.assertEqual(deps, expdeps)
1186
1187    def test_log_once(self):
1188        f = io.StringIO()
1189        with mock.patch('mesonbuild.mlog.log_file', f), \
1190                mock.patch('mesonbuild.mlog._logged_once', set()):
1191            mesonbuild.mlog.log_once('foo')
1192            mesonbuild.mlog.log_once('foo')
1193            actual = f.getvalue().strip()
1194            self.assertEqual(actual, 'foo', actual)
1195
1196    def test_log_once_ansi(self):
1197        f = io.StringIO()
1198        with mock.patch('mesonbuild.mlog.log_file', f), \
1199                mock.patch('mesonbuild.mlog._logged_once', set()):
1200            mesonbuild.mlog.log_once(mesonbuild.mlog.bold('foo'))
1201            mesonbuild.mlog.log_once(mesonbuild.mlog.bold('foo'))
1202            actual = f.getvalue().strip()
1203            self.assertEqual(actual.count('foo'), 1, actual)
1204
1205            mesonbuild.mlog.log_once('foo')
1206            actual = f.getvalue().strip()
1207            self.assertEqual(actual.count('foo'), 1, actual)
1208
1209            f.truncate()
1210
1211            mesonbuild.mlog.warning('bar', once=True)
1212            mesonbuild.mlog.warning('bar', once=True)
1213            actual = f.getvalue().strip()
1214            self.assertEqual(actual.count('bar'), 1, actual)
1215
1216    def test_sort_libpaths(self):
1217        sort_libpaths = mesonbuild.dependencies.base.sort_libpaths
1218        self.assertEqual(sort_libpaths(
1219            ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'],
1220            ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']),
1221            ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])
1222        self.assertEqual(sort_libpaths(
1223            ['/usr/local/lib', '/home/mesonuser/.local/lib', '/usr/lib'],
1224            ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']),
1225            ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])
1226        self.assertEqual(sort_libpaths(
1227            ['/usr/lib', '/usr/local/lib', '/home/mesonuser/.local/lib'],
1228            ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']),
1229            ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])
1230        self.assertEqual(sort_libpaths(
1231            ['/usr/lib', '/usr/local/lib', '/home/mesonuser/.local/lib'],
1232            ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/libdata/pkgconfig']),
1233            ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])
1234
1235    def test_dependency_factory_order(self):
1236        b = mesonbuild.dependencies.base
1237        with tempfile.TemporaryDirectory() as tmpdir:
1238            with chdir(tmpdir):
1239                env = get_fake_env()
1240
1241                f = b.DependencyFactory(
1242                    'test_dep',
1243                    methods=[b.DependencyMethods.PKGCONFIG, b.DependencyMethods.CMAKE]
1244                )
1245                actual = [m() for m in f(env, MachineChoice.HOST, {'required': False})]
1246                self.assertListEqual([m.type_name for m in actual], ['pkgconfig', 'cmake'])
1247
1248                f = b.DependencyFactory(
1249                    'test_dep',
1250                    methods=[b.DependencyMethods.CMAKE, b.DependencyMethods.PKGCONFIG]
1251                )
1252                actual = [m() for m in f(env, MachineChoice.HOST, {'required': False})]
1253                self.assertListEqual([m.type_name for m in actual], ['cmake', 'pkgconfig'])
1254
1255    def test_validate_json(self) -> None:
1256        """Validate the json schema for the test cases."""
1257        try:
1258            from jsonschema import validate, ValidationError
1259        except ImportError:
1260            if is_ci():
1261                raise
1262            raise unittest.SkipTest('Python jsonschema module not found.')
1263
1264        with Path('data/test.schema.json').open() as f:
1265            schema = json.load(f)
1266
1267        errors = []  # type: T.Tuple[str, Exception]
1268        for p in Path('test cases').glob('**/test.json'):
1269            with p.open() as f:
1270                try:
1271                    validate(json.load(f), schema=schema)
1272                except ValidationError as e:
1273                    errors.append((p.resolve(), e))
1274
1275        for f, e in errors:
1276            print('Failed to validate: "{}"'.format(f))
1277            print(str(e))
1278
1279        self.assertFalse(errors)
1280
1281@unittest.skipIf(is_tarball(), 'Skipping because this is a tarball release')
1282class DataTests(unittest.TestCase):
1283
1284    def test_snippets(self):
1285        hashcounter = re.compile('^ *(#)+')
1286        snippet_dir = Path('docs/markdown/snippets')
1287        self.assertTrue(snippet_dir.is_dir())
1288        for f in snippet_dir.glob('*'):
1289            self.assertTrue(f.is_file())
1290            if f.parts[-1].endswith('~'):
1291                continue
1292            if f.suffix == '.md':
1293                in_code_block = False
1294                with f.open() as snippet:
1295                    for line in snippet:
1296                        if line.startswith('    '):
1297                            continue
1298                        if line.startswith('```'):
1299                            in_code_block = not in_code_block
1300                        if in_code_block:
1301                            continue
1302                        m = re.match(hashcounter, line)
1303                        if m:
1304                            self.assertEqual(len(m.group(0)), 2, 'All headings in snippets must have two hash symbols: ' + f.name)
1305                self.assertFalse(in_code_block, 'Unclosed code block.')
1306            else:
1307                if f.name != 'add_release_note_snippets_here':
1308                    self.assertTrue(False, 'A file without .md suffix in snippets dir: ' + f.name)
1309
1310    def test_compiler_options_documented(self):
1311        '''
1312        Test that C and C++ compiler options and base options are documented in
1313        Builtin-Options.md. Only tests the default compiler for the current
1314        platform on the CI.
1315        '''
1316        md = None
1317        with open('docs/markdown/Builtin-options.md', encoding='utf-8') as f:
1318            md = f.read()
1319        self.assertIsNotNone(md)
1320        env = get_fake_env()
1321        # FIXME: Support other compilers
1322        cc = env.detect_c_compiler(MachineChoice.HOST)
1323        cpp = env.detect_cpp_compiler(MachineChoice.HOST)
1324        for comp in (cc, cpp):
1325            for opt in comp.get_options().keys():
1326                self.assertIn(opt, md)
1327            for opt in comp.base_options:
1328                self.assertIn(opt, md)
1329        self.assertNotIn('b_unknown', md)
1330
1331    @staticmethod
1332    def _get_section_content(name, sections, md):
1333        for section in sections:
1334            if section and section.group(1) == name:
1335                try:
1336                    next_section = next(sections)
1337                    end = next_section.start()
1338                except StopIteration:
1339                    end = len(md)
1340                # Extract the content for this section
1341                return md[section.end():end]
1342        raise RuntimeError('Could not find "{}" heading'.format(name))
1343
1344    def test_builtin_options_documented(self):
1345        '''
1346        Test that universal options and base options are documented in
1347        Builtin-Options.md.
1348        '''
1349        from itertools import tee
1350        md = None
1351        with open('docs/markdown/Builtin-options.md', encoding='utf-8') as f:
1352            md = f.read()
1353        self.assertIsNotNone(md)
1354
1355        found_entries = set()
1356        sections = re.finditer(r"^## (.+)$", md, re.MULTILINE)
1357        # Extract the content for this section
1358        content = self._get_section_content("Universal options", sections, md)
1359        subsections = tee(re.finditer(r"^### (.+)$", content, re.MULTILINE))
1360        subcontent1 = self._get_section_content("Directories", subsections[0], content)
1361        subcontent2 = self._get_section_content("Core options", subsections[1], content)
1362        for subcontent in (subcontent1, subcontent2):
1363            # Find the option names
1364            options = set()
1365            # Match either a table row or a table heading separator: | ------ |
1366            rows = re.finditer(r"^\|(?: (\w+) .* | *-+ *)\|", subcontent, re.MULTILINE)
1367            # Skip the header of the first table
1368            next(rows)
1369            # Skip the heading separator of the first table
1370            next(rows)
1371            for m in rows:
1372                value = m.group(1)
1373                # End when the `buildtype` table starts
1374                if value is None:
1375                    break
1376                options.add(value)
1377            self.assertEqual(len(found_entries & options), 0)
1378            found_entries |= options
1379
1380        self.assertEqual(found_entries, set([
1381            *mesonbuild.coredata.builtin_options.keys(),
1382            *mesonbuild.coredata.builtin_options_per_machine.keys()
1383        ]))
1384
1385        # Check that `buildtype` table inside `Core options` matches how
1386        # setting of builtin options behaves
1387        #
1388        # Find all tables inside this subsection
1389        tables = re.finditer(r"^\| (\w+) .* \|\n\| *[-|\s]+ *\|$", subcontent2, re.MULTILINE)
1390        # Get the table we want using the header of the first column
1391        table = self._get_section_content('buildtype', tables, subcontent2)
1392        # Get table row data
1393        rows = re.finditer(r"^\|(?: (\w+)\s+\| (\w+)\s+\| (\w+) .* | *-+ *)\|", table, re.MULTILINE)
1394        env = get_fake_env()
1395        for m in rows:
1396            buildtype, debug, opt = m.groups()
1397            if debug == 'true':
1398                debug = True
1399            elif debug == 'false':
1400                debug = False
1401            else:
1402                raise RuntimeError('Invalid debug value {!r} in row:\n{}'.format(debug, m.group()))
1403            env.coredata.set_builtin_option('buildtype', buildtype)
1404            self.assertEqual(env.coredata.builtins['buildtype'].value, buildtype)
1405            self.assertEqual(env.coredata.builtins['optimization'].value, opt)
1406            self.assertEqual(env.coredata.builtins['debug'].value, debug)
1407
1408    def test_cpu_families_documented(self):
1409        with open("docs/markdown/Reference-tables.md", encoding='utf-8') as f:
1410            md = f.read()
1411        self.assertIsNotNone(md)
1412
1413        sections = re.finditer(r"^## (.+)$", md, re.MULTILINE)
1414        content = self._get_section_content("CPU families", sections, md)
1415        # Find the list entries
1416        arches = [m.group(1) for m in re.finditer(r"^\| (\w+) +\|", content, re.MULTILINE)]
1417        # Drop the header
1418        arches = set(arches[1:])
1419        self.assertEqual(arches, set(mesonbuild.environment.known_cpu_families))
1420
1421    def test_markdown_files_in_sitemap(self):
1422        '''
1423        Test that each markdown files in docs/markdown is referenced in sitemap.txt
1424        '''
1425        with open("docs/sitemap.txt", encoding='utf-8') as f:
1426            md = f.read()
1427        self.assertIsNotNone(md)
1428        toc = list(m.group(1) for m in re.finditer(r"^\s*(\w.*)$", md, re.MULTILINE))
1429        markdownfiles = [f.name for f in Path("docs/markdown").iterdir() if f.is_file() and f.suffix == '.md']
1430        exceptions = ['_Sidebar.md']
1431        for f in markdownfiles:
1432            if f not in exceptions:
1433                self.assertIn(f, toc)
1434
1435    def test_vim_syntax_highlighting(self):
1436        '''
1437        Ensure that vim syntax highlighting files were updated for new
1438        functions in the global namespace in build files.
1439        '''
1440        env = get_fake_env()
1441        interp = Interpreter(FakeBuild(env), mock=True)
1442        with open('data/syntax-highlighting/vim/syntax/meson.vim') as f:
1443            res = re.search(r'syn keyword mesonBuiltin(\s+\\\s\w+)+', f.read(), re.MULTILINE)
1444            defined = set([a.strip() for a in res.group().split('\\')][1:])
1445            self.assertEqual(defined, set(chain(interp.funcs.keys(), interp.builtin.keys())))
1446
1447    @unittest.skipIf(is_pull(), 'Skipping because this is a pull request')
1448    def test_json_grammar_syntax_highlighting(self):
1449        '''
1450        Ensure that syntax highlighting JSON grammar written by TingPing was
1451        updated for new functions in the global namespace in build files.
1452        https://github.com/TingPing/language-meson/
1453        '''
1454        env = get_fake_env()
1455        interp = Interpreter(FakeBuild(env), mock=True)
1456        url = 'https://raw.githubusercontent.com/TingPing/language-meson/master/grammars/meson.json'
1457        try:
1458            # Use a timeout to avoid blocking forever in case the network is
1459            # slow or unavailable in a weird way
1460            r = urllib.request.urlopen(url, timeout=URLOPEN_TIMEOUT)
1461        except urllib.error.URLError as e:
1462            # Skip test when network is not available, such as during packaging
1463            # by a distro or Flatpak
1464            if not isinstance(e, urllib.error.HTTPError):
1465                raise unittest.SkipTest('Network unavailable')
1466            # Don't fail the test if github is down, but do fail if 4xx
1467            if e.code >= 500:
1468                raise unittest.SkipTest('Server error ' + str(e.code))
1469            raise e
1470        # On Python 3.5, we must decode bytes to string. Newer versions don't require that.
1471        grammar = json.loads(r.read().decode('utf-8', 'surrogatepass'))
1472        for each in grammar['patterns']:
1473            if 'name' in each and each['name'] == 'support.function.builtin.meson':
1474                # The string is of the form: (?x)\\b(func1|func2|...\n)\\b\\s*(?=\\() and
1475                # we convert that to [func1, func2, ...] without using regex to parse regex
1476                funcs = set(each['match'].split('\\b(')[1].split('\n')[0].split('|'))
1477            if 'name' in each and each['name'] == 'support.variable.meson':
1478                # \\b(builtin1|builtin2...)\\b
1479                builtin = set(each['match'].split('\\b(')[1].split(')\\b')[0].split('|'))
1480        self.assertEqual(builtin, set(interp.builtin.keys()))
1481        self.assertEqual(funcs, set(interp.funcs.keys()))
1482
1483    def test_all_functions_defined_in_ast_interpreter(self):
1484        '''
1485        Ensure that the all functions defined in the Interpreter are also defined
1486        in the AstInterpreter (and vice versa).
1487        '''
1488        env = get_fake_env()
1489        interp = Interpreter(FakeBuild(env), mock=True)
1490        astint = AstInterpreter('.', '', '')
1491        self.assertEqual(set(interp.funcs.keys()), set(astint.funcs.keys()))
1492
1493    def test_mesondata_is_up_to_date(self):
1494        from mesonbuild.mesondata import mesondata
1495        err_msg = textwrap.dedent('''
1496
1497            ###########################################################
1498            ###        mesonbuild.mesondata is not up-to-date       ###
1499            ###  Please regenerate it by running tools/gen_data.py  ###
1500            ###########################################################
1501
1502        ''')
1503
1504        root_dir = Path(__file__).resolve().parent
1505        mesonbuild_dir = root_dir / 'mesonbuild'
1506
1507        data_dirs = mesonbuild_dir.glob('**/data')
1508        data_files = []  # type: T.List[T.Tuple(str, str)]
1509
1510        for i in data_dirs:
1511            for p in i.iterdir():
1512                data_files += [(p.relative_to(mesonbuild_dir).as_posix(), hashlib.sha256(p.read_bytes()).hexdigest())]
1513
1514        from pprint import pprint
1515        current_files = set(mesondata.keys())
1516        scanned_files = set([x[0] for x in data_files])
1517
1518        self.assertSetEqual(current_files, scanned_files, err_msg + 'Data files were added or removed\n')
1519        errors = []
1520        for i in data_files:
1521            if mesondata[i[0]].sha256sum != i[1]:
1522                errors += [i[0]]
1523
1524        self.assertListEqual(errors, [], err_msg + 'Files were changed')
1525
1526class BasePlatformTests(unittest.TestCase):
1527    prefix = '/usr'
1528    libdir = 'lib'
1529
1530    def setUp(self):
1531        super().setUp()
1532        self.maxDiff = None
1533        src_root = os.path.dirname(__file__)
1534        src_root = os.path.join(os.getcwd(), src_root)
1535        self.src_root = src_root
1536        # Get the backend
1537        # FIXME: Extract this from argv?
1538        self.backend = getattr(Backend, os.environ.get('MESON_UNIT_TEST_BACKEND', 'ninja'))
1539        self.meson_args = ['--backend=' + self.backend.name]
1540        self.meson_native_file = None
1541        self.meson_cross_file = None
1542        self.meson_command = python_command + [get_meson_script()]
1543        self.setup_command = self.meson_command + self.meson_args
1544        self.mconf_command = self.meson_command + ['configure']
1545        self.mintro_command = self.meson_command + ['introspect']
1546        self.wrap_command = self.meson_command + ['wrap']
1547        self.rewrite_command = self.meson_command + ['rewrite']
1548        # Backend-specific build commands
1549        self.build_command, self.clean_command, self.test_command, self.install_command, \
1550            self.uninstall_command = get_backend_commands(self.backend)
1551        # Test directories
1552        self.common_test_dir = os.path.join(src_root, 'test cases/common')
1553        self.vala_test_dir = os.path.join(src_root, 'test cases/vala')
1554        self.framework_test_dir = os.path.join(src_root, 'test cases/frameworks')
1555        self.unit_test_dir = os.path.join(src_root, 'test cases/unit')
1556        self.rewrite_test_dir = os.path.join(src_root, 'test cases/rewrite')
1557        self.linuxlike_test_dir = os.path.join(src_root, 'test cases/linuxlike')
1558        # Misc stuff
1559        self.orig_env = os.environ.copy()
1560        if self.backend is Backend.ninja:
1561            self.no_rebuild_stdout = ['ninja: no work to do.', 'samu: nothing to do']
1562        else:
1563            # VS doesn't have a stable output when no changes are done
1564            # XCode backend is untested with unit tests, help welcome!
1565            self.no_rebuild_stdout = ['UNKNOWN BACKEND {!r}'.format(self.backend.name)]
1566
1567        self.builddirs = []
1568        self.new_builddir()
1569
1570    def change_builddir(self, newdir):
1571        self.builddir = newdir
1572        self.privatedir = os.path.join(self.builddir, 'meson-private')
1573        self.logdir = os.path.join(self.builddir, 'meson-logs')
1574        self.installdir = os.path.join(self.builddir, 'install')
1575        self.distdir = os.path.join(self.builddir, 'meson-dist')
1576        self.mtest_command = self.meson_command + ['test', '-C', self.builddir]
1577        self.builddirs.append(self.builddir)
1578
1579    def new_builddir(self):
1580        if not is_cygwin():
1581            # Keep builddirs inside the source tree so that virus scanners
1582            # don't complain
1583            newdir = tempfile.mkdtemp(dir=os.getcwd())
1584        else:
1585            # But not on Cygwin because that breaks the umask tests. See:
1586            # https://github.com/mesonbuild/meson/pull/5546#issuecomment-509666523
1587            newdir = tempfile.mkdtemp()
1588        # In case the directory is inside a symlinked directory, find the real
1589        # path otherwise we might not find the srcdir from inside the builddir.
1590        newdir = os.path.realpath(newdir)
1591        self.change_builddir(newdir)
1592
1593    def _print_meson_log(self):
1594        log = os.path.join(self.logdir, 'meson-log.txt')
1595        if not os.path.isfile(log):
1596            print("{!r} doesn't exist".format(log))
1597            return
1598        with open(log, 'r', encoding='utf-8') as f:
1599            print(f.read())
1600
1601    def tearDown(self):
1602        for path in self.builddirs:
1603            try:
1604                windows_proof_rmtree(path)
1605            except FileNotFoundError:
1606                pass
1607        os.environ.clear()
1608        os.environ.update(self.orig_env)
1609        super().tearDown()
1610
1611    def _run(self, command, *, workdir=None, override_envvars=None):
1612        '''
1613        Run a command while printing the stdout and stderr to stdout,
1614        and also return a copy of it
1615        '''
1616        # If this call hangs CI will just abort. It is very hard to distinguish
1617        # between CI issue and test bug in that case. Set timeout and fail loud
1618        # instead.
1619        if override_envvars is None:
1620            env = None
1621        else:
1622            env = os.environ.copy()
1623            env.update(override_envvars)
1624
1625        p = subprocess.run(command, stdout=subprocess.PIPE,
1626                           stderr=subprocess.STDOUT, env=env,
1627                           universal_newlines=True, cwd=workdir, timeout=60 * 5)
1628        print(p.stdout)
1629        if p.returncode != 0:
1630            if 'MESON_SKIP_TEST' in p.stdout:
1631                raise unittest.SkipTest('Project requested skipping.')
1632            raise subprocess.CalledProcessError(p.returncode, command, output=p.stdout)
1633        return p.stdout
1634
1635    def init(self, srcdir, *,
1636             extra_args=None,
1637             default_args=True,
1638             inprocess=False,
1639             override_envvars=None,
1640             workdir=None):
1641        self.assertPathExists(srcdir)
1642        if extra_args is None:
1643            extra_args = []
1644        if not isinstance(extra_args, list):
1645            extra_args = [extra_args]
1646        args = [srcdir, self.builddir]
1647        if default_args:
1648            args += ['--prefix', self.prefix]
1649            if self.libdir:
1650                args += ['--libdir', self.libdir]
1651            if self.meson_native_file:
1652                args += ['--native-file', self.meson_native_file]
1653            if self.meson_cross_file:
1654                args += ['--cross-file', self.meson_cross_file]
1655        self.privatedir = os.path.join(self.builddir, 'meson-private')
1656        if inprocess:
1657            try:
1658                if override_envvars is not None:
1659                    old_envvars = os.environ.copy()
1660                    os.environ.update(override_envvars)
1661                (returncode, out, err) = run_configure_inprocess(self.meson_args + args + extra_args)
1662                if override_envvars is not None:
1663                    os.environ.clear()
1664                    os.environ.update(old_envvars)
1665                if 'MESON_SKIP_TEST' in out:
1666                    raise unittest.SkipTest('Project requested skipping.')
1667                if returncode != 0:
1668                    self._print_meson_log()
1669                    print('Stdout:\n')
1670                    print(out)
1671                    print('Stderr:\n')
1672                    print(err)
1673                    raise RuntimeError('Configure failed')
1674            except Exception:
1675                self._print_meson_log()
1676                raise
1677            finally:
1678                # Close log file to satisfy Windows file locking
1679                mesonbuild.mlog.shutdown()
1680                mesonbuild.mlog.log_dir = None
1681                mesonbuild.mlog.log_file = None
1682        else:
1683            try:
1684                out = self._run(self.setup_command + args + extra_args, override_envvars=override_envvars, workdir=workdir)
1685            except unittest.SkipTest:
1686                raise unittest.SkipTest('Project requested skipping: ' + srcdir)
1687            except Exception:
1688                self._print_meson_log()
1689                raise
1690        return out
1691
1692    def build(self, target=None, *, extra_args=None, override_envvars=None):
1693        if extra_args is None:
1694            extra_args = []
1695        # Add arguments for building the target (if specified),
1696        # and using the build dir (if required, with VS)
1697        args = get_builddir_target_args(self.backend, self.builddir, target)
1698        return self._run(self.build_command + args + extra_args, workdir=self.builddir, override_envvars=override_envvars)
1699
1700    def clean(self, *, override_envvars=None):
1701        dir_args = get_builddir_target_args(self.backend, self.builddir, None)
1702        self._run(self.clean_command + dir_args, workdir=self.builddir, override_envvars=override_envvars)
1703
1704    def run_tests(self, *, inprocess=False, override_envvars=None):
1705        if not inprocess:
1706            self._run(self.test_command, workdir=self.builddir, override_envvars=override_envvars)
1707        else:
1708            if override_envvars is not None:
1709                old_envvars = os.environ.copy()
1710                os.environ.update(override_envvars)
1711            try:
1712                run_mtest_inprocess(['-C', self.builddir])
1713            finally:
1714                if override_envvars is not None:
1715                    os.environ.clear()
1716                    os.environ.update(old_envvars)
1717
1718    def install(self, *, use_destdir=True, override_envvars=None):
1719        if self.backend is not Backend.ninja:
1720            raise unittest.SkipTest('{!r} backend can\'t install files'.format(self.backend.name))
1721        if use_destdir:
1722            destdir = {'DESTDIR': self.installdir}
1723            if override_envvars is None:
1724                override_envvars = destdir
1725            else:
1726                override_envvars.update(destdir)
1727        self._run(self.install_command, workdir=self.builddir, override_envvars=override_envvars)
1728
1729    def uninstall(self, *, override_envvars=None):
1730        self._run(self.uninstall_command, workdir=self.builddir, override_envvars=override_envvars)
1731
1732    def run_target(self, target, *, override_envvars=None):
1733        '''
1734        Run a Ninja target while printing the stdout and stderr to stdout,
1735        and also return a copy of it
1736        '''
1737        return self.build(target=target, override_envvars=override_envvars)
1738
1739    def setconf(self, arg, will_build=True):
1740        if not isinstance(arg, list):
1741            arg = [arg]
1742        if will_build:
1743            ensure_backend_detects_changes(self.backend)
1744        self._run(self.mconf_command + arg + [self.builddir])
1745
1746    def wipe(self):
1747        windows_proof_rmtree(self.builddir)
1748
1749    def utime(self, f):
1750        ensure_backend_detects_changes(self.backend)
1751        os.utime(f)
1752
1753    def get_compdb(self):
1754        if self.backend is not Backend.ninja:
1755            raise unittest.SkipTest('Compiler db not available with {} backend'.format(self.backend.name))
1756        try:
1757            with open(os.path.join(self.builddir, 'compile_commands.json')) as ifile:
1758                contents = json.load(ifile)
1759        except FileNotFoundError:
1760            raise unittest.SkipTest('Compiler db not found')
1761        # If Ninja is using .rsp files, generate them, read their contents, and
1762        # replace it as the command for all compile commands in the parsed json.
1763        if len(contents) > 0 and contents[0]['command'].endswith('.rsp'):
1764            # Pretend to build so that the rsp files are generated
1765            self.build(extra_args=['-d', 'keeprsp', '-n'])
1766            for each in contents:
1767                # Extract the actual command from the rsp file
1768                compiler, rsp = each['command'].split(' @')
1769                rsp = os.path.join(self.builddir, rsp)
1770                # Replace the command with its contents
1771                with open(rsp, 'r', encoding='utf-8') as f:
1772                    each['command'] = compiler + ' ' + f.read()
1773        return contents
1774
1775    def get_meson_log(self):
1776        with open(os.path.join(self.builddir, 'meson-logs', 'meson-log.txt')) as f:
1777            return f.readlines()
1778
1779    def get_meson_log_compiler_checks(self):
1780        '''
1781        Fetch a list command-lines run by meson for compiler checks.
1782        Each command-line is returned as a list of arguments.
1783        '''
1784        log = self.get_meson_log()
1785        prefix = 'Command line:'
1786        cmds = [l[len(prefix):].split() for l in log if l.startswith(prefix)]
1787        return cmds
1788
1789    def get_meson_log_sanitychecks(self):
1790        '''
1791        Same as above, but for the sanity checks that were run
1792        '''
1793        log = self.get_meson_log()
1794        prefix = 'Sanity check compiler command line:'
1795        cmds = [l[len(prefix):].split() for l in log if l.startswith(prefix)]
1796        return cmds
1797
1798    def introspect(self, args):
1799        if isinstance(args, str):
1800            args = [args]
1801        out = subprocess.check_output(self.mintro_command + args + [self.builddir],
1802                                      universal_newlines=True)
1803        return json.loads(out)
1804
1805    def introspect_directory(self, directory, args):
1806        if isinstance(args, str):
1807            args = [args]
1808        out = subprocess.check_output(self.mintro_command + args + [directory],
1809                                      universal_newlines=True)
1810        try:
1811            obj = json.loads(out)
1812        except Exception as e:
1813            print(out)
1814            raise e
1815        return obj
1816
1817    def assertPathEqual(self, path1, path2):
1818        '''
1819        Handles a lot of platform-specific quirks related to paths such as
1820        separator, case-sensitivity, etc.
1821        '''
1822        self.assertEqual(PurePath(path1), PurePath(path2))
1823
1824    def assertPathListEqual(self, pathlist1, pathlist2):
1825        self.assertEqual(len(pathlist1), len(pathlist2))
1826        worklist = list(zip(pathlist1, pathlist2))
1827        for i in worklist:
1828            if i[0] is None:
1829                self.assertEqual(i[0], i[1])
1830            else:
1831                self.assertPathEqual(i[0], i[1])
1832
1833    def assertPathBasenameEqual(self, path, basename):
1834        msg = '{!r} does not end with {!r}'.format(path, basename)
1835        # We cannot use os.path.basename because it returns '' when the path
1836        # ends with '/' for some silly reason. This is not how the UNIX utility
1837        # `basename` works.
1838        path_basename = PurePath(path).parts[-1]
1839        self.assertEqual(PurePath(path_basename), PurePath(basename), msg)
1840
1841    def assertReconfiguredBuildIsNoop(self):
1842        'Assert that we reconfigured and then there was nothing to do'
1843        ret = self.build()
1844        self.assertIn('The Meson build system', ret)
1845        if self.backend is Backend.ninja:
1846            for line in ret.split('\n'):
1847                if line in self.no_rebuild_stdout:
1848                    break
1849            else:
1850                raise AssertionError('build was reconfigured, but was not no-op')
1851        elif self.backend is Backend.vs:
1852            # Ensure that some target said that no rebuild was done
1853            # XXX: Note CustomBuild did indeed rebuild, because of the regen checker!
1854            self.assertIn('ClCompile:\n  All outputs are up-to-date.', ret)
1855            self.assertIn('Link:\n  All outputs are up-to-date.', ret)
1856            # Ensure that no targets were built
1857            self.assertNotRegex(ret, re.compile('ClCompile:\n [^\n]*cl', flags=re.IGNORECASE))
1858            self.assertNotRegex(ret, re.compile('Link:\n [^\n]*link', flags=re.IGNORECASE))
1859        elif self.backend is Backend.xcode:
1860            raise unittest.SkipTest('Please help us fix this test on the xcode backend')
1861        else:
1862            raise RuntimeError('Invalid backend: {!r}'.format(self.backend.name))
1863
1864    def assertBuildIsNoop(self):
1865        ret = self.build()
1866        if self.backend is Backend.ninja:
1867            self.assertIn(ret.split('\n')[-2], self.no_rebuild_stdout)
1868        elif self.backend is Backend.vs:
1869            # Ensure that some target of each type said that no rebuild was done
1870            # We always have at least one CustomBuild target for the regen checker
1871            self.assertIn('CustomBuild:\n  All outputs are up-to-date.', ret)
1872            self.assertIn('ClCompile:\n  All outputs are up-to-date.', ret)
1873            self.assertIn('Link:\n  All outputs are up-to-date.', ret)
1874            # Ensure that no targets were built
1875            self.assertNotRegex(ret, re.compile('CustomBuild:\n [^\n]*cl', flags=re.IGNORECASE))
1876            self.assertNotRegex(ret, re.compile('ClCompile:\n [^\n]*cl', flags=re.IGNORECASE))
1877            self.assertNotRegex(ret, re.compile('Link:\n [^\n]*link', flags=re.IGNORECASE))
1878        elif self.backend is Backend.xcode:
1879            raise unittest.SkipTest('Please help us fix this test on the xcode backend')
1880        else:
1881            raise RuntimeError('Invalid backend: {!r}'.format(self.backend.name))
1882
1883    def assertRebuiltTarget(self, target):
1884        ret = self.build()
1885        if self.backend is Backend.ninja:
1886            self.assertIn('Linking target {}'.format(target), ret)
1887        elif self.backend is Backend.vs:
1888            # Ensure that this target was rebuilt
1889            linkre = re.compile('Link:\n [^\n]*link[^\n]*' + target, flags=re.IGNORECASE)
1890            self.assertRegex(ret, linkre)
1891        elif self.backend is Backend.xcode:
1892            raise unittest.SkipTest('Please help us fix this test on the xcode backend')
1893        else:
1894            raise RuntimeError('Invalid backend: {!r}'.format(self.backend.name))
1895
1896    @staticmethod
1897    def get_target_from_filename(filename):
1898        base = os.path.splitext(filename)[0]
1899        if base.startswith(('lib', 'cyg')):
1900            return base[3:]
1901        return base
1902
1903    def assertBuildRelinkedOnlyTarget(self, target):
1904        ret = self.build()
1905        if self.backend is Backend.ninja:
1906            linked_targets = []
1907            for line in ret.split('\n'):
1908                if 'Linking target' in line:
1909                    fname = line.rsplit('target ')[-1]
1910                    linked_targets.append(self.get_target_from_filename(fname))
1911            self.assertEqual(linked_targets, [target])
1912        elif self.backend is Backend.vs:
1913            # Ensure that this target was rebuilt
1914            linkre = re.compile(r'Link:\n  [^\n]*link.exe[^\n]*/OUT:".\\([^"]*)"', flags=re.IGNORECASE)
1915            matches = linkre.findall(ret)
1916            self.assertEqual(len(matches), 1, msg=matches)
1917            self.assertEqual(self.get_target_from_filename(matches[0]), target)
1918        elif self.backend is Backend.xcode:
1919            raise unittest.SkipTest('Please help us fix this test on the xcode backend')
1920        else:
1921            raise RuntimeError('Invalid backend: {!r}'.format(self.backend.name))
1922
1923    def assertPathExists(self, path):
1924        m = 'Path {!r} should exist'.format(path)
1925        self.assertTrue(os.path.exists(path), msg=m)
1926
1927    def assertPathDoesNotExist(self, path):
1928        m = 'Path {!r} should not exist'.format(path)
1929        self.assertFalse(os.path.exists(path), msg=m)
1930
1931
1932class AllPlatformTests(BasePlatformTests):
1933    '''
1934    Tests that should run on all platforms
1935    '''
1936
1937    def test_default_options_prefix(self):
1938        '''
1939        Tests that setting a prefix in default_options in project() works.
1940        Can't be an ordinary test because we pass --prefix to meson there.
1941        https://github.com/mesonbuild/meson/issues/1349
1942        '''
1943        testdir = os.path.join(self.common_test_dir, '90 default options')
1944        self.init(testdir, default_args=False)
1945        opts = self.introspect('--buildoptions')
1946        for opt in opts:
1947            if opt['name'] == 'prefix':
1948                prefix = opt['value']
1949        self.assertEqual(prefix, '/absoluteprefix')
1950
1951    def test_do_conf_file_preserve_newlines(self):
1952
1953        def conf_file(in_data, confdata):
1954            with temp_filename() as fin:
1955                with open(fin, 'wb') as fobj:
1956                    fobj.write(in_data.encode('utf-8'))
1957                with temp_filename() as fout:
1958                    mesonbuild.mesonlib.do_conf_file(fin, fout, confdata, 'meson')
1959                    with open(fout, 'rb') as fobj:
1960                        return fobj.read().decode('utf-8')
1961
1962        confdata = {'VAR': ('foo', 'bar')}
1963        self.assertEqual(conf_file('@VAR@\n@VAR@\n', confdata), 'foo\nfoo\n')
1964        self.assertEqual(conf_file('@VAR@\r\n@VAR@\r\n', confdata), 'foo\r\nfoo\r\n')
1965
1966    def test_do_conf_file_by_format(self):
1967        def conf_str(in_data, confdata, vformat):
1968            (result, missing_variables, confdata_useless) = mesonbuild.mesonlib.do_conf_str(in_data, confdata, variable_format = vformat)
1969            return '\n'.join(result)
1970
1971        def check_formats(confdata, result):
1972            self.assertEqual(conf_str(['#mesondefine VAR'], confdata, 'meson'), result)
1973            self.assertEqual(conf_str(['#cmakedefine VAR ${VAR}'], confdata, 'cmake'), result)
1974            self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), result)
1975
1976        confdata = ConfigurationData()
1977        # Key error as they do not exists
1978        check_formats(confdata, '/* #undef VAR */\n')
1979
1980        # Check boolean
1981        confdata.values = {'VAR': (False, 'description')}
1982        check_formats(confdata, '#undef VAR\n')
1983        confdata.values = {'VAR': (True, 'description')}
1984        check_formats(confdata, '#define VAR\n')
1985
1986        # Check string
1987        confdata.values = {'VAR': ('value', 'description')}
1988        check_formats(confdata, '#define VAR value\n')
1989
1990        # Check integer
1991        confdata.values = {'VAR': (10, 'description')}
1992        check_formats(confdata, '#define VAR 10\n')
1993
1994        # Check multiple string with cmake formats
1995        confdata.values = {'VAR': ('value', 'description')}
1996        self.assertEqual(conf_str(['#cmakedefine VAR xxx @VAR@ yyy @VAR@'], confdata, 'cmake@'), '#define VAR xxx value yyy value\n')
1997        self.assertEqual(conf_str(['#define VAR xxx @VAR@ yyy @VAR@'], confdata, 'cmake@'), '#define VAR xxx value yyy value')
1998        self.assertEqual(conf_str(['#cmakedefine VAR xxx ${VAR} yyy ${VAR}'], confdata, 'cmake'), '#define VAR xxx value yyy value\n')
1999        self.assertEqual(conf_str(['#define VAR xxx ${VAR} yyy ${VAR}'], confdata, 'cmake'), '#define VAR xxx value yyy value')
2000
2001        # Handles meson format exceptions
2002        #   Unknown format
2003        self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#mesondefine VAR xxx'], confdata, 'unknown_format')
2004        #   More than 2 params in mesondefine
2005        self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#mesondefine VAR xxx'], confdata, 'meson')
2006        #   Mismatched line with format
2007        self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#cmakedefine VAR'], confdata, 'meson')
2008        self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#mesondefine VAR'], confdata, 'cmake')
2009        self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#mesondefine VAR'], confdata, 'cmake@')
2010        #   Dict value in confdata
2011        confdata.values = {'VAR': (['value'], 'description')}
2012        self.assertRaises(mesonbuild.mesonlib.MesonException, conf_str, ['#mesondefine VAR'], confdata, 'meson')
2013
2014    def test_absolute_prefix_libdir(self):
2015        '''
2016        Tests that setting absolute paths for --prefix and --libdir work. Can't
2017        be an ordinary test because these are set via the command-line.
2018        https://github.com/mesonbuild/meson/issues/1341
2019        https://github.com/mesonbuild/meson/issues/1345
2020        '''
2021        testdir = os.path.join(self.common_test_dir, '90 default options')
2022        # on Windows, /someabs is *not* an absolute path
2023        prefix = 'x:/someabs' if is_windows() else '/someabs'
2024        libdir = 'libdir'
2025        extra_args = ['--prefix=' + prefix,
2026                      # This can just be a relative path, but we want to test
2027                      # that passing this as an absolute path also works
2028                      '--libdir=' + prefix + '/' + libdir]
2029        self.init(testdir, extra_args=extra_args, default_args=False)
2030        opts = self.introspect('--buildoptions')
2031        for opt in opts:
2032            if opt['name'] == 'prefix':
2033                self.assertEqual(prefix, opt['value'])
2034            elif opt['name'] == 'libdir':
2035                self.assertEqual(libdir, opt['value'])
2036
2037    def test_libdir_must_be_inside_prefix(self):
2038        '''
2039        Tests that libdir is forced to be inside prefix no matter how it is set.
2040        Must be a unit test for obvious reasons.
2041        '''
2042        testdir = os.path.join(self.common_test_dir, '1 trivial')
2043        # libdir being inside prefix is ok
2044        if is_windows():
2045            args = ['--prefix', 'x:/opt', '--libdir', 'x:/opt/lib32']
2046        else:
2047            args = ['--prefix', '/opt', '--libdir', '/opt/lib32']
2048        self.init(testdir, extra_args=args)
2049        self.wipe()
2050        # libdir not being inside prefix is not ok
2051        if is_windows():
2052            args = ['--prefix', 'x:/usr', '--libdir', 'x:/opt/lib32']
2053        else:
2054            args = ['--prefix', '/usr', '--libdir', '/opt/lib32']
2055        self.assertRaises(subprocess.CalledProcessError, self.init, testdir, extra_args=args)
2056        self.wipe()
2057        # libdir must be inside prefix even when set via mesonconf
2058        self.init(testdir)
2059        if is_windows():
2060            self.assertRaises(subprocess.CalledProcessError, self.setconf, '-Dlibdir=x:/opt', False)
2061        else:
2062            self.assertRaises(subprocess.CalledProcessError, self.setconf, '-Dlibdir=/opt', False)
2063
2064    def test_prefix_dependent_defaults(self):
2065        '''
2066        Tests that configured directory paths are set to prefix dependent
2067        defaults.
2068        '''
2069        testdir = os.path.join(self.common_test_dir, '1 trivial')
2070        expected = {
2071            '/opt': {'prefix': '/opt',
2072                     'bindir': 'bin', 'datadir': 'share', 'includedir': 'include',
2073                     'infodir': 'share/info',
2074                     'libexecdir': 'libexec', 'localedir': 'share/locale',
2075                     'localstatedir': 'var', 'mandir': 'share/man',
2076                     'sbindir': 'sbin', 'sharedstatedir': 'com',
2077                     'sysconfdir': 'etc'},
2078            '/usr': {'prefix': '/usr',
2079                     'bindir': 'bin', 'datadir': 'share', 'includedir': 'include',
2080                     'infodir': 'share/info',
2081                     'libexecdir': 'libexec', 'localedir': 'share/locale',
2082                     'localstatedir': '/var', 'mandir': 'share/man',
2083                     'sbindir': 'sbin', 'sharedstatedir': '/var/lib',
2084                     'sysconfdir': '/etc'},
2085            '/usr/local': {'prefix': '/usr/local',
2086                           'bindir': 'bin', 'datadir': 'share',
2087                           'includedir': 'include', 'infodir': 'share/info',
2088                           'libexecdir': 'libexec',
2089                           'localedir': 'share/locale',
2090                           'localstatedir': '/var/local', 'mandir': 'share/man',
2091                           'sbindir': 'sbin', 'sharedstatedir': '/var/local/lib',
2092                           'sysconfdir': 'etc'},
2093            # N.B. We don't check 'libdir' as it's platform dependent, see
2094            # default_libdir():
2095        }
2096
2097        if mesonbuild.mesonlib.default_prefix() == '/usr/local':
2098            expected[None] = expected['/usr/local']
2099
2100        for prefix in expected:
2101            args = []
2102            if prefix:
2103                args += ['--prefix', prefix]
2104            self.init(testdir, extra_args=args, default_args=False)
2105            opts = self.introspect('--buildoptions')
2106            for opt in opts:
2107                name = opt['name']
2108                value = opt['value']
2109                if name in expected[prefix]:
2110                    self.assertEqual(value, expected[prefix][name])
2111            self.wipe()
2112
2113    def test_default_options_prefix_dependent_defaults(self):
2114        '''
2115        Tests that setting a prefix in default_options in project() sets prefix
2116        dependent defaults for other options, and that those defaults can
2117        be overridden in default_options or by the command line.
2118        '''
2119        testdir = os.path.join(self.common_test_dir, '168 default options prefix dependent defaults')
2120        expected = {
2121            '':
2122            {'prefix':         '/usr',
2123             'sysconfdir':     '/etc',
2124             'localstatedir':  '/var',
2125             'sharedstatedir': '/sharedstate'},
2126            '--prefix=/usr':
2127            {'prefix':         '/usr',
2128             'sysconfdir':     '/etc',
2129             'localstatedir':  '/var',
2130             'sharedstatedir': '/sharedstate'},
2131            '--sharedstatedir=/var/state':
2132            {'prefix':         '/usr',
2133             'sysconfdir':     '/etc',
2134             'localstatedir':  '/var',
2135             'sharedstatedir': '/var/state'},
2136            '--sharedstatedir=/var/state --prefix=/usr --sysconfdir=sysconf':
2137            {'prefix':         '/usr',
2138             'sysconfdir':     'sysconf',
2139             'localstatedir':  '/var',
2140             'sharedstatedir': '/var/state'},
2141        }
2142        for args in expected:
2143            self.init(testdir, extra_args=args.split(), default_args=False)
2144            opts = self.introspect('--buildoptions')
2145            for opt in opts:
2146                name = opt['name']
2147                value = opt['value']
2148                if name in expected[args]:
2149                    self.assertEqual(value, expected[args][name])
2150            self.wipe()
2151
2152    def test_clike_get_library_dirs(self):
2153        env = get_fake_env()
2154        cc = env.detect_c_compiler(MachineChoice.HOST)
2155        for d in cc.get_library_dirs(env):
2156            self.assertTrue(os.path.exists(d))
2157            self.assertTrue(os.path.isdir(d))
2158            self.assertTrue(os.path.isabs(d))
2159
2160    def test_static_library_overwrite(self):
2161        '''
2162        Tests that static libraries are never appended to, always overwritten.
2163        Has to be a unit test because this involves building a project,
2164        reconfiguring, and building it again so that `ar` is run twice on the
2165        same static library.
2166        https://github.com/mesonbuild/meson/issues/1355
2167        '''
2168        testdir = os.path.join(self.common_test_dir, '3 static')
2169        env = get_fake_env(testdir, self.builddir, self.prefix)
2170        cc = env.detect_c_compiler(MachineChoice.HOST)
2171        static_linker = env.detect_static_linker(cc)
2172        if is_windows():
2173            raise unittest.SkipTest('https://github.com/mesonbuild/meson/issues/1526')
2174        if not isinstance(static_linker, mesonbuild.linkers.ArLinker):
2175            raise unittest.SkipTest('static linker is not `ar`')
2176        # Configure
2177        self.init(testdir)
2178        # Get name of static library
2179        targets = self.introspect('--targets')
2180        self.assertEqual(len(targets), 1)
2181        libname = targets[0]['filename'][0]
2182        # Build and get contents of static library
2183        self.build()
2184        before = self._run(['ar', 't', os.path.join(self.builddir, libname)]).split()
2185        # Filter out non-object-file contents
2186        before = [f for f in before if f.endswith(('.o', '.obj'))]
2187        # Static library should contain only one object
2188        self.assertEqual(len(before), 1, msg=before)
2189        # Change the source to be built into the static library
2190        self.setconf('-Dsource=libfile2.c')
2191        self.build()
2192        after = self._run(['ar', 't', os.path.join(self.builddir, libname)]).split()
2193        # Filter out non-object-file contents
2194        after = [f for f in after if f.endswith(('.o', '.obj'))]
2195        # Static library should contain only one object
2196        self.assertEqual(len(after), 1, msg=after)
2197        # and the object must have changed
2198        self.assertNotEqual(before, after)
2199
2200    def test_static_compile_order(self):
2201        '''
2202        Test that the order of files in a compiler command-line while compiling
2203        and linking statically is deterministic. This can't be an ordinary test
2204        case because we need to inspect the compiler database.
2205        https://github.com/mesonbuild/meson/pull/951
2206        '''
2207        testdir = os.path.join(self.common_test_dir, '5 linkstatic')
2208        self.init(testdir)
2209        compdb = self.get_compdb()
2210        # Rules will get written out in this order
2211        self.assertTrue(compdb[0]['file'].endswith("libfile.c"))
2212        self.assertTrue(compdb[1]['file'].endswith("libfile2.c"))
2213        self.assertTrue(compdb[2]['file'].endswith("libfile3.c"))
2214        self.assertTrue(compdb[3]['file'].endswith("libfile4.c"))
2215        # FIXME: We don't have access to the linker command
2216
2217    def test_run_target_files_path(self):
2218        '''
2219        Test that run_targets are run from the correct directory
2220        https://github.com/mesonbuild/meson/issues/957
2221        '''
2222        testdir = os.path.join(self.common_test_dir, '54 run target')
2223        self.init(testdir)
2224        self.run_target('check_exists')
2225
2226    def test_install_introspection(self):
2227        '''
2228        Tests that the Meson introspection API exposes install filenames correctly
2229        https://github.com/mesonbuild/meson/issues/829
2230        '''
2231        if self.backend is not Backend.ninja:
2232            raise unittest.SkipTest('{!r} backend can\'t install files'.format(self.backend.name))
2233        testdir = os.path.join(self.common_test_dir, '8 install')
2234        self.init(testdir)
2235        intro = self.introspect('--targets')
2236        if intro[0]['type'] == 'executable':
2237            intro = intro[::-1]
2238        self.assertPathListEqual(intro[0]['install_filename'], ['/usr/lib/libstat.a'])
2239        self.assertPathListEqual(intro[1]['install_filename'], ['/usr/bin/prog' + exe_suffix])
2240
2241    def test_install_subdir_introspection(self):
2242        '''
2243        Test that the Meson introspection API also contains subdir install information
2244        https://github.com/mesonbuild/meson/issues/5556
2245        '''
2246        testdir = os.path.join(self.common_test_dir, '62 install subdir')
2247        self.init(testdir)
2248        intro = self.introspect('--installed')
2249        expected = {
2250            'sub2': 'share/sub2',
2251            'subdir/sub1': 'share/sub1',
2252            'subdir/sub_elided': 'share',
2253            'sub1': 'share/sub1',
2254            'sub/sub1': 'share/sub1',
2255            'sub_elided': 'share',
2256            'nested_elided/sub': 'share',
2257        }
2258
2259        self.assertEqual(len(intro), len(expected))
2260
2261        # Convert expected to PurePath
2262        expected_converted = {PurePath(os.path.join(testdir, key)): PurePath(os.path.join(self.prefix, val)) for key, val in expected.items()}
2263        intro_converted = {PurePath(key): PurePath(val) for key, val in intro.items()}
2264
2265        for src, dst in expected_converted.items():
2266            self.assertIn(src, intro_converted)
2267            self.assertEqual(dst, intro_converted[src])
2268
2269    def test_install_introspection_multiple_outputs(self):
2270        '''
2271        Tests that the Meson introspection API exposes multiple install filenames correctly without crashing
2272        https://github.com/mesonbuild/meson/pull/4555
2273
2274        Reverted to the first file only because of https://github.com/mesonbuild/meson/pull/4547#discussion_r244173438
2275        TODO Change the format to a list officially in a followup PR
2276        '''
2277        if self.backend is not Backend.ninja:
2278            raise unittest.SkipTest('{!r} backend can\'t install files'.format(self.backend.name))
2279        testdir = os.path.join(self.common_test_dir, '144 custom target multiple outputs')
2280        self.init(testdir)
2281        intro = self.introspect('--targets')
2282        if intro[0]['type'] == 'executable':
2283            intro = intro[::-1]
2284        self.assertPathListEqual(intro[0]['install_filename'], ['/usr/include/diff.h', '/usr/bin/diff.sh'])
2285        self.assertPathListEqual(intro[1]['install_filename'], ['/opt/same.h', '/opt/same.sh'])
2286        self.assertPathListEqual(intro[2]['install_filename'], ['/usr/include/first.h', None])
2287        self.assertPathListEqual(intro[3]['install_filename'], [None, '/usr/bin/second.sh'])
2288
2289    def test_install_log_content(self):
2290        '''
2291        Tests that the install-log.txt is consistent with the installed files and directories.
2292        Specifically checks that the log file only contains one entry per file/directory.
2293        https://github.com/mesonbuild/meson/issues/4499
2294        '''
2295        testdir = os.path.join(self.common_test_dir, '62 install subdir')
2296        self.init(testdir)
2297        self.install()
2298        installpath = Path(self.installdir)
2299        # Find installed files and directories
2300        expected = {installpath: 0}
2301        for name in installpath.rglob('*'):
2302            expected[name] = 0
2303        # Find logged files and directories
2304        with Path(self.builddir, 'meson-logs', 'install-log.txt').open() as f:
2305            logged = list(map(lambda l: Path(l.strip()),
2306                              filter(lambda l: not l.startswith('#'),
2307                                     f.readlines())))
2308        for name in logged:
2309            self.assertTrue(name in expected, 'Log contains extra entry {}'.format(name))
2310            expected[name] += 1
2311
2312        for name, count in expected.items():
2313            self.assertGreater(count, 0, 'Log is missing entry for {}'.format(name))
2314            self.assertLess(count, 2, 'Log has multiple entries for {}'.format(name))
2315
2316    def test_uninstall(self):
2317        exename = os.path.join(self.installdir, 'usr/bin/prog' + exe_suffix)
2318        testdir = os.path.join(self.common_test_dir, '8 install')
2319        self.init(testdir)
2320        self.assertPathDoesNotExist(exename)
2321        self.install()
2322        self.assertPathExists(exename)
2323        self.uninstall()
2324        self.assertPathDoesNotExist(exename)
2325
2326    def test_forcefallback(self):
2327        testdir = os.path.join(self.unit_test_dir, '31 forcefallback')
2328        self.init(testdir, extra_args=['--wrap-mode=forcefallback'])
2329        self.build()
2330        self.run_tests()
2331
2332    def test_force_fallback_for(self):
2333        testdir = os.path.join(self.unit_test_dir, '31 forcefallback')
2334        self.init(testdir, extra_args=['--force-fallback-for=zlib,foo'])
2335        self.build()
2336        self.run_tests()
2337
2338    def test_env_ops_dont_stack(self):
2339        '''
2340        Test that env ops prepend/append do not stack, and that this usage issues a warning
2341        '''
2342        testdir = os.path.join(self.unit_test_dir, '63 test env does not stack')
2343        out = self.init(testdir)
2344        self.assertRegex(out, r'WARNING: Overriding.*TEST_VAR_APPEND')
2345        self.assertRegex(out, r'WARNING: Overriding.*TEST_VAR_PREPEND')
2346        self.assertNotRegex(out, r'WARNING: Overriding.*TEST_VAR_SET')
2347        self.run_tests()
2348
2349    def test_testsetups(self):
2350        if not shutil.which('valgrind'):
2351            raise unittest.SkipTest('Valgrind not installed.')
2352        testdir = os.path.join(self.unit_test_dir, '2 testsetups')
2353        self.init(testdir)
2354        self.build()
2355        # Run tests without setup
2356        self.run_tests()
2357        with open(os.path.join(self.logdir, 'testlog.txt')) as f:
2358            basic_log = f.read()
2359        # Run buggy test with setup that has env that will make it fail
2360        self.assertRaises(subprocess.CalledProcessError,
2361                          self._run, self.mtest_command + ['--setup=valgrind'])
2362        with open(os.path.join(self.logdir, 'testlog-valgrind.txt')) as f:
2363            vg_log = f.read()
2364        self.assertFalse('TEST_ENV is set' in basic_log)
2365        self.assertFalse('Memcheck' in basic_log)
2366        self.assertTrue('TEST_ENV is set' in vg_log)
2367        self.assertTrue('Memcheck' in vg_log)
2368        # Run buggy test with setup without env that will pass
2369        self._run(self.mtest_command + ['--setup=wrapper'])
2370        # Setup with no properties works
2371        self._run(self.mtest_command + ['--setup=empty'])
2372        # Setup with only env works
2373        self._run(self.mtest_command + ['--setup=onlyenv'])
2374        self._run(self.mtest_command + ['--setup=onlyenv2'])
2375        self._run(self.mtest_command + ['--setup=onlyenv3'])
2376        # Setup with only a timeout works
2377        self._run(self.mtest_command + ['--setup=timeout'])
2378
2379    def test_testsetup_selection(self):
2380        testdir = os.path.join(self.unit_test_dir, '14 testsetup selection')
2381        self.init(testdir)
2382        self.build()
2383
2384        # Run tests without setup
2385        self.run_tests()
2386
2387        self.assertRaises(subprocess.CalledProcessError, self._run, self.mtest_command + ['--setup=missingfromfoo'])
2388        self._run(self.mtest_command + ['--setup=missingfromfoo', '--no-suite=foo:'])
2389
2390        self._run(self.mtest_command + ['--setup=worksforall'])
2391        self._run(self.mtest_command + ['--setup=main:worksforall'])
2392
2393        self.assertRaises(subprocess.CalledProcessError, self._run,
2394                          self.mtest_command + ['--setup=onlyinbar'])
2395        self.assertRaises(subprocess.CalledProcessError, self._run,
2396                          self.mtest_command + ['--setup=onlyinbar', '--no-suite=main:'])
2397        self._run(self.mtest_command + ['--setup=onlyinbar', '--no-suite=main:', '--no-suite=foo:'])
2398        self._run(self.mtest_command + ['--setup=bar:onlyinbar'])
2399        self.assertRaises(subprocess.CalledProcessError, self._run,
2400                          self.mtest_command + ['--setup=foo:onlyinbar'])
2401        self.assertRaises(subprocess.CalledProcessError, self._run,
2402                          self.mtest_command + ['--setup=main:onlyinbar'])
2403
2404    def test_testsetup_default(self):
2405        testdir = os.path.join(self.unit_test_dir, '49 testsetup default')
2406        self.init(testdir)
2407        self.build()
2408
2409        # Run tests without --setup will cause the default setup to be used
2410        self.run_tests()
2411        with open(os.path.join(self.logdir, 'testlog.txt')) as f:
2412            default_log = f.read()
2413
2414        # Run tests with explicitly using the same setup that is set as default
2415        self._run(self.mtest_command + ['--setup=mydefault'])
2416        with open(os.path.join(self.logdir, 'testlog-mydefault.txt')) as f:
2417            mydefault_log = f.read()
2418
2419        # Run tests with another setup
2420        self._run(self.mtest_command + ['--setup=other'])
2421        with open(os.path.join(self.logdir, 'testlog-other.txt')) as f:
2422            other_log = f.read()
2423
2424        self.assertTrue('ENV_A is 1' in default_log)
2425        self.assertTrue('ENV_B is 2' in default_log)
2426        self.assertTrue('ENV_C is 2' in default_log)
2427
2428        self.assertTrue('ENV_A is 1' in mydefault_log)
2429        self.assertTrue('ENV_B is 2' in mydefault_log)
2430        self.assertTrue('ENV_C is 2' in mydefault_log)
2431
2432        self.assertTrue('ENV_A is 1' in other_log)
2433        self.assertTrue('ENV_B is 3' in other_log)
2434        self.assertTrue('ENV_C is 2' in other_log)
2435
2436    def assertFailedTestCount(self, failure_count, command):
2437        try:
2438            self._run(command)
2439            self.assertEqual(0, failure_count, 'Expected %d tests to fail.' % failure_count)
2440        except subprocess.CalledProcessError as e:
2441            self.assertEqual(e.returncode, failure_count)
2442
2443    def test_suite_selection(self):
2444        testdir = os.path.join(self.unit_test_dir, '4 suite selection')
2445        self.init(testdir)
2446        self.build()
2447
2448        self.assertFailedTestCount(4, self.mtest_command)
2449
2450        self.assertFailedTestCount(0, self.mtest_command + ['--suite', ':success'])
2451        self.assertFailedTestCount(3, self.mtest_command + ['--suite', ':fail'])
2452        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', ':success'])
2453        self.assertFailedTestCount(1, self.mtest_command + ['--no-suite', ':fail'])
2454
2455        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'mainprj'])
2456        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjsucc'])
2457        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjfail'])
2458        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjmix'])
2459        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'mainprj'])
2460        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjsucc'])
2461        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjfail'])
2462        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjmix'])
2463
2464        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'mainprj:fail'])
2465        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'mainprj:success'])
2466        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'mainprj:fail'])
2467        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'mainprj:success'])
2468
2469        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjfail:fail'])
2470        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjfail:success'])
2471        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjfail:fail'])
2472        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjfail:success'])
2473
2474        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjsucc:fail'])
2475        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjsucc:success'])
2476        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjsucc:fail'])
2477        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjsucc:success'])
2478
2479        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjmix:fail'])
2480        self.assertFailedTestCount(0, self.mtest_command + ['--suite', 'subprjmix:success'])
2481        self.assertFailedTestCount(3, self.mtest_command + ['--no-suite', 'subprjmix:fail'])
2482        self.assertFailedTestCount(4, self.mtest_command + ['--no-suite', 'subprjmix:success'])
2483
2484        self.assertFailedTestCount(2, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix:fail'])
2485        self.assertFailedTestCount(3, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix', '--suite', 'mainprj'])
2486        self.assertFailedTestCount(2, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix', '--suite', 'mainprj', '--no-suite', 'subprjmix:fail'])
2487        self.assertFailedTestCount(1, self.mtest_command + ['--suite', 'subprjfail', '--suite', 'subprjmix', '--suite', 'mainprj', '--no-suite', 'subprjmix:fail', 'mainprj-failing_test'])
2488
2489        self.assertFailedTestCount(2, self.mtest_command + ['--no-suite', 'subprjfail:fail', '--no-suite', 'subprjmix:fail'])
2490
2491    def test_build_by_default(self):
2492        testdir = os.path.join(self.common_test_dir, '133 build by default')
2493        self.init(testdir)
2494        self.build()
2495        genfile1 = os.path.join(self.builddir, 'generated1.dat')
2496        genfile2 = os.path.join(self.builddir, 'generated2.dat')
2497        exe1 = os.path.join(self.builddir, 'fooprog' + exe_suffix)
2498        exe2 = os.path.join(self.builddir, 'barprog' + exe_suffix)
2499        self.assertPathExists(genfile1)
2500        self.assertPathExists(genfile2)
2501        self.assertPathDoesNotExist(exe1)
2502        self.assertPathDoesNotExist(exe2)
2503        self.build(target=('fooprog' + exe_suffix))
2504        self.assertPathExists(exe1)
2505        self.build(target=('barprog' + exe_suffix))
2506        self.assertPathExists(exe2)
2507
2508    def test_internal_include_order(self):
2509        if mesonbuild.environment.detect_msys2_arch() and ('MESON_RSP_THRESHOLD' in os.environ):
2510            raise unittest.SkipTest('Test does not yet support gcc rsp files on msys2')
2511
2512        testdir = os.path.join(self.common_test_dir, '134 include order')
2513        self.init(testdir)
2514        execmd = fxecmd = None
2515        for cmd in self.get_compdb():
2516            if 'someexe' in cmd['command']:
2517                execmd = cmd['command']
2518                continue
2519            if 'somefxe' in cmd['command']:
2520                fxecmd = cmd['command']
2521                continue
2522        if not execmd or not fxecmd:
2523            raise Exception('Could not find someexe and somfxe commands')
2524        # Check include order for 'someexe'
2525        incs = [a for a in split_args(execmd) if a.startswith("-I")]
2526        self.assertEqual(len(incs), 9)
2527        # Need to run the build so the private dir is created.
2528        self.build()
2529        pdirs = glob(os.path.join(self.builddir, 'sub4/someexe*.p'))
2530        self.assertEqual(len(pdirs), 1)
2531        privdir = pdirs[0][len(self.builddir)+1:]
2532        self.assertPathEqual(incs[0], "-I" + privdir)
2533        # target build subdir
2534        self.assertPathEqual(incs[1], "-Isub4")
2535        # target source subdir
2536        self.assertPathBasenameEqual(incs[2], 'sub4')
2537        # include paths added via per-target c_args: ['-I'...]
2538        self.assertPathBasenameEqual(incs[3], 'sub3')
2539        # target include_directories: build dir
2540        self.assertPathEqual(incs[4], "-Isub2")
2541        # target include_directories: source dir
2542        self.assertPathBasenameEqual(incs[5], 'sub2')
2543        # target internal dependency include_directories: build dir
2544        self.assertPathEqual(incs[6], "-Isub1")
2545        # target internal dependency include_directories: source dir
2546        self.assertPathBasenameEqual(incs[7], 'sub1')
2547        # custom target include dir
2548        self.assertPathEqual(incs[8], '-Ictsub')
2549        # Check include order for 'somefxe'
2550        incs = [a for a in split_args(fxecmd) if a.startswith('-I')]
2551        self.assertEqual(len(incs), 9)
2552        # target private dir
2553        pdirs = glob(os.path.join(self.builddir, 'somefxe*.p'))
2554        self.assertEqual(len(pdirs), 1)
2555        privdir = pdirs[0][len(self.builddir)+1:]
2556        self.assertPathEqual(incs[0], '-I' + privdir)
2557        # target build dir
2558        self.assertPathEqual(incs[1], '-I.')
2559        # target source dir
2560        self.assertPathBasenameEqual(incs[2], os.path.basename(testdir))
2561        # target internal dependency correct include_directories: build dir
2562        self.assertPathEqual(incs[3], "-Isub4")
2563        # target internal dependency correct include_directories: source dir
2564        self.assertPathBasenameEqual(incs[4], 'sub4')
2565        # target internal dependency dep include_directories: build dir
2566        self.assertPathEqual(incs[5], "-Isub1")
2567        # target internal dependency dep include_directories: source dir
2568        self.assertPathBasenameEqual(incs[6], 'sub1')
2569        # target internal dependency wrong include_directories: build dir
2570        self.assertPathEqual(incs[7], "-Isub2")
2571        # target internal dependency wrong include_directories: source dir
2572        self.assertPathBasenameEqual(incs[8], 'sub2')
2573
2574    def test_compiler_detection(self):
2575        '''
2576        Test that automatic compiler detection and setting from the environment
2577        both work just fine. This is needed because while running project tests
2578        and other unit tests, we always read CC/CXX/etc from the environment.
2579        '''
2580        gnu = mesonbuild.compilers.GnuCompiler
2581        clang = mesonbuild.compilers.ClangCompiler
2582        intel = mesonbuild.compilers.IntelGnuLikeCompiler
2583        msvc = (mesonbuild.compilers.VisualStudioCCompiler, mesonbuild.compilers.VisualStudioCPPCompiler)
2584        clangcl = (mesonbuild.compilers.ClangClCCompiler, mesonbuild.compilers.ClangClCPPCompiler)
2585        ar = mesonbuild.linkers.ArLinker
2586        lib = mesonbuild.linkers.VisualStudioLinker
2587        langs = [('c', 'CC'), ('cpp', 'CXX')]
2588        if not is_windows() and platform.machine().lower() != 'e2k':
2589            langs += [('objc', 'OBJC'), ('objcpp', 'OBJCXX')]
2590        testdir = os.path.join(self.unit_test_dir, '5 compiler detection')
2591        env = get_fake_env(testdir, self.builddir, self.prefix)
2592        for lang, evar in langs:
2593            # Detect with evar and do sanity checks on that
2594            if evar in os.environ:
2595                ecc = getattr(env, 'detect_{}_compiler'.format(lang))(MachineChoice.HOST)
2596                self.assertTrue(ecc.version)
2597                elinker = env.detect_static_linker(ecc)
2598                # Pop it so we don't use it for the next detection
2599                evalue = os.environ.pop(evar)
2600                # Very rough/strict heuristics. Would never work for actual
2601                # compiler detection, but should be ok for the tests.
2602                ebase = os.path.basename(evalue)
2603                if ebase.startswith('g') or ebase.endswith(('-gcc', '-g++')):
2604                    self.assertIsInstance(ecc, gnu)
2605                    self.assertIsInstance(elinker, ar)
2606                elif 'clang-cl' in ebase:
2607                    self.assertIsInstance(ecc, clangcl)
2608                    self.assertIsInstance(elinker, lib)
2609                elif 'clang' in ebase:
2610                    self.assertIsInstance(ecc, clang)
2611                    self.assertIsInstance(elinker, ar)
2612                elif ebase.startswith('ic'):
2613                    self.assertIsInstance(ecc, intel)
2614                    self.assertIsInstance(elinker, ar)
2615                elif ebase.startswith('cl'):
2616                    self.assertIsInstance(ecc, msvc)
2617                    self.assertIsInstance(elinker, lib)
2618                else:
2619                    raise AssertionError('Unknown compiler {!r}'.format(evalue))
2620                # Check that we actually used the evalue correctly as the compiler
2621                self.assertEqual(ecc.get_exelist(), split_args(evalue))
2622            # Do auto-detection of compiler based on platform, PATH, etc.
2623            cc = getattr(env, 'detect_{}_compiler'.format(lang))(MachineChoice.HOST)
2624            self.assertTrue(cc.version)
2625            linker = env.detect_static_linker(cc)
2626            # Check compiler type
2627            if isinstance(cc, gnu):
2628                self.assertIsInstance(linker, ar)
2629                if is_osx():
2630                    self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker)
2631                elif is_sunos():
2632                    self.assertIsInstance(cc.linker, (mesonbuild.linkers.SolarisDynamicLinker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin))
2633                else:
2634                    self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin)
2635            if isinstance(cc, clangcl):
2636                self.assertIsInstance(linker, lib)
2637                self.assertIsInstance(cc.linker, mesonbuild.linkers.ClangClDynamicLinker)
2638            if isinstance(cc, clang):
2639                self.assertIsInstance(linker, ar)
2640                if is_osx():
2641                    self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker)
2642                elif is_windows():
2643                    # This is clang, not clang-cl
2644                    self.assertIsInstance(cc.linker, mesonbuild.linkers.MSVCDynamicLinker)
2645                else:
2646                    self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuLikeDynamicLinkerMixin)
2647            if isinstance(cc, intel):
2648                self.assertIsInstance(linker, ar)
2649                if is_osx():
2650                    self.assertIsInstance(cc.linker, mesonbuild.linkers.AppleDynamicLinker)
2651                elif is_windows():
2652                    self.assertIsInstance(cc.linker, mesonbuild.linkers.XilinkDynamicLinker)
2653                else:
2654                    self.assertIsInstance(cc.linker, mesonbuild.linkers.GnuDynamicLinker)
2655            if isinstance(cc, msvc):
2656                self.assertTrue(is_windows())
2657                self.assertIsInstance(linker, lib)
2658                self.assertEqual(cc.id, 'msvc')
2659                self.assertTrue(hasattr(cc, 'is_64'))
2660                self.assertIsInstance(cc.linker, mesonbuild.linkers.MSVCDynamicLinker)
2661                # If we're on Windows CI, we know what the compiler will be
2662                if 'arch' in os.environ:
2663                    if os.environ['arch'] == 'x64':
2664                        self.assertTrue(cc.is_64)
2665                    else:
2666                        self.assertFalse(cc.is_64)
2667            # Set evar ourselves to a wrapper script that just calls the same
2668            # exelist + some argument. This is meant to test that setting
2669            # something like `ccache gcc -pipe` or `distcc ccache gcc` works.
2670            wrapper = os.path.join(testdir, 'compiler wrapper.py')
2671            wrappercc = python_command + [wrapper] + cc.get_exelist() + ['-DSOME_ARG']
2672            wrappercc_s = ''
2673            for w in wrappercc:
2674                wrappercc_s += quote_arg(w) + ' '
2675            os.environ[evar] = wrappercc_s
2676            wcc = getattr(env, 'detect_{}_compiler'.format(lang))(MachineChoice.HOST)
2677            # Check static linker too
2678            wrapperlinker = python_command + [wrapper] + linker.get_exelist() + linker.get_always_args()
2679            wrapperlinker_s = ''
2680            for w in wrapperlinker:
2681                wrapperlinker_s += quote_arg(w) + ' '
2682            os.environ['AR'] = wrapperlinker_s
2683            wlinker = env.detect_static_linker(wcc)
2684            # Pop it so we don't use it for the next detection
2685            evalue = os.environ.pop('AR')
2686            # Must be the same type since it's a wrapper around the same exelist
2687            self.assertIs(type(cc), type(wcc))
2688            self.assertIs(type(linker), type(wlinker))
2689            # Ensure that the exelist is correct
2690            self.assertEqual(wcc.get_exelist(), wrappercc)
2691            self.assertEqual(wlinker.get_exelist(), wrapperlinker)
2692            # Ensure that the version detection worked correctly
2693            self.assertEqual(cc.version, wcc.version)
2694            if hasattr(cc, 'is_64'):
2695                self.assertEqual(cc.is_64, wcc.is_64)
2696
2697    def test_always_prefer_c_compiler_for_asm(self):
2698        testdir = os.path.join(self.common_test_dir, '137 c cpp and asm')
2699        # Skip if building with MSVC
2700        env = get_fake_env(testdir, self.builddir, self.prefix)
2701        if env.detect_c_compiler(MachineChoice.HOST).get_id() == 'msvc':
2702            raise unittest.SkipTest('MSVC can\'t compile assembly')
2703        self.init(testdir)
2704        commands = {'c-asm': {}, 'cpp-asm': {}, 'cpp-c-asm': {}, 'c-cpp-asm': {}}
2705        for cmd in self.get_compdb():
2706            # Get compiler
2707            split = split_args(cmd['command'])
2708            if split[0] == 'ccache':
2709                compiler = split[1]
2710            else:
2711                compiler = split[0]
2712            # Classify commands
2713            if 'Ic-asm' in cmd['command']:
2714                if cmd['file'].endswith('.S'):
2715                    commands['c-asm']['asm'] = compiler
2716                elif cmd['file'].endswith('.c'):
2717                    commands['c-asm']['c'] = compiler
2718                else:
2719                    raise AssertionError('{!r} found in cpp-asm?'.format(cmd['command']))
2720            elif 'Icpp-asm' in cmd['command']:
2721                if cmd['file'].endswith('.S'):
2722                    commands['cpp-asm']['asm'] = compiler
2723                elif cmd['file'].endswith('.cpp'):
2724                    commands['cpp-asm']['cpp'] = compiler
2725                else:
2726                    raise AssertionError('{!r} found in cpp-asm?'.format(cmd['command']))
2727            elif 'Ic-cpp-asm' in cmd['command']:
2728                if cmd['file'].endswith('.S'):
2729                    commands['c-cpp-asm']['asm'] = compiler
2730                elif cmd['file'].endswith('.c'):
2731                    commands['c-cpp-asm']['c'] = compiler
2732                elif cmd['file'].endswith('.cpp'):
2733                    commands['c-cpp-asm']['cpp'] = compiler
2734                else:
2735                    raise AssertionError('{!r} found in c-cpp-asm?'.format(cmd['command']))
2736            elif 'Icpp-c-asm' in cmd['command']:
2737                if cmd['file'].endswith('.S'):
2738                    commands['cpp-c-asm']['asm'] = compiler
2739                elif cmd['file'].endswith('.c'):
2740                    commands['cpp-c-asm']['c'] = compiler
2741                elif cmd['file'].endswith('.cpp'):
2742                    commands['cpp-c-asm']['cpp'] = compiler
2743                else:
2744                    raise AssertionError('{!r} found in cpp-c-asm?'.format(cmd['command']))
2745            else:
2746                raise AssertionError('Unknown command {!r} found'.format(cmd['command']))
2747        # Check that .S files are always built with the C compiler
2748        self.assertEqual(commands['c-asm']['asm'], commands['c-asm']['c'])
2749        self.assertEqual(commands['c-asm']['asm'], commands['cpp-asm']['asm'])
2750        self.assertEqual(commands['cpp-asm']['asm'], commands['c-cpp-asm']['c'])
2751        self.assertEqual(commands['c-cpp-asm']['asm'], commands['c-cpp-asm']['c'])
2752        self.assertEqual(commands['cpp-c-asm']['asm'], commands['cpp-c-asm']['c'])
2753        self.assertNotEqual(commands['cpp-asm']['asm'], commands['cpp-asm']['cpp'])
2754        self.assertNotEqual(commands['c-cpp-asm']['c'], commands['c-cpp-asm']['cpp'])
2755        self.assertNotEqual(commands['cpp-c-asm']['c'], commands['cpp-c-asm']['cpp'])
2756        # Check that the c-asm target is always linked with the C linker
2757        build_ninja = os.path.join(self.builddir, 'build.ninja')
2758        with open(build_ninja, 'r', encoding='utf-8') as f:
2759            contents = f.read()
2760            m = re.search('build c-asm.*: c_LINKER', contents)
2761        self.assertIsNotNone(m, msg=contents)
2762
2763    def test_preprocessor_checks_CPPFLAGS(self):
2764        '''
2765        Test that preprocessor compiler checks read CPPFLAGS and also CFLAGS but
2766        not LDFLAGS.
2767        '''
2768        testdir = os.path.join(self.common_test_dir, '136 get define')
2769        define = 'MESON_TEST_DEFINE_VALUE'
2770        # NOTE: this list can't have \n, ' or "
2771        # \n is never substituted by the GNU pre-processor via a -D define
2772        # ' and " confuse split_args() even when they are escaped
2773        # % and # confuse the MSVC preprocessor
2774        # !, ^, *, and < confuse lcc preprocessor
2775        value = 'spaces and fun@$&()-=_+{}[]:;>?,./~`'
2776        for env_var in ['CPPFLAGS', 'CFLAGS']:
2777            env = {}
2778            env[env_var] = '-D{}="{}"'.format(define, value)
2779            env['LDFLAGS'] = '-DMESON_FAIL_VALUE=cflags-read'.format(define)
2780            self.init(testdir, extra_args=['-D{}={}'.format(define, value)], override_envvars=env)
2781
2782    def test_custom_target_exe_data_deterministic(self):
2783        testdir = os.path.join(self.common_test_dir, '113 custom target capture')
2784        self.init(testdir)
2785        meson_exe_dat1 = glob(os.path.join(self.privatedir, 'meson_exe*.dat'))
2786        self.wipe()
2787        self.init(testdir)
2788        meson_exe_dat2 = glob(os.path.join(self.privatedir, 'meson_exe*.dat'))
2789        self.assertListEqual(meson_exe_dat1, meson_exe_dat2)
2790
2791    def test_noop_changes_cause_no_rebuilds(self):
2792        '''
2793        Test that no-op changes to the build files such as mtime do not cause
2794        a rebuild of anything.
2795        '''
2796        testdir = os.path.join(self.common_test_dir, '6 linkshared')
2797        self.init(testdir)
2798        self.build()
2799        # Immediately rebuilding should not do anything
2800        self.assertBuildIsNoop()
2801        # Changing mtime of meson.build should not rebuild anything
2802        self.utime(os.path.join(testdir, 'meson.build'))
2803        self.assertReconfiguredBuildIsNoop()
2804        # Changing mtime of libefile.c should rebuild the library, but not relink the executable
2805        self.utime(os.path.join(testdir, 'libfile.c'))
2806        self.assertBuildRelinkedOnlyTarget('mylib')
2807
2808    def test_source_changes_cause_rebuild(self):
2809        '''
2810        Test that changes to sources and headers cause rebuilds, but not
2811        changes to unused files (as determined by the dependency file) in the
2812        input files list.
2813        '''
2814        testdir = os.path.join(self.common_test_dir, '20 header in file list')
2815        self.init(testdir)
2816        self.build()
2817        # Immediately rebuilding should not do anything
2818        self.assertBuildIsNoop()
2819        # Changing mtime of header.h should rebuild everything
2820        self.utime(os.path.join(testdir, 'header.h'))
2821        self.assertBuildRelinkedOnlyTarget('prog')
2822
2823    def test_custom_target_changes_cause_rebuild(self):
2824        '''
2825        Test that in a custom target, changes to the input files, the
2826        ExternalProgram, and any File objects on the command-line cause
2827        a rebuild.
2828        '''
2829        testdir = os.path.join(self.common_test_dir, '60 custom header generator')
2830        self.init(testdir)
2831        self.build()
2832        # Immediately rebuilding should not do anything
2833        self.assertBuildIsNoop()
2834        # Changing mtime of these should rebuild everything
2835        for f in ('input.def', 'makeheader.py', 'somefile.txt'):
2836            self.utime(os.path.join(testdir, f))
2837            self.assertBuildRelinkedOnlyTarget('prog')
2838
2839    def test_source_generator_program_cause_rebuild(self):
2840        '''
2841        Test that changes to generator programs in the source tree cause
2842        a rebuild.
2843        '''
2844        testdir = os.path.join(self.common_test_dir, '94 gen extra')
2845        self.init(testdir)
2846        self.build()
2847        # Immediately rebuilding should not do anything
2848        self.assertBuildIsNoop()
2849        # Changing mtime of generator should rebuild the executable
2850        self.utime(os.path.join(testdir, 'srcgen.py'))
2851        self.assertRebuiltTarget('basic')
2852
2853    def test_static_library_lto(self):
2854        '''
2855        Test that static libraries can be built with LTO and linked to
2856        executables. On Linux, this requires the use of gcc-ar.
2857        https://github.com/mesonbuild/meson/issues/1646
2858        '''
2859        testdir = os.path.join(self.common_test_dir, '5 linkstatic')
2860
2861        env = get_fake_env(testdir, self.builddir, self.prefix)
2862        if env.detect_c_compiler(MachineChoice.HOST).get_id() == 'clang' and is_windows():
2863            raise unittest.SkipTest('LTO not (yet) supported by windows clang')
2864
2865        self.init(testdir, extra_args='-Db_lto=true')
2866        self.build()
2867        self.run_tests()
2868
2869    def test_dist_git(self):
2870        if not shutil.which('git'):
2871            raise unittest.SkipTest('Git not found')
2872        if self.backend is not Backend.ninja:
2873            raise unittest.SkipTest('Dist is only supported with Ninja')
2874
2875        try:
2876            self.dist_impl(_git_init)
2877        except PermissionError:
2878            # When run under Windows CI, something (virus scanner?)
2879            # holds on to the git files so cleaning up the dir
2880            # fails sometimes.
2881            pass
2882
2883    def has_working_hg(self):
2884        if not shutil.which('hg'):
2885            return False
2886        try:
2887            # This check should not be necessary, but
2888            # CI under macOS passes the above test even
2889            # though Mercurial is not installed.
2890            if subprocess.call(['hg', '--version'],
2891                               stdout=subprocess.DEVNULL,
2892                               stderr=subprocess.DEVNULL) != 0:
2893                return False
2894            return True
2895        except FileNotFoundError:
2896            return False
2897
2898
2899    def test_dist_hg(self):
2900        if not self.has_working_hg():
2901            raise unittest.SkipTest('Mercurial not found or broken.')
2902        if self.backend is not Backend.ninja:
2903            raise unittest.SkipTest('Dist is only supported with Ninja')
2904
2905        def hg_init(project_dir):
2906            subprocess.check_call(['hg', 'init'], cwd=project_dir)
2907            with open(os.path.join(project_dir, '.hg', 'hgrc'), 'w') as f:
2908                print('[ui]', file=f)
2909                print('username=Author Person <teh_coderz@example.com>', file=f)
2910            subprocess.check_call(['hg', 'add', 'meson.build', 'distexe.c'], cwd=project_dir)
2911            subprocess.check_call(['hg', 'commit', '-m', 'I am a project'], cwd=project_dir)
2912
2913        try:
2914            self.dist_impl(hg_init, include_subprojects=False)
2915        except PermissionError:
2916            # When run under Windows CI, something (virus scanner?)
2917            # holds on to the hg files so cleaning up the dir
2918            # fails sometimes.
2919            pass
2920
2921    def test_dist_git_script(self):
2922        if not shutil.which('git'):
2923            raise unittest.SkipTest('Git not found')
2924        if self.backend is not Backend.ninja:
2925            raise unittest.SkipTest('Dist is only supported with Ninja')
2926
2927        try:
2928            with tempfile.TemporaryDirectory() as tmpdir:
2929                project_dir = os.path.join(tmpdir, 'a')
2930                shutil.copytree(os.path.join(self.unit_test_dir, '35 dist script'),
2931                                project_dir)
2932                _git_init(project_dir)
2933                self.init(project_dir)
2934                self.build('dist')
2935        except PermissionError:
2936            # When run under Windows CI, something (virus scanner?)
2937            # holds on to the git files so cleaning up the dir
2938            # fails sometimes.
2939            pass
2940
2941    def create_dummy_subproject(self, project_dir, name):
2942        path = os.path.join(project_dir, 'subprojects', name)
2943        os.makedirs(path)
2944        with open(os.path.join(path, 'meson.build'), 'w') as ofile:
2945            ofile.write("project('{}')".format(name))
2946        return path
2947
2948    def dist_impl(self, vcs_init, include_subprojects=True):
2949        # Create this on the fly because having rogue .git directories inside
2950        # the source tree leads to all kinds of trouble.
2951        with tempfile.TemporaryDirectory() as project_dir:
2952            with open(os.path.join(project_dir, 'meson.build'), 'w') as ofile:
2953                ofile.write('''project('disttest', 'c', version : '1.4.3')
2954e = executable('distexe', 'distexe.c')
2955test('dist test', e)
2956subproject('vcssub', required : false)
2957subproject('tarballsub', required : false)
2958''')
2959            with open(os.path.join(project_dir, 'distexe.c'), 'w') as ofile:
2960                ofile.write('''#include<stdio.h>
2961
2962int main(int argc, char **argv) {
2963    printf("I am a distribution test.\\n");
2964    return 0;
2965}
2966''')
2967            xz_distfile = os.path.join(self.distdir, 'disttest-1.4.3.tar.xz')
2968            xz_checksumfile = xz_distfile + '.sha256sum'
2969            zip_distfile = os.path.join(self.distdir, 'disttest-1.4.3.zip')
2970            zip_checksumfile = zip_distfile + '.sha256sum'
2971            vcs_init(project_dir)
2972            if include_subprojects:
2973                vcs_init(self.create_dummy_subproject(project_dir, 'vcssub'))
2974                self.create_dummy_subproject(project_dir, 'tarballsub')
2975                self.create_dummy_subproject(project_dir, 'unusedsub')
2976            self.init(project_dir)
2977            self.build('dist')
2978            self.assertPathExists(xz_distfile)
2979            self.assertPathExists(xz_checksumfile)
2980            self.assertPathDoesNotExist(zip_distfile)
2981            self.assertPathDoesNotExist(zip_checksumfile)
2982            self._run(self.meson_command + ['dist', '--formats', 'zip'],
2983                      workdir=self.builddir)
2984            self.assertPathExists(zip_distfile)
2985            self.assertPathExists(zip_checksumfile)
2986
2987            if include_subprojects:
2988                z = zipfile.ZipFile(zip_distfile)
2989                self.assertEqual(sorted(['disttest-1.4.3/',
2990                                         'disttest-1.4.3/meson.build',
2991                                         'disttest-1.4.3/distexe.c']),
2992                                 sorted(z.namelist()))
2993
2994                self._run(self.meson_command + ['dist', '--formats', 'zip', '--include-subprojects'],
2995                          workdir=self.builddir)
2996                z = zipfile.ZipFile(zip_distfile)
2997                self.assertEqual(sorted(['disttest-1.4.3/',
2998                                         'disttest-1.4.3/subprojects/',
2999                                         'disttest-1.4.3/meson.build',
3000                                         'disttest-1.4.3/distexe.c',
3001                                         'disttest-1.4.3/subprojects/tarballsub/',
3002                                         'disttest-1.4.3/subprojects/vcssub/',
3003                                         'disttest-1.4.3/subprojects/tarballsub/meson.build',
3004                                         'disttest-1.4.3/subprojects/vcssub/meson.build']),
3005                                 sorted(z.namelist()))
3006
3007    def test_rpath_uses_ORIGIN(self):
3008        '''
3009        Test that built targets use $ORIGIN in rpath, which ensures that they
3010        are relocatable and ensures that builds are reproducible since the
3011        build directory won't get embedded into the built binaries.
3012        '''
3013        if is_windows() or is_cygwin():
3014            raise unittest.SkipTest('Windows PE/COFF binaries do not use RPATH')
3015        testdir = os.path.join(self.common_test_dir, '42 library chain')
3016        self.init(testdir)
3017        self.build()
3018        for each in ('prog', 'subdir/liblib1.so', ):
3019            rpath = get_rpath(os.path.join(self.builddir, each))
3020            self.assertTrue(rpath, 'Rpath could not be determined for {}.'.format(each))
3021            if is_dragonflybsd():
3022                # DragonflyBSD will prepend /usr/lib/gccVERSION to the rpath,
3023                # so ignore that.
3024                self.assertTrue(rpath.startswith('/usr/lib/gcc'))
3025                rpaths = rpath.split(':')[1:]
3026            else:
3027                rpaths = rpath.split(':')
3028            for path in rpaths:
3029                self.assertTrue(path.startswith('$ORIGIN'), msg=(each, path))
3030        # These two don't link to anything else, so they do not need an rpath entry.
3031        for each in ('subdir/subdir2/liblib2.so', 'subdir/subdir3/liblib3.so'):
3032            rpath = get_rpath(os.path.join(self.builddir, each))
3033            if is_dragonflybsd():
3034                # The rpath should be equal to /usr/lib/gccVERSION
3035                self.assertTrue(rpath.startswith('/usr/lib/gcc'))
3036                self.assertEqual(len(rpath.split(':')), 1)
3037            else:
3038                self.assertTrue(rpath is None)
3039
3040    def test_dash_d_dedup(self):
3041        testdir = os.path.join(self.unit_test_dir, '9 d dedup')
3042        self.init(testdir)
3043        cmd = self.get_compdb()[0]['command']
3044        self.assertTrue('-D FOO -D BAR' in cmd or
3045                        '"-D" "FOO" "-D" "BAR"' in cmd or
3046                        '/D FOO /D BAR' in cmd or
3047                        '"/D" "FOO" "/D" "BAR"' in cmd)
3048
3049    def test_all_forbidden_targets_tested(self):
3050        '''
3051        Test that all forbidden targets are tested in the '154 reserved targets'
3052        test. Needs to be a unit test because it accesses Meson internals.
3053        '''
3054        testdir = os.path.join(self.common_test_dir, '154 reserved targets')
3055        targets = mesonbuild.coredata.forbidden_target_names
3056        # We don't actually define a target with this name
3057        targets.pop('build.ninja')
3058        # Remove this to avoid multiple entries with the same name
3059        # but different case.
3060        targets.pop('PHONY')
3061        for i in targets:
3062            self.assertPathExists(os.path.join(testdir, i))
3063
3064    def detect_prebuild_env(self):
3065        env = get_fake_env()
3066        cc = env.detect_c_compiler(MachineChoice.HOST)
3067        stlinker = env.detect_static_linker(cc)
3068        if mesonbuild.mesonlib.is_windows():
3069            object_suffix = 'obj'
3070            shared_suffix = 'dll'
3071        elif mesonbuild.mesonlib.is_cygwin():
3072            object_suffix = 'o'
3073            shared_suffix = 'dll'
3074        elif mesonbuild.mesonlib.is_osx():
3075            object_suffix = 'o'
3076            shared_suffix = 'dylib'
3077        else:
3078            object_suffix = 'o'
3079            shared_suffix = 'so'
3080        return (cc, stlinker, object_suffix, shared_suffix)
3081
3082    def pbcompile(self, compiler, source, objectfile, extra_args=None):
3083        cmd = compiler.get_exelist()
3084        extra_args = extra_args or []
3085        if compiler.get_argument_syntax() == 'msvc':
3086            cmd += ['/nologo', '/Fo' + objectfile, '/c', source] + extra_args
3087        else:
3088            cmd += ['-c', source, '-o', objectfile] + extra_args
3089        subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
3090
3091    def test_prebuilt_object(self):
3092        (compiler, _, object_suffix, _) = self.detect_prebuild_env()
3093        tdir = os.path.join(self.unit_test_dir, '15 prebuilt object')
3094        source = os.path.join(tdir, 'source.c')
3095        objectfile = os.path.join(tdir, 'prebuilt.' + object_suffix)
3096        self.pbcompile(compiler, source, objectfile)
3097        try:
3098            self.init(tdir)
3099            self.build()
3100            self.run_tests()
3101        finally:
3102            os.unlink(objectfile)
3103
3104    def build_static_lib(self, compiler, linker, source, objectfile, outfile, extra_args=None):
3105        if extra_args is None:
3106            extra_args = []
3107        if compiler.get_argument_syntax() == 'msvc':
3108            link_cmd = ['lib', '/NOLOGO', '/OUT:' + outfile, objectfile]
3109        else:
3110            link_cmd = ['ar', 'csr', outfile, objectfile]
3111        link_cmd = linker.get_exelist()
3112        link_cmd += linker.get_always_args()
3113        link_cmd += linker.get_std_link_args()
3114        link_cmd += linker.get_output_args(outfile)
3115        link_cmd += [objectfile]
3116        self.pbcompile(compiler, source, objectfile, extra_args=extra_args)
3117        try:
3118            subprocess.check_call(link_cmd)
3119        finally:
3120            os.unlink(objectfile)
3121
3122    def test_prebuilt_static_lib(self):
3123        (cc, stlinker, object_suffix, _) = self.detect_prebuild_env()
3124        tdir = os.path.join(self.unit_test_dir, '16 prebuilt static')
3125        source = os.path.join(tdir, 'libdir/best.c')
3126        objectfile = os.path.join(tdir, 'libdir/best.' + object_suffix)
3127        stlibfile = os.path.join(tdir, 'libdir/libbest.a')
3128        self.build_static_lib(cc, stlinker, source, objectfile, stlibfile)
3129        # Run the test
3130        try:
3131            self.init(tdir)
3132            self.build()
3133            self.run_tests()
3134        finally:
3135            os.unlink(stlibfile)
3136
3137    def build_shared_lib(self, compiler, source, objectfile, outfile, impfile, extra_args=None):
3138        if extra_args is None:
3139            extra_args = []
3140        if compiler.get_argument_syntax() == 'msvc':
3141            link_cmd = compiler.get_linker_exelist() + [
3142                '/NOLOGO', '/DLL', '/DEBUG', '/IMPLIB:' + impfile,
3143                '/OUT:' + outfile, objectfile]
3144        else:
3145            if not (compiler.info.is_windows() or compiler.info.is_cygwin() or compiler.info.is_darwin()):
3146                extra_args += ['-fPIC']
3147            link_cmd = compiler.get_exelist() + ['-shared', '-o', outfile, objectfile]
3148            if not mesonbuild.mesonlib.is_osx():
3149                link_cmd += ['-Wl,-soname=' + os.path.basename(outfile)]
3150        self.pbcompile(compiler, source, objectfile, extra_args=extra_args)
3151        try:
3152            subprocess.check_call(link_cmd)
3153        finally:
3154            os.unlink(objectfile)
3155
3156    def test_prebuilt_shared_lib(self):
3157        (cc, _, object_suffix, shared_suffix) = self.detect_prebuild_env()
3158        tdir = os.path.join(self.unit_test_dir, '17 prebuilt shared')
3159        source = os.path.join(tdir, 'alexandria.c')
3160        objectfile = os.path.join(tdir, 'alexandria.' + object_suffix)
3161        impfile = os.path.join(tdir, 'alexandria.lib')
3162        if cc.get_argument_syntax() == 'msvc':
3163            shlibfile = os.path.join(tdir, 'alexandria.' + shared_suffix)
3164        elif is_cygwin():
3165            shlibfile = os.path.join(tdir, 'cygalexandria.' + shared_suffix)
3166        else:
3167            shlibfile = os.path.join(tdir, 'libalexandria.' + shared_suffix)
3168        self.build_shared_lib(cc, source, objectfile, shlibfile, impfile)
3169        # Run the test
3170        try:
3171            self.init(tdir)
3172            self.build()
3173            self.run_tests()
3174        finally:
3175            os.unlink(shlibfile)
3176            if mesonbuild.mesonlib.is_windows():
3177                # Clean up all the garbage MSVC writes in the
3178                # source tree.
3179                for fname in glob(os.path.join(tdir, 'alexandria.*')):
3180                    if os.path.splitext(fname)[1] not in ['.c', '.h']:
3181                        os.unlink(fname)
3182
3183    @skipIfNoPkgconfig
3184    def test_pkgconfig_static(self):
3185        '''
3186        Test that the we prefer static libraries when `static: true` is
3187        passed to dependency() with pkg-config. Can't be an ordinary test
3188        because we need to build libs and try to find them from meson.build
3189
3190        Also test that it's not a hard error to have unsatisfiable library deps
3191        since system libraries -lm will never be found statically.
3192        https://github.com/mesonbuild/meson/issues/2785
3193        '''
3194        (cc, stlinker, objext, shext) = self.detect_prebuild_env()
3195        testdir = os.path.join(self.unit_test_dir, '18 pkgconfig static')
3196        source = os.path.join(testdir, 'foo.c')
3197        objectfile = os.path.join(testdir, 'foo.' + objext)
3198        stlibfile = os.path.join(testdir, 'libfoo.a')
3199        impfile = os.path.join(testdir, 'foo.lib')
3200        if cc.get_argument_syntax() == 'msvc':
3201            shlibfile = os.path.join(testdir, 'foo.' + shext)
3202        elif is_cygwin():
3203            shlibfile = os.path.join(testdir, 'cygfoo.' + shext)
3204        else:
3205            shlibfile = os.path.join(testdir, 'libfoo.' + shext)
3206        # Build libs
3207        self.build_static_lib(cc, stlinker, source, objectfile, stlibfile, extra_args=['-DFOO_STATIC'])
3208        self.build_shared_lib(cc, source, objectfile, shlibfile, impfile)
3209        # Run test
3210        try:
3211            self.init(testdir, override_envvars={'PKG_CONFIG_LIBDIR': self.builddir})
3212            self.build()
3213            self.run_tests()
3214        finally:
3215            os.unlink(stlibfile)
3216            os.unlink(shlibfile)
3217            if mesonbuild.mesonlib.is_windows():
3218                # Clean up all the garbage MSVC writes in the
3219                # source tree.
3220                for fname in glob(os.path.join(testdir, 'foo.*')):
3221                    if os.path.splitext(fname)[1] not in ['.c', '.h', '.in']:
3222                        os.unlink(fname)
3223
3224    @skipIfNoPkgconfig
3225    def test_pkgconfig_gen_escaping(self):
3226        testdir = os.path.join(self.common_test_dir, '47 pkgconfig-gen')
3227        prefix = '/usr/with spaces'
3228        libdir = 'lib'
3229        self.init(testdir, extra_args=['--prefix=' + prefix,
3230                                       '--libdir=' + libdir])
3231        # Find foo dependency
3232        os.environ['PKG_CONFIG_LIBDIR'] = self.privatedir
3233        env = get_fake_env(testdir, self.builddir, self.prefix)
3234        kwargs = {'required': True, 'silent': True}
3235        foo_dep = PkgConfigDependency('libfoo', env, kwargs)
3236        # Ensure link_args are properly quoted
3237        libdir = PurePath(prefix) / PurePath(libdir)
3238        link_args = ['-L' + libdir.as_posix(), '-lfoo']
3239        self.assertEqual(foo_dep.get_link_args(), link_args)
3240        # Ensure include args are properly quoted
3241        incdir = PurePath(prefix) / PurePath('include')
3242        cargs = ['-I' + incdir.as_posix(), '-DLIBFOO']
3243        # pkg-config and pkgconf does not respect the same order
3244        self.assertEqual(sorted(foo_dep.get_compile_args()), sorted(cargs))
3245
3246    def test_array_option_change(self):
3247        def get_opt():
3248            opts = self.introspect('--buildoptions')
3249            for x in opts:
3250                if x.get('name') == 'list':
3251                    return x
3252            raise Exception(opts)
3253
3254        expected = {
3255            'name': 'list',
3256            'description': 'list',
3257            'section': 'user',
3258            'type': 'array',
3259            'value': ['foo', 'bar'],
3260            'machine': 'any',
3261        }
3262        tdir = os.path.join(self.unit_test_dir, '19 array option')
3263        self.init(tdir)
3264        original = get_opt()
3265        self.assertDictEqual(original, expected)
3266
3267        expected['value'] = ['oink', 'boink']
3268        self.setconf('-Dlist=oink,boink')
3269        changed = get_opt()
3270        self.assertEqual(changed, expected)
3271
3272    def test_array_option_bad_change(self):
3273        def get_opt():
3274            opts = self.introspect('--buildoptions')
3275            for x in opts:
3276                if x.get('name') == 'list':
3277                    return x
3278            raise Exception(opts)
3279
3280        expected = {
3281            'name': 'list',
3282            'description': 'list',
3283            'section': 'user',
3284            'type': 'array',
3285            'value': ['foo', 'bar'],
3286            'machine': 'any',
3287        }
3288        tdir = os.path.join(self.unit_test_dir, '19 array option')
3289        self.init(tdir)
3290        original = get_opt()
3291        self.assertDictEqual(original, expected)
3292        with self.assertRaises(subprocess.CalledProcessError):
3293            self.setconf('-Dlist=bad')
3294        changed = get_opt()
3295        self.assertDictEqual(changed, expected)
3296
3297    def test_array_option_empty_equivalents(self):
3298        """Array options treat -Dopt=[] and -Dopt= as equivalent."""
3299        def get_opt():
3300            opts = self.introspect('--buildoptions')
3301            for x in opts:
3302                if x.get('name') == 'list':
3303                    return x
3304            raise Exception(opts)
3305
3306        expected = {
3307            'name': 'list',
3308            'description': 'list',
3309            'section': 'user',
3310            'type': 'array',
3311            'value': [],
3312            'machine': 'any',
3313        }
3314        tdir = os.path.join(self.unit_test_dir, '19 array option')
3315        self.init(tdir, extra_args='-Dlist=')
3316        original = get_opt()
3317        self.assertDictEqual(original, expected)
3318
3319    def opt_has(self, name, value):
3320        res = self.introspect('--buildoptions')
3321        found = False
3322        for i in res:
3323            if i['name'] == name:
3324                self.assertEqual(i['value'], value)
3325                found = True
3326                break
3327        self.assertTrue(found, "Array option not found in introspect data.")
3328
3329    def test_free_stringarray_setting(self):
3330        testdir = os.path.join(self.common_test_dir, '43 options')
3331        self.init(testdir)
3332        self.opt_has('free_array_opt', [])
3333        self.setconf('-Dfree_array_opt=foo,bar', will_build=False)
3334        self.opt_has('free_array_opt', ['foo', 'bar'])
3335        self.setconf("-Dfree_array_opt=['a,b', 'c,d']", will_build=False)
3336        self.opt_has('free_array_opt', ['a,b', 'c,d'])
3337
3338    def test_subproject_promotion(self):
3339        testdir = os.path.join(self.unit_test_dir, '12 promote')
3340        workdir = os.path.join(self.builddir, 'work')
3341        shutil.copytree(testdir, workdir)
3342        spdir = os.path.join(workdir, 'subprojects')
3343        s3dir = os.path.join(spdir, 's3')
3344        scommondir = os.path.join(spdir, 'scommon')
3345        self.assertFalse(os.path.isdir(s3dir))
3346        subprocess.check_call(self.wrap_command + ['promote', 's3'], cwd=workdir)
3347        self.assertTrue(os.path.isdir(s3dir))
3348        self.assertFalse(os.path.isdir(scommondir))
3349        self.assertNotEqual(subprocess.call(self.wrap_command + ['promote', 'scommon'],
3350                                            cwd=workdir,
3351                                            stdout=subprocess.DEVNULL), 0)
3352        self.assertNotEqual(subprocess.call(self.wrap_command + ['promote', 'invalid/path/to/scommon'],
3353                                            cwd=workdir,
3354                                            stderr=subprocess.DEVNULL), 0)
3355        self.assertFalse(os.path.isdir(scommondir))
3356        subprocess.check_call(self.wrap_command + ['promote', 'subprojects/s2/subprojects/scommon'], cwd=workdir)
3357        self.assertTrue(os.path.isdir(scommondir))
3358        promoted_wrap = os.path.join(spdir, 'athing.wrap')
3359        self.assertFalse(os.path.isfile(promoted_wrap))
3360        subprocess.check_call(self.wrap_command + ['promote', 'athing'], cwd=workdir)
3361        self.assertTrue(os.path.isfile(promoted_wrap))
3362        self.init(workdir)
3363        self.build()
3364
3365    def test_subproject_promotion_wrap(self):
3366        testdir = os.path.join(self.unit_test_dir, '44 promote wrap')
3367        workdir = os.path.join(self.builddir, 'work')
3368        shutil.copytree(testdir, workdir)
3369        spdir = os.path.join(workdir, 'subprojects')
3370
3371        ambiguous_wrap = os.path.join(spdir, 'ambiguous.wrap')
3372        self.assertNotEqual(subprocess.call(self.wrap_command + ['promote', 'ambiguous'],
3373                                            cwd=workdir,
3374                                            stdout=subprocess.DEVNULL), 0)
3375        self.assertFalse(os.path.isfile(ambiguous_wrap))
3376        subprocess.check_call(self.wrap_command + ['promote', 'subprojects/s2/subprojects/ambiguous.wrap'], cwd=workdir)
3377        self.assertTrue(os.path.isfile(ambiguous_wrap))
3378
3379    def test_warning_location(self):
3380        tdir = os.path.join(self.unit_test_dir, '22 warning location')
3381        out = self.init(tdir)
3382        for expected in [
3383            r'meson.build:4: WARNING: Keyword argument "link_with" defined multiple times.',
3384            r'sub' + os.path.sep + r'meson.build:3: WARNING: Keyword argument "link_with" defined multiple times.',
3385            r'meson.build:6: WARNING: a warning of some sort',
3386            r'sub' + os.path.sep + r'meson.build:4: WARNING: subdir warning',
3387            r'meson.build:7: WARNING: Module unstable-simd has no backwards or forwards compatibility and might not exist in future releases.',
3388            r"meson.build:11: WARNING: The variable(s) 'MISSING' in the input file 'conf.in' are not present in the given configuration data.",
3389            r'meson.build:1: WARNING: Passed invalid keyword argument "invalid".',
3390        ]:
3391            self.assertRegex(out, re.escape(expected))
3392
3393        for wd in [
3394            self.src_root,
3395            self.builddir,
3396            os.getcwd(),
3397        ]:
3398            self.new_builddir()
3399            out = self.init(tdir, workdir=wd)
3400            expected = os.path.join(relpath(tdir, self.src_root), 'meson.build')
3401            relwd = relpath(self.src_root, wd)
3402            if relwd != '.':
3403                expected = os.path.join(relwd, expected)
3404                expected = '\n' + expected + ':'
3405            self.assertIn(expected, out)
3406
3407    def test_error_location_path(self):
3408        '''Test locations in meson errors contain correct paths'''
3409        # this list contains errors from all the different steps in the
3410        # lexer/parser/interpreter we have tests for.
3411        for (t, f) in [
3412            ('10 out of bounds', 'meson.build'),
3413            ('18 wrong plusassign', 'meson.build'),
3414            ('61 bad option argument', 'meson_options.txt'),
3415            ('102 subdir parse error', os.path.join('subdir', 'meson.build')),
3416            ('103 invalid option file', 'meson_options.txt'),
3417        ]:
3418            tdir = os.path.join(self.src_root, 'test cases', 'failing', t)
3419
3420            for wd in [
3421                self.src_root,
3422                self.builddir,
3423                os.getcwd(),
3424            ]:
3425                try:
3426                    self.init(tdir, workdir=wd)
3427                except subprocess.CalledProcessError as e:
3428                    expected = os.path.join('test cases', 'failing', t, f)
3429                    relwd = relpath(self.src_root, wd)
3430                    if relwd != '.':
3431                        expected = os.path.join(relwd, expected)
3432                    expected = '\n' + expected + ':'
3433                    self.assertIn(expected, e.output)
3434                else:
3435                    self.fail('configure unexpectedly succeeded')
3436
3437    def test_permitted_method_kwargs(self):
3438        tdir = os.path.join(self.unit_test_dir, '25 non-permitted kwargs')
3439        out = self.init(tdir)
3440        for expected in [
3441            r'WARNING: Passed invalid keyword argument "prefixxx".',
3442            r'WARNING: Passed invalid keyword argument "argsxx".',
3443            r'WARNING: Passed invalid keyword argument "invalidxx".',
3444        ]:
3445            self.assertRegex(out, re.escape(expected))
3446
3447    def test_templates(self):
3448        ninja = detect_ninja()
3449        if ninja is None:
3450            raise unittest.SkipTest('This test currently requires ninja. Fix this once "meson build" works.')
3451        langs = ['c']
3452        env = get_fake_env()
3453        try:
3454            env.detect_cpp_compiler(MachineChoice.HOST)
3455            langs.append('cpp')
3456        except EnvironmentException:
3457            pass
3458        try:
3459            env.detect_cs_compiler(MachineChoice.HOST)
3460            langs.append('cs')
3461        except EnvironmentException:
3462            pass
3463        try:
3464            env.detect_d_compiler(MachineChoice.HOST)
3465            langs.append('d')
3466        except EnvironmentException:
3467            pass
3468        try:
3469            env.detect_java_compiler(MachineChoice.HOST)
3470            langs.append('java')
3471        except EnvironmentException:
3472            pass
3473        try:
3474            env.detect_cuda_compiler(MachineChoice.HOST)
3475            langs.append('cuda')
3476        except EnvironmentException:
3477            pass
3478        try:
3479            env.detect_fortran_compiler(MachineChoice.HOST)
3480            langs.append('fortran')
3481        except EnvironmentException:
3482            pass
3483        try:
3484            env.detect_objc_compiler(MachineChoice.HOST)
3485            langs.append('objc')
3486        except EnvironmentException:
3487            pass
3488        try:
3489            env.detect_objcpp_compiler(MachineChoice.HOST)
3490            langs.append('objcpp')
3491        except EnvironmentException:
3492            pass
3493        # FIXME: omitting rust as Windows AppVeyor CI finds Rust but doesn't link correctly
3494        if not is_windows():
3495            try:
3496                env.detect_rust_compiler(MachineChoice.HOST)
3497                langs.append('rust')
3498            except EnvironmentException:
3499                pass
3500
3501        for lang in langs:
3502            for target_type in ('executable', 'library'):
3503                # test empty directory
3504                with tempfile.TemporaryDirectory() as tmpdir:
3505                    self._run(self.meson_command + ['init', '--language', lang, '--type', target_type],
3506                              workdir=tmpdir)
3507                    self._run(self.setup_command + ['--backend=ninja', 'builddir'],
3508                              workdir=tmpdir)
3509                    self._run(ninja,
3510                              workdir=os.path.join(tmpdir, 'builddir'))
3511            # test directory with existing code file
3512            if lang in ('c', 'cpp', 'd'):
3513                with tempfile.TemporaryDirectory() as tmpdir:
3514                    with open(os.path.join(tmpdir, 'foo.' + lang), 'w') as f:
3515                        f.write('int main(void) {}')
3516                    self._run(self.meson_command + ['init', '-b'], workdir=tmpdir)
3517            elif lang in ('java'):
3518                with tempfile.TemporaryDirectory() as tmpdir:
3519                    with open(os.path.join(tmpdir, 'Foo.' + lang), 'w') as f:
3520                        f.write('public class Foo { public static void main() {} }')
3521                    self._run(self.meson_command + ['init', '-b'], workdir=tmpdir)
3522
3523    def test_compiler_run_command(self):
3524        '''
3525        The test checks that the compiler object can be passed to
3526        run_command().
3527        '''
3528        testdir = os.path.join(self.unit_test_dir, '24 compiler run_command')
3529        self.init(testdir)
3530
3531    def test_identical_target_name_in_subproject_flat_layout(self):
3532        '''
3533        Test that identical targets in different subprojects do not collide
3534        if layout is flat.
3535        '''
3536        testdir = os.path.join(self.common_test_dir, '177 identical target name in subproject flat layout')
3537        self.init(testdir, extra_args=['--layout=flat'])
3538        self.build()
3539
3540    def test_identical_target_name_in_subdir_flat_layout(self):
3541        '''
3542        Test that identical targets in different subdirs do not collide
3543        if layout is flat.
3544        '''
3545        testdir = os.path.join(self.common_test_dir, '186 same target name flat layout')
3546        self.init(testdir, extra_args=['--layout=flat'])
3547        self.build()
3548
3549    def test_flock(self):
3550        exception_raised = False
3551        with tempfile.TemporaryDirectory() as tdir:
3552            os.mkdir(os.path.join(tdir, 'meson-private'))
3553            with BuildDirLock(tdir):
3554                try:
3555                    with BuildDirLock(tdir):
3556                        pass
3557                except MesonException:
3558                    exception_raised = True
3559        self.assertTrue(exception_raised, 'Double locking did not raise exception.')
3560
3561    @unittest.skipIf(is_osx(), 'Test not applicable to OSX')
3562    def test_check_module_linking(self):
3563        """
3564        Test that link_with: a shared module issues a warning
3565        https://github.com/mesonbuild/meson/issues/2865
3566        (That an error is raised on OSX is exercised by test failing/78)
3567        """
3568        tdir = os.path.join(self.unit_test_dir, '30 shared_mod linking')
3569        out = self.init(tdir)
3570        msg = ('''WARNING: target links against shared modules. This is not
3571recommended as it is not supported on some platforms''')
3572        self.assertIn(msg, out)
3573
3574    def test_ndebug_if_release_disabled(self):
3575        testdir = os.path.join(self.unit_test_dir, '28 ndebug if-release')
3576        self.init(testdir, extra_args=['--buildtype=release', '-Db_ndebug=if-release'])
3577        self.build()
3578        exe = os.path.join(self.builddir, 'main')
3579        self.assertEqual(b'NDEBUG=1', subprocess.check_output(exe).strip())
3580
3581    def test_ndebug_if_release_enabled(self):
3582        testdir = os.path.join(self.unit_test_dir, '28 ndebug if-release')
3583        self.init(testdir, extra_args=['--buildtype=debugoptimized', '-Db_ndebug=if-release'])
3584        self.build()
3585        exe = os.path.join(self.builddir, 'main')
3586        self.assertEqual(b'NDEBUG=0', subprocess.check_output(exe).strip())
3587
3588    def test_guessed_linker_dependencies(self):
3589        '''
3590        Test that meson adds dependencies for libraries based on the final
3591        linker command line.
3592        '''
3593        testdirbase = os.path.join(self.unit_test_dir, '29 guessed linker dependencies')
3594        testdirlib = os.path.join(testdirbase, 'lib')
3595
3596        extra_args = None
3597        libdir_flags = ['-L']
3598        env = get_fake_env(testdirlib, self.builddir, self.prefix)
3599        if env.detect_c_compiler(MachineChoice.HOST).get_id() in {'msvc', 'clang-cl', 'intel-cl'}:
3600            # msvc-like compiler, also test it with msvc-specific flags
3601            libdir_flags += ['/LIBPATH:', '-LIBPATH:']
3602        else:
3603            # static libraries are not linkable with -l with msvc because meson installs them
3604            # as .a files which unix_args_to_native will not know as it expects libraries to use
3605            # .lib as extension. For a DLL the import library is installed as .lib. Thus for msvc
3606            # this tests needs to use shared libraries to test the path resolving logic in the
3607            # dependency generation code path.
3608            extra_args = ['--default-library', 'static']
3609
3610        initial_builddir = self.builddir
3611        initial_installdir = self.installdir
3612
3613        for libdir_flag in libdir_flags:
3614            # build library
3615            self.new_builddir()
3616            self.init(testdirlib, extra_args=extra_args)
3617            self.build()
3618            self.install()
3619            libbuilddir = self.builddir
3620            installdir = self.installdir
3621            libdir = os.path.join(self.installdir, self.prefix.lstrip('/').lstrip('\\'), 'lib')
3622
3623            # build user of library
3624            self.new_builddir()
3625            # replace is needed because meson mangles platform paths passed via LDFLAGS
3626            self.init(os.path.join(testdirbase, 'exe'),
3627                      override_envvars={"LDFLAGS": '{}{}'.format(libdir_flag, libdir.replace('\\', '/'))})
3628            self.build()
3629            self.assertBuildIsNoop()
3630
3631            # rebuild library
3632            exebuilddir = self.builddir
3633            self.installdir = installdir
3634            self.builddir = libbuilddir
3635            # Microsoft's compiler is quite smart about touching import libs on changes,
3636            # so ensure that there is actually a change in symbols.
3637            self.setconf('-Dmore_exports=true')
3638            self.build()
3639            self.install()
3640            # no ensure_backend_detects_changes needed because self.setconf did that already
3641
3642            # assert user of library will be rebuild
3643            self.builddir = exebuilddir
3644            self.assertRebuiltTarget('app')
3645
3646            # restore dirs for the next test case
3647            self.installdir = initial_builddir
3648            self.builddir = initial_installdir
3649
3650    def test_conflicting_d_dash_option(self):
3651        testdir = os.path.join(self.unit_test_dir, '37 mixed command line args')
3652        with self.assertRaises(subprocess.CalledProcessError) as e:
3653            self.init(testdir, extra_args=['-Dbindir=foo', '--bindir=bar'])
3654            # Just to ensure that we caught the correct error
3655            self.assertIn('passed as both', e.stderr)
3656
3657    def _test_same_option_twice(self, arg, args):
3658        testdir = os.path.join(self.unit_test_dir, '37 mixed command line args')
3659        self.init(testdir, extra_args=args)
3660        opts = self.introspect('--buildoptions')
3661        for item in opts:
3662            if item['name'] == arg:
3663                self.assertEqual(item['value'], 'bar')
3664                return
3665        raise Exception('Missing {} value?'.format(arg))
3666
3667    def test_same_dash_option_twice(self):
3668        self._test_same_option_twice('bindir', ['--bindir=foo', '--bindir=bar'])
3669
3670    def test_same_d_option_twice(self):
3671        self._test_same_option_twice('bindir', ['-Dbindir=foo', '-Dbindir=bar'])
3672
3673    def test_same_project_d_option_twice(self):
3674        self._test_same_option_twice('one', ['-Done=foo', '-Done=bar'])
3675
3676    def _test_same_option_twice_configure(self, arg, args):
3677        testdir = os.path.join(self.unit_test_dir, '37 mixed command line args')
3678        self.init(testdir)
3679        self.setconf(args)
3680        opts = self.introspect('--buildoptions')
3681        for item in opts:
3682            if item['name'] == arg:
3683                self.assertEqual(item['value'], 'bar')
3684                return
3685        raise Exception('Missing {} value?'.format(arg))
3686
3687    def test_same_dash_option_twice_configure(self):
3688        self._test_same_option_twice_configure(
3689            'bindir', ['--bindir=foo', '--bindir=bar'])
3690
3691    def test_same_d_option_twice_configure(self):
3692        self._test_same_option_twice_configure(
3693            'bindir', ['-Dbindir=foo', '-Dbindir=bar'])
3694
3695    def test_same_project_d_option_twice_configure(self):
3696        self._test_same_option_twice_configure(
3697            'one', ['-Done=foo', '-Done=bar'])
3698
3699    def test_command_line(self):
3700        testdir = os.path.join(self.unit_test_dir, '34 command line')
3701
3702        # Verify default values when passing no args that affect the
3703        # configuration, and as a bonus, test that --profile-self works.
3704        self.init(testdir, extra_args=['--profile-self'])
3705        obj = mesonbuild.coredata.load(self.builddir)
3706        self.assertEqual(obj.builtins['default_library'].value, 'static')
3707        self.assertEqual(obj.builtins['warning_level'].value, '1')
3708        self.assertEqual(obj.user_options['set_sub_opt'].value, True)
3709        self.assertEqual(obj.user_options['subp:subp_opt'].value, 'default3')
3710        self.wipe()
3711
3712        # warning_level is special, it's --warnlevel instead of --warning-level
3713        # for historical reasons
3714        self.init(testdir, extra_args=['--warnlevel=2'])
3715        obj = mesonbuild.coredata.load(self.builddir)
3716        self.assertEqual(obj.builtins['warning_level'].value, '2')
3717        self.setconf('--warnlevel=3')
3718        obj = mesonbuild.coredata.load(self.builddir)
3719        self.assertEqual(obj.builtins['warning_level'].value, '3')
3720        self.wipe()
3721
3722        # But when using -D syntax, it should be 'warning_level'
3723        self.init(testdir, extra_args=['-Dwarning_level=2'])
3724        obj = mesonbuild.coredata.load(self.builddir)
3725        self.assertEqual(obj.builtins['warning_level'].value, '2')
3726        self.setconf('-Dwarning_level=3')
3727        obj = mesonbuild.coredata.load(self.builddir)
3728        self.assertEqual(obj.builtins['warning_level'].value, '3')
3729        self.wipe()
3730
3731        # Mixing --option and -Doption is forbidden
3732        with self.assertRaises(subprocess.CalledProcessError) as cm:
3733            self.init(testdir, extra_args=['--warnlevel=1', '-Dwarning_level=3'])
3734        self.assertNotEqual(0, cm.exception.returncode)
3735        self.assertIn('as both', cm.exception.output)
3736        self.init(testdir)
3737        with self.assertRaises(subprocess.CalledProcessError) as cm:
3738            self.setconf(['--warnlevel=1', '-Dwarning_level=3'])
3739        self.assertNotEqual(0, cm.exception.returncode)
3740        self.assertIn('as both', cm.exception.output)
3741        self.wipe()
3742
3743        # --default-library should override default value from project()
3744        self.init(testdir, extra_args=['--default-library=both'])
3745        obj = mesonbuild.coredata.load(self.builddir)
3746        self.assertEqual(obj.builtins['default_library'].value, 'both')
3747        self.setconf('--default-library=shared')
3748        obj = mesonbuild.coredata.load(self.builddir)
3749        self.assertEqual(obj.builtins['default_library'].value, 'shared')
3750        if self.backend is Backend.ninja:
3751            # reconfigure target works only with ninja backend
3752            self.build('reconfigure')
3753            obj = mesonbuild.coredata.load(self.builddir)
3754            self.assertEqual(obj.builtins['default_library'].value, 'shared')
3755        self.wipe()
3756
3757        # Should warn on unknown options
3758        out = self.init(testdir, extra_args=['-Dbad=1', '-Dfoo=2', '-Dwrong_link_args=foo'])
3759        self.assertIn('Unknown options: "bad, foo, wrong_link_args"', out)
3760        self.wipe()
3761
3762        # Should fail on malformed option
3763        with self.assertRaises(subprocess.CalledProcessError) as cm:
3764            self.init(testdir, extra_args=['-Dfoo'])
3765        self.assertNotEqual(0, cm.exception.returncode)
3766        self.assertIn('Option \'foo\' must have a value separated by equals sign.', cm.exception.output)
3767        self.init(testdir)
3768        with self.assertRaises(subprocess.CalledProcessError) as cm:
3769            self.setconf('-Dfoo')
3770        self.assertNotEqual(0, cm.exception.returncode)
3771        self.assertIn('Option \'foo\' must have a value separated by equals sign.', cm.exception.output)
3772        self.wipe()
3773
3774        # It is not an error to set wrong option for unknown subprojects or
3775        # language because we don't have control on which one will be selected.
3776        self.init(testdir, extra_args=['-Dc_wrong=1', '-Dwrong:bad=1', '-Db_wrong=1'])
3777        self.wipe()
3778
3779        # Test we can set subproject option
3780        self.init(testdir, extra_args=['-Dsubp:subp_opt=foo'])
3781        obj = mesonbuild.coredata.load(self.builddir)
3782        self.assertEqual(obj.user_options['subp:subp_opt'].value, 'foo')
3783        self.wipe()
3784
3785        # c_args value should be parsed with split_args
3786        self.init(testdir, extra_args=['-Dc_args=-Dfoo -Dbar "-Dthird=one two"'])
3787        obj = mesonbuild.coredata.load(self.builddir)
3788        self.assertEqual(obj.compiler_options.host['c']['args'].value, ['-Dfoo', '-Dbar', '-Dthird=one two'])
3789
3790        self.setconf('-Dc_args="foo bar" one two')
3791        obj = mesonbuild.coredata.load(self.builddir)
3792        self.assertEqual(obj.compiler_options.host['c']['args'].value, ['foo bar', 'one', 'two'])
3793        self.wipe()
3794
3795        self.init(testdir, extra_args=['-Dset_percent_opt=myoption%'])
3796        obj = mesonbuild.coredata.load(self.builddir)
3797        self.assertEqual(obj.user_options['set_percent_opt'].value, 'myoption%')
3798        self.wipe()
3799
3800        # Setting a 2nd time the same option should override the first value
3801        try:
3802            self.init(testdir, extra_args=['--bindir=foo', '--bindir=bar',
3803                                           '-Dbuildtype=plain', '-Dbuildtype=release',
3804                                           '-Db_sanitize=address', '-Db_sanitize=thread',
3805                                           '-Dc_args=-Dfoo', '-Dc_args=-Dbar'])
3806            obj = mesonbuild.coredata.load(self.builddir)
3807            self.assertEqual(obj.builtins['bindir'].value, 'bar')
3808            self.assertEqual(obj.builtins['buildtype'].value, 'release')
3809            self.assertEqual(obj.base_options['b_sanitize'].value, 'thread')
3810            self.assertEqual(obj.compiler_options.host['c']['args'].value, ['-Dbar'])
3811            self.setconf(['--bindir=bar', '--bindir=foo',
3812                          '-Dbuildtype=release', '-Dbuildtype=plain',
3813                          '-Db_sanitize=thread', '-Db_sanitize=address',
3814                          '-Dc_args=-Dbar', '-Dc_args=-Dfoo'])
3815            obj = mesonbuild.coredata.load(self.builddir)
3816            self.assertEqual(obj.builtins['bindir'].value, 'foo')
3817            self.assertEqual(obj.builtins['buildtype'].value, 'plain')
3818            self.assertEqual(obj.base_options['b_sanitize'].value, 'address')
3819            self.assertEqual(obj.compiler_options.host['c']['args'].value, ['-Dfoo'])
3820            self.wipe()
3821        except KeyError:
3822            # Ignore KeyError, it happens on CI for compilers that does not
3823            # support b_sanitize. We have to test with a base option because
3824            # they used to fail this test with Meson 0.46 an earlier versions.
3825            pass
3826
3827    def test_warning_level_0(self):
3828        testdir = os.path.join(self.common_test_dir, '214 warning level 0')
3829
3830        # Verify default values when passing no args
3831        self.init(testdir)
3832        obj = mesonbuild.coredata.load(self.builddir)
3833        self.assertEqual(obj.builtins['warning_level'].value, '0')
3834        self.wipe()
3835
3836        # verify we can override w/ --warnlevel
3837        self.init(testdir, extra_args=['--warnlevel=1'])
3838        obj = mesonbuild.coredata.load(self.builddir)
3839        self.assertEqual(obj.builtins['warning_level'].value, '1')
3840        self.setconf('--warnlevel=0')
3841        obj = mesonbuild.coredata.load(self.builddir)
3842        self.assertEqual(obj.builtins['warning_level'].value, '0')
3843        self.wipe()
3844
3845        # verify we can override w/ -Dwarning_level
3846        self.init(testdir, extra_args=['-Dwarning_level=1'])
3847        obj = mesonbuild.coredata.load(self.builddir)
3848        self.assertEqual(obj.builtins['warning_level'].value, '1')
3849        self.setconf('-Dwarning_level=0')
3850        obj = mesonbuild.coredata.load(self.builddir)
3851        self.assertEqual(obj.builtins['warning_level'].value, '0')
3852        self.wipe()
3853
3854    def test_feature_check_usage_subprojects(self):
3855        testdir = os.path.join(self.unit_test_dir, '41 featurenew subprojects')
3856        out = self.init(testdir)
3857        # Parent project warns correctly
3858        self.assertRegex(out, "WARNING: Project targeting '>=0.45'.*'0.47.0': dict")
3859        # Subprojects warn correctly
3860        self.assertRegex(out, r"\|WARNING: Project targeting '>=0.40'.*'0.44.0': disabler")
3861        self.assertRegex(out, r"\|WARNING: Project targeting '!=0.40'.*'0.44.0': disabler")
3862        # Subproject has a new-enough meson_version, no warning
3863        self.assertNotRegex(out, "WARNING: Project targeting.*Python")
3864        # Ensure a summary is printed in the subproject and the outer project
3865        self.assertRegex(out, r"\|WARNING: Project specifies a minimum meson_version '>=0.40'")
3866        self.assertRegex(out, r"\| \* 0.44.0: {'disabler'}")
3867        self.assertRegex(out, "WARNING: Project specifies a minimum meson_version '>=0.45'")
3868        self.assertRegex(out, " * 0.47.0: {'dict'}")
3869
3870    def test_configure_file_warnings(self):
3871        testdir = os.path.join(self.common_test_dir, "14 configure file")
3872        out = self.init(testdir)
3873        self.assertRegex(out, "WARNING:.*'empty'.*config.h.in.*not present.*")
3874        self.assertRegex(out, "WARNING:.*'FOO_BAR'.*nosubst-nocopy2.txt.in.*not present.*")
3875        self.assertRegex(out, "WARNING:.*'empty'.*config.h.in.*not present.*")
3876        self.assertRegex(out, "WARNING:.*empty configuration_data.*test.py.in")
3877        # Warnings for configuration files that are overwritten.
3878        self.assertRegex(out, "WARNING:.*\"double_output.txt\".*overwrites")
3879        self.assertRegex(out, "WARNING:.*\"subdir.double_output2.txt\".*overwrites")
3880        self.assertNotRegex(out, "WARNING:.*no_write_conflict.txt.*overwrites")
3881        self.assertNotRegex(out, "WARNING:.*@BASENAME@.*overwrites")
3882        self.assertRegex(out, "WARNING:.*\"sameafterbasename\".*overwrites")
3883        # No warnings about empty configuration data objects passed to files with substitutions
3884        self.assertNotRegex(out, "WARNING:.*empty configuration_data.*nosubst-nocopy1.txt.in")
3885        self.assertNotRegex(out, "WARNING:.*empty configuration_data.*nosubst-nocopy2.txt.in")
3886        with open(os.path.join(self.builddir, 'nosubst-nocopy1.txt'), 'rb') as f:
3887            self.assertEqual(f.read().strip(), b'/* #undef FOO_BAR */')
3888        with open(os.path.join(self.builddir, 'nosubst-nocopy2.txt'), 'rb') as f:
3889            self.assertEqual(f.read().strip(), b'')
3890        self.assertRegex(out, r"DEPRECATION:.*\['array'\] is invalid.*dict")
3891
3892    def test_dirs(self):
3893        with tempfile.TemporaryDirectory() as containing:
3894            with tempfile.TemporaryDirectory(dir=containing) as srcdir:
3895                mfile = os.path.join(srcdir, 'meson.build')
3896                of = open(mfile, 'w')
3897                of.write("project('foobar', 'c')\n")
3898                of.close()
3899                pc = subprocess.run(self.setup_command,
3900                                    cwd=srcdir,
3901                                    stdout=subprocess.PIPE,
3902                                    stderr=subprocess.DEVNULL)
3903                self.assertIn(b'Must specify at least one directory name', pc.stdout)
3904                with tempfile.TemporaryDirectory(dir=srcdir) as builddir:
3905                    subprocess.run(self.setup_command,
3906                                   check=True,
3907                                   cwd=builddir,
3908                                   stdout=subprocess.DEVNULL,
3909                                   stderr=subprocess.DEVNULL)
3910
3911    def get_opts_as_dict(self):
3912        result = {}
3913        for i in self.introspect('--buildoptions'):
3914            result[i['name']] = i['value']
3915        return result
3916
3917    def test_buildtype_setting(self):
3918        testdir = os.path.join(self.common_test_dir, '1 trivial')
3919        self.init(testdir)
3920        opts = self.get_opts_as_dict()
3921        self.assertEqual(opts['buildtype'], 'debug')
3922        self.assertEqual(opts['debug'], True)
3923        self.setconf('-Ddebug=false')
3924        opts = self.get_opts_as_dict()
3925        self.assertEqual(opts['debug'], False)
3926        self.assertEqual(opts['buildtype'], 'plain')
3927        self.assertEqual(opts['optimization'], '0')
3928
3929        # Setting optimizations to 3 should cause buildtype
3930        # to go to release mode.
3931        self.setconf('-Doptimization=3')
3932        opts = self.get_opts_as_dict()
3933        self.assertEqual(opts['buildtype'], 'release')
3934        self.assertEqual(opts['debug'], False)
3935        self.assertEqual(opts['optimization'], '3')
3936
3937        # Going to debug build type should reset debugging
3938        # and optimization
3939        self.setconf('-Dbuildtype=debug')
3940        opts = self.get_opts_as_dict()
3941        self.assertEqual(opts['buildtype'], 'debug')
3942        self.assertEqual(opts['debug'], True)
3943        self.assertEqual(opts['optimization'], '0')
3944
3945        # Command-line parsing of buildtype settings should be the same as
3946        # setting with `meson configure`.
3947        #
3948        # Setting buildtype should set optimization/debug
3949        self.new_builddir()
3950        self.init(testdir, extra_args=['-Dbuildtype=debugoptimized'])
3951        opts = self.get_opts_as_dict()
3952        self.assertEqual(opts['debug'], True)
3953        self.assertEqual(opts['optimization'], '2')
3954        self.assertEqual(opts['buildtype'], 'debugoptimized')
3955        # Setting optimization/debug should set buildtype
3956        self.new_builddir()
3957        self.init(testdir, extra_args=['-Doptimization=2', '-Ddebug=true'])
3958        opts = self.get_opts_as_dict()
3959        self.assertEqual(opts['debug'], True)
3960        self.assertEqual(opts['optimization'], '2')
3961        self.assertEqual(opts['buildtype'], 'debugoptimized')
3962        # Setting both buildtype and debug on the command-line should work, and
3963        # should warn not to do that. Also test that --debug is parsed as -Ddebug=true
3964        self.new_builddir()
3965        out = self.init(testdir, extra_args=['-Dbuildtype=debugoptimized', '--debug'])
3966        self.assertRegex(out, 'Recommend using either.*buildtype.*debug.*redundant')
3967        opts = self.get_opts_as_dict()
3968        self.assertEqual(opts['debug'], True)
3969        self.assertEqual(opts['optimization'], '2')
3970        self.assertEqual(opts['buildtype'], 'debugoptimized')
3971
3972    @skipIfNoPkgconfig
3973    @unittest.skipIf(is_windows(), 'Help needed with fixing this test on windows')
3974    def test_native_dep_pkgconfig(self):
3975        testdir = os.path.join(self.unit_test_dir,
3976                               '46 native dep pkgconfig var')
3977        with tempfile.NamedTemporaryFile(mode='w', delete=False) as crossfile:
3978            crossfile.write(textwrap.dedent(
3979                '''[binaries]
3980                pkgconfig = '{0}'
3981
3982                [properties]
3983
3984                [host_machine]
3985                system = 'linux'
3986                cpu_family = 'arm'
3987                cpu = 'armv7'
3988                endian = 'little'
3989                '''.format(os.path.join(testdir, 'cross_pkgconfig.py'))))
3990            crossfile.flush()
3991            self.meson_cross_file = crossfile.name
3992
3993        env = {'PKG_CONFIG_LIBDIR':  os.path.join(testdir,
3994                                                  'native_pkgconfig')}
3995        self.init(testdir, extra_args=['-Dstart_native=false'], override_envvars=env)
3996        self.wipe()
3997        self.init(testdir, extra_args=['-Dstart_native=true'], override_envvars=env)
3998
3999    @skipIfNoPkgconfig
4000    @unittest.skipIf(is_windows(), 'Help needed with fixing this test on windows')
4001    def test_pkg_config_libdir(self):
4002        testdir = os.path.join(self.unit_test_dir,
4003                               '46 native dep pkgconfig var')
4004        with tempfile.NamedTemporaryFile(mode='w', delete=False) as crossfile:
4005            crossfile.write(textwrap.dedent(
4006                '''[binaries]
4007                pkgconfig = 'pkg-config'
4008
4009                [properties]
4010                pkg_config_libdir = ['{0}']
4011
4012                [host_machine]
4013                system = 'linux'
4014                cpu_family = 'arm'
4015                cpu = 'armv7'
4016                endian = 'little'
4017                '''.format(os.path.join(testdir, 'cross_pkgconfig'))))
4018            crossfile.flush()
4019            self.meson_cross_file = crossfile.name
4020
4021        env = {'PKG_CONFIG_LIBDIR':  os.path.join(testdir,
4022                                                  'native_pkgconfig')}
4023        self.init(testdir, extra_args=['-Dstart_native=false'], override_envvars=env)
4024        self.wipe()
4025        self.init(testdir, extra_args=['-Dstart_native=true'], override_envvars=env)
4026
4027    def __reconfigure(self, change_minor=False):
4028        # Set an older version to force a reconfigure from scratch
4029        filename = os.path.join(self.privatedir, 'coredata.dat')
4030        with open(filename, 'rb') as f:
4031            obj = pickle.load(f)
4032        if change_minor:
4033            v = mesonbuild.coredata.version.split('.')
4034            obj.version = '.'.join(v[0:2] + [str(int(v[2]) + 1)])
4035        else:
4036            obj.version = '0.47.0'
4037        with open(filename, 'wb') as f:
4038            pickle.dump(obj, f)
4039
4040    def test_reconfigure(self):
4041        testdir = os.path.join(self.unit_test_dir, '48 reconfigure')
4042        self.init(testdir, extra_args=['-Dopt1=val1'])
4043        self.setconf('-Dopt2=val2')
4044
4045        self.__reconfigure()
4046
4047        out = self.init(testdir, extra_args=['--reconfigure', '-Dopt3=val3'])
4048        self.assertRegex(out, 'Regenerating configuration from scratch')
4049        self.assertRegex(out, 'opt1 val1')
4050        self.assertRegex(out, 'opt2 val2')
4051        self.assertRegex(out, 'opt3 val3')
4052        self.assertRegex(out, 'opt4 default4')
4053        self.build()
4054        self.run_tests()
4055
4056        # Create a file in builddir and verify wipe command removes it
4057        filename = os.path.join(self.builddir, 'something')
4058        open(filename, 'w').close()
4059        self.assertTrue(os.path.exists(filename))
4060        out = self.init(testdir, extra_args=['--wipe', '-Dopt4=val4'])
4061        self.assertFalse(os.path.exists(filename))
4062        self.assertRegex(out, 'opt1 val1')
4063        self.assertRegex(out, 'opt2 val2')
4064        self.assertRegex(out, 'opt3 val3')
4065        self.assertRegex(out, 'opt4 val4')
4066        self.build()
4067        self.run_tests()
4068
4069    def test_wipe_from_builddir(self):
4070        testdir = os.path.join(self.common_test_dir, '161 custom target subdir depend files')
4071        self.init(testdir)
4072        self.__reconfigure()
4073
4074        with Path(self.builddir):
4075            self.init(testdir, extra_args=['--wipe'])
4076
4077    def test_minor_version_does_not_reconfigure_wipe(self):
4078        testdir = os.path.join(self.unit_test_dir, '48 reconfigure')
4079        self.init(testdir, extra_args=['-Dopt1=val1'])
4080        self.setconf('-Dopt2=val2')
4081
4082        self.__reconfigure(change_minor=True)
4083
4084        out = self.init(testdir, extra_args=['--reconfigure', '-Dopt3=val3'])
4085        self.assertNotRegex(out, 'Regenerating configuration from scratch')
4086        self.assertRegex(out, 'opt1 val1')
4087        self.assertRegex(out, 'opt2 val2')
4088        self.assertRegex(out, 'opt3 val3')
4089        self.assertRegex(out, 'opt4 default4')
4090        self.build()
4091        self.run_tests()
4092
4093    def test_target_construct_id_from_path(self):
4094        # This id is stable but not guessable.
4095        # The test is supposed to prevent unintentional
4096        # changes of target ID generation.
4097        target_id = Target.construct_id_from_path('some/obscure/subdir',
4098                                                  'target-id', '@suffix')
4099        self.assertEqual('5e002d3@@target-id@suffix', target_id)
4100        target_id = Target.construct_id_from_path('subproject/foo/subdir/bar',
4101                                                  'target2-id', '@other')
4102        self.assertEqual('81d46d1@@target2-id@other', target_id)
4103
4104    def test_introspect_projectinfo_without_configured_build(self):
4105        testfile = os.path.join(self.common_test_dir, '35 run program', 'meson.build')
4106        res = self.introspect_directory(testfile, '--projectinfo')
4107        self.assertEqual(set(res['buildsystem_files']), set(['meson.build']))
4108        self.assertEqual(res['version'], 'undefined')
4109        self.assertEqual(res['descriptive_name'], 'run command')
4110        self.assertEqual(res['subprojects'], [])
4111
4112        testfile = os.path.join(self.common_test_dir, '43 options', 'meson.build')
4113        res = self.introspect_directory(testfile, '--projectinfo')
4114        self.assertEqual(set(res['buildsystem_files']), set(['meson_options.txt', 'meson.build']))
4115        self.assertEqual(res['version'], 'undefined')
4116        self.assertEqual(res['descriptive_name'], 'options')
4117        self.assertEqual(res['subprojects'], [])
4118
4119        testfile = os.path.join(self.common_test_dir, '46 subproject options', 'meson.build')
4120        res = self.introspect_directory(testfile, '--projectinfo')
4121        self.assertEqual(set(res['buildsystem_files']), set(['meson_options.txt', 'meson.build']))
4122        self.assertEqual(res['version'], 'undefined')
4123        self.assertEqual(res['descriptive_name'], 'suboptions')
4124        self.assertEqual(len(res['subprojects']), 1)
4125        subproject_files = set(f.replace('\\', '/') for f in res['subprojects'][0]['buildsystem_files'])
4126        self.assertEqual(subproject_files, set(['subprojects/subproject/meson_options.txt', 'subprojects/subproject/meson.build']))
4127        self.assertEqual(res['subprojects'][0]['name'], 'subproject')
4128        self.assertEqual(res['subprojects'][0]['version'], 'undefined')
4129        self.assertEqual(res['subprojects'][0]['descriptive_name'], 'subproject')
4130
4131    def test_introspect_projectinfo_subprojects(self):
4132        testdir = os.path.join(self.common_test_dir, '102 subproject subdir')
4133        self.init(testdir)
4134        res = self.introspect('--projectinfo')
4135        expected = {
4136            'descriptive_name': 'proj',
4137            'version': 'undefined',
4138            'subproject_dir': 'subprojects',
4139            'subprojects': [
4140                {
4141                    'descriptive_name': 'sub',
4142                    'name': 'sub',
4143                    'version': '1.0'
4144                },
4145                {
4146                    'descriptive_name': 'sub_implicit',
4147                    'name': 'sub_implicit',
4148                    'version': '1.0',
4149                },
4150                {
4151                    'descriptive_name': 'sub-novar',
4152                    'name': 'sub_novar',
4153                    'version': '1.0',
4154                },
4155            ]
4156        }
4157        res['subprojects'] = sorted(res['subprojects'], key=lambda i: i['name'])
4158        self.assertDictEqual(expected, res)
4159
4160    def test_introspection_target_subproject(self):
4161        testdir = os.path.join(self.common_test_dir, '45 subproject')
4162        self.init(testdir)
4163        res = self.introspect('--targets')
4164
4165        expected = {
4166            'sublib': 'sublib',
4167            'simpletest': 'sublib',
4168            'user': None
4169        }
4170
4171        for entry in res:
4172            name = entry['name']
4173            self.assertEqual(entry['subproject'], expected[name])
4174
4175    def test_introspect_projectinfo_subproject_dir(self):
4176        testdir = os.path.join(self.common_test_dir, '78 custom subproject dir')
4177        self.init(testdir)
4178        res = self.introspect('--projectinfo')
4179
4180        self.assertEqual(res['subproject_dir'], 'custom_subproject_dir')
4181
4182    def test_introspect_projectinfo_subproject_dir_from_source(self):
4183        testfile = os.path.join(self.common_test_dir, '78 custom subproject dir', 'meson.build')
4184        res = self.introspect_directory(testfile, '--projectinfo')
4185
4186        self.assertEqual(res['subproject_dir'], 'custom_subproject_dir')
4187
4188    @skipIfNoExecutable('clang-format')
4189    def test_clang_format(self):
4190        if self.backend is not Backend.ninja:
4191            raise unittest.SkipTest('Clang-format is for now only supported on Ninja, not {}'.format(self.backend.name))
4192        testdir = os.path.join(self.unit_test_dir, '54 clang-format')
4193        testfile = os.path.join(testdir, 'prog.c')
4194        badfile = os.path.join(testdir, 'prog_orig_c')
4195        goodfile = os.path.join(testdir, 'prog_expected_c')
4196        testheader = os.path.join(testdir, 'header.h')
4197        badheader = os.path.join(testdir, 'header_orig_h')
4198        goodheader = os.path.join(testdir, 'header_expected_h')
4199        try:
4200            shutil.copyfile(badfile, testfile)
4201            shutil.copyfile(badheader, testheader)
4202            self.init(testdir)
4203            self.assertNotEqual(Path(testfile).read_text(),
4204                                Path(goodfile).read_text())
4205            self.assertNotEqual(Path(testheader).read_text(),
4206                                Path(goodheader).read_text())
4207            self.run_target('clang-format')
4208            self.assertEqual(Path(testheader).read_text(),
4209                             Path(goodheader).read_text())
4210        finally:
4211            if os.path.exists(testfile):
4212                os.unlink(testfile)
4213            if os.path.exists(testheader):
4214                os.unlink(testheader)
4215
4216    @skipIfNoExecutable('clang-tidy')
4217    def test_clang_tidy(self):
4218        if self.backend is not Backend.ninja:
4219            raise unittest.SkipTest('Clang-tidy is for now only supported on Ninja, not {}'.format(self.backend.name))
4220        if shutil.which('c++') is None:
4221            raise unittest.SkipTest('Clang-tidy breaks when ccache is used and "c++" not in path.')
4222        if is_osx():
4223            raise unittest.SkipTest('Apple ships a broken clang-tidy that chokes on -pipe.')
4224        testdir = os.path.join(self.unit_test_dir, '70 clang-tidy')
4225        self.init(testdir, override_envvars={'CXX': 'c++'})
4226        out = self.run_target('clang-tidy')
4227        self.assertIn('cttest.cpp:4:20', out)
4228
4229    def test_identity_cross(self):
4230        testdir = os.path.join(self.unit_test_dir, '71 cross')
4231        # Do a build to generate a cross file where the host is this target
4232        self.init(testdir, extra_args=['-Dgenerate=true'])
4233        self.meson_cross_file = os.path.join(self.builddir, "crossfile")
4234        self.assertTrue(os.path.exists(self.meson_cross_file))
4235        # Now verify that this is detected as cross
4236        self.new_builddir()
4237        self.init(testdir)
4238
4239    def test_introspect_buildoptions_without_configured_build(self):
4240        testdir = os.path.join(self.unit_test_dir, '59 introspect buildoptions')
4241        testfile = os.path.join(testdir, 'meson.build')
4242        res_nb = self.introspect_directory(testfile, ['--buildoptions'] + self.meson_args)
4243        self.init(testdir, default_args=False)
4244        res_wb = self.introspect('--buildoptions')
4245        self.maxDiff = None
4246        self.assertListEqual(res_nb, res_wb)
4247
4248    def test_meson_configure_from_source_does_not_crash(self):
4249        testdir = os.path.join(self.unit_test_dir, '59 introspect buildoptions')
4250        self._run(self.mconf_command + [testdir])
4251
4252    def test_introspect_json_dump(self):
4253        testdir = os.path.join(self.unit_test_dir, '57 introspection')
4254        self.init(testdir)
4255        infodir = os.path.join(self.builddir, 'meson-info')
4256        self.assertPathExists(infodir)
4257
4258        def assertKeyTypes(key_type_list, obj):
4259            for i in key_type_list:
4260                self.assertIn(i[0], obj)
4261                self.assertIsInstance(obj[i[0]], i[1])
4262
4263        root_keylist = [
4264            ('benchmarks', list),
4265            ('buildoptions', list),
4266            ('buildsystem_files', list),
4267            ('dependencies', list),
4268            ('installed', dict),
4269            ('projectinfo', dict),
4270            ('targets', list),
4271            ('tests', list),
4272        ]
4273
4274        test_keylist = [
4275            ('cmd', list),
4276            ('env', dict),
4277            ('name', str),
4278            ('timeout', int),
4279            ('suite', list),
4280            ('is_parallel', bool),
4281            ('protocol', str),
4282        ]
4283
4284        buildoptions_keylist = [
4285            ('name', str),
4286            ('section', str),
4287            ('type', str),
4288            ('description', str),
4289            ('machine', str),
4290        ]
4291
4292        buildoptions_typelist = [
4293            ('combo', str, [('choices', list)]),
4294            ('string', str, []),
4295            ('boolean', bool, []),
4296            ('integer', int, []),
4297            ('array', list, []),
4298        ]
4299
4300        buildoptions_sections = ['core', 'backend', 'base', 'compiler', 'directory', 'user', 'test']
4301        buildoptions_machines = ['any', 'build', 'host']
4302
4303        dependencies_typelist = [
4304            ('name', str),
4305            ('version', str),
4306            ('compile_args', list),
4307            ('link_args', list),
4308        ]
4309
4310        targets_typelist = [
4311            ('name', str),
4312            ('id', str),
4313            ('type', str),
4314            ('defined_in', str),
4315            ('filename', list),
4316            ('build_by_default', bool),
4317            ('target_sources', list),
4318            ('installed', bool),
4319        ]
4320
4321        targets_sources_typelist = [
4322            ('language', str),
4323            ('compiler', list),
4324            ('parameters', list),
4325            ('sources', list),
4326            ('generated_sources', list),
4327        ]
4328
4329        # First load all files
4330        res = {}
4331        for i in root_keylist:
4332            curr = os.path.join(infodir, 'intro-{}.json'.format(i[0]))
4333            self.assertPathExists(curr)
4334            with open(curr, 'r') as fp:
4335                res[i[0]] = json.load(fp)
4336
4337        assertKeyTypes(root_keylist, res)
4338
4339        # Check Tests and benchmarks
4340        tests_to_find = ['test case 1', 'test case 2', 'benchmark 1']
4341        for i in res['benchmarks'] + res['tests']:
4342            assertKeyTypes(test_keylist, i)
4343            if i['name'] in tests_to_find:
4344                tests_to_find.remove(i['name'])
4345        self.assertListEqual(tests_to_find, [])
4346
4347        # Check buildoptions
4348        buildopts_to_find = {'cpp_std': 'c++11'}
4349        for i in res['buildoptions']:
4350            assertKeyTypes(buildoptions_keylist, i)
4351            valid_type = False
4352            for j in buildoptions_typelist:
4353                if i['type'] == j[0]:
4354                    self.assertIsInstance(i['value'], j[1])
4355                    assertKeyTypes(j[2], i)
4356                    valid_type = True
4357                    break
4358
4359            self.assertIn(i['section'], buildoptions_sections)
4360            self.assertIn(i['machine'], buildoptions_machines)
4361            self.assertTrue(valid_type)
4362            if i['name'] in buildopts_to_find:
4363                self.assertEqual(i['value'], buildopts_to_find[i['name']])
4364                buildopts_to_find.pop(i['name'], None)
4365        self.assertDictEqual(buildopts_to_find, {})
4366
4367        # Check buildsystem_files
4368        bs_files = ['meson.build', 'meson_options.txt', 'sharedlib/meson.build', 'staticlib/meson.build']
4369        bs_files = [os.path.join(testdir, x) for x in bs_files]
4370        self.assertPathListEqual(list(sorted(res['buildsystem_files'])), list(sorted(bs_files)))
4371
4372        # Check dependencies
4373        dependencies_to_find = ['threads']
4374        for i in res['dependencies']:
4375            assertKeyTypes(dependencies_typelist, i)
4376            if i['name'] in dependencies_to_find:
4377                dependencies_to_find.remove(i['name'])
4378        self.assertListEqual(dependencies_to_find, [])
4379
4380        # Check projectinfo
4381        self.assertDictEqual(res['projectinfo'], {'version': '1.2.3', 'descriptive_name': 'introspection', 'subproject_dir': 'subprojects', 'subprojects': []})
4382
4383        # Check targets
4384        targets_to_find = {
4385            'sharedTestLib': ('shared library', True, False, 'sharedlib/meson.build'),
4386            'staticTestLib': ('static library', True, False, 'staticlib/meson.build'),
4387            'test1': ('executable', True, True, 'meson.build'),
4388            'test2': ('executable', True, False, 'meson.build'),
4389            'test3': ('executable', True, False, 'meson.build'),
4390        }
4391        for i in res['targets']:
4392            assertKeyTypes(targets_typelist, i)
4393            if i['name'] in targets_to_find:
4394                tgt = targets_to_find[i['name']]
4395                self.assertEqual(i['type'], tgt[0])
4396                self.assertEqual(i['build_by_default'], tgt[1])
4397                self.assertEqual(i['installed'], tgt[2])
4398                self.assertPathEqual(i['defined_in'], os.path.join(testdir, tgt[3]))
4399                targets_to_find.pop(i['name'], None)
4400            for j in i['target_sources']:
4401                assertKeyTypes(targets_sources_typelist, j)
4402        self.assertDictEqual(targets_to_find, {})
4403
4404    def test_introspect_file_dump_equals_all(self):
4405        testdir = os.path.join(self.unit_test_dir, '57 introspection')
4406        self.init(testdir)
4407        res_all = self.introspect('--all')
4408        res_file = {}
4409
4410        root_keylist = [
4411            'benchmarks',
4412            'buildoptions',
4413            'buildsystem_files',
4414            'dependencies',
4415            'installed',
4416            'projectinfo',
4417            'targets',
4418            'tests',
4419        ]
4420
4421        infodir = os.path.join(self.builddir, 'meson-info')
4422        self.assertPathExists(infodir)
4423        for i in root_keylist:
4424            curr = os.path.join(infodir, 'intro-{}.json'.format(i))
4425            self.assertPathExists(curr)
4426            with open(curr, 'r') as fp:
4427                res_file[i] = json.load(fp)
4428
4429        self.assertEqual(res_all, res_file)
4430
4431    def test_introspect_meson_info(self):
4432        testdir = os.path.join(self.unit_test_dir, '57 introspection')
4433        introfile = os.path.join(self.builddir, 'meson-info', 'meson-info.json')
4434        self.init(testdir)
4435        self.assertPathExists(introfile)
4436        with open(introfile, 'r') as fp:
4437            res1 = json.load(fp)
4438
4439        for i in ['meson_version', 'directories', 'introspection', 'build_files_updated', 'error']:
4440            self.assertIn(i, res1)
4441
4442        self.assertEqual(res1['error'], False)
4443        self.assertEqual(res1['build_files_updated'], True)
4444
4445    def test_introspect_config_update(self):
4446        testdir = os.path.join(self.unit_test_dir, '57 introspection')
4447        introfile = os.path.join(self.builddir, 'meson-info', 'intro-buildoptions.json')
4448        self.init(testdir)
4449        self.assertPathExists(introfile)
4450        with open(introfile, 'r') as fp:
4451            res1 = json.load(fp)
4452
4453        self.setconf('-Dcpp_std=c++14')
4454        self.setconf('-Dbuildtype=release')
4455
4456        for idx, i in enumerate(res1):
4457            if i['name'] == 'cpp_std':
4458                res1[idx]['value'] = 'c++14'
4459            if i['name'] == 'build.cpp_std':
4460                res1[idx]['value'] = 'c++14'
4461            if i['name'] == 'buildtype':
4462                res1[idx]['value'] = 'release'
4463            if i['name'] == 'optimization':
4464                res1[idx]['value'] = '3'
4465            if i['name'] == 'debug':
4466                res1[idx]['value'] = False
4467
4468        with open(introfile, 'r') as fp:
4469            res2 = json.load(fp)
4470
4471        self.assertListEqual(res1, res2)
4472
4473    def test_introspect_targets_from_source(self):
4474        testdir = os.path.join(self.unit_test_dir, '57 introspection')
4475        testfile = os.path.join(testdir, 'meson.build')
4476        introfile = os.path.join(self.builddir, 'meson-info', 'intro-targets.json')
4477        self.init(testdir)
4478        self.assertPathExists(introfile)
4479        with open(introfile, 'r') as fp:
4480            res_wb = json.load(fp)
4481
4482        res_nb = self.introspect_directory(testfile, ['--targets'] + self.meson_args)
4483
4484        # Account for differences in output
4485        for i in res_wb:
4486            i['filename'] = [os.path.relpath(x, self.builddir) for x in i['filename']]
4487            if 'install_filename' in i:
4488                del i['install_filename']
4489
4490            sources = []
4491            for j in i['target_sources']:
4492                sources += j['sources']
4493            i['target_sources'] = [{
4494                'language': 'unknown',
4495                'compiler': [],
4496                'parameters': [],
4497                'sources': sources,
4498                'generated_sources': []
4499            }]
4500
4501        self.maxDiff = None
4502        self.assertListEqual(res_nb, res_wb)
4503
4504    def test_introspect_ast_source(self):
4505        testdir = os.path.join(self.unit_test_dir, '57 introspection')
4506        testfile = os.path.join(testdir, 'meson.build')
4507        res_nb = self.introspect_directory(testfile, ['--ast'] + self.meson_args)
4508
4509        node_counter = {}
4510
4511        def accept_node(json_node):
4512            self.assertIsInstance(json_node, dict)
4513            for i in ['lineno', 'colno', 'end_lineno', 'end_colno']:
4514                self.assertIn(i, json_node)
4515                self.assertIsInstance(json_node[i], int)
4516            self.assertIn('node', json_node)
4517            n = json_node['node']
4518            self.assertIsInstance(n, str)
4519            self.assertIn(n, nodes)
4520            if n not in node_counter:
4521                node_counter[n] = 0
4522            node_counter[n] = node_counter[n] + 1
4523            for nodeDesc in nodes[n]:
4524                key = nodeDesc[0]
4525                func = nodeDesc[1]
4526                self.assertIn(key, json_node)
4527                if func is None:
4528                    tp = nodeDesc[2]
4529                    self.assertIsInstance(json_node[key], tp)
4530                    continue
4531                func(json_node[key])
4532
4533        def accept_node_list(node_list):
4534            self.assertIsInstance(node_list, list)
4535            for i in node_list:
4536                accept_node(i)
4537
4538        def accept_kwargs(kwargs):
4539            self.assertIsInstance(kwargs, list)
4540            for i in kwargs:
4541                self.assertIn('key', i)
4542                self.assertIn('val', i)
4543                accept_node(i['key'])
4544                accept_node(i['val'])
4545
4546        nodes = {
4547            'BooleanNode': [('value', None, bool)],
4548            'IdNode': [('value', None, str)],
4549            'NumberNode': [('value', None, int)],
4550            'StringNode': [('value', None, str)],
4551            'ContinueNode': [],
4552            'BreakNode': [],
4553            'ArgumentNode': [('positional', accept_node_list), ('kwargs', accept_kwargs)],
4554            'ArrayNode': [('args', accept_node)],
4555            'DictNode': [('args', accept_node)],
4556            'EmptyNode': [],
4557            'OrNode': [('left', accept_node), ('right', accept_node)],
4558            'AndNode': [('left', accept_node), ('right', accept_node)],
4559            'ComparisonNode': [('left', accept_node), ('right', accept_node), ('ctype', None, str)],
4560            'ArithmeticNode': [('left', accept_node), ('right', accept_node), ('op', None, str)],
4561            'NotNode': [('right', accept_node)],
4562            'CodeBlockNode': [('lines', accept_node_list)],
4563            'IndexNode': [('object', accept_node), ('index', accept_node)],
4564            'MethodNode': [('object', accept_node), ('args', accept_node), ('name', None, str)],
4565            'FunctionNode': [('args', accept_node), ('name', None, str)],
4566            'AssignmentNode': [('value', accept_node), ('var_name', None, str)],
4567            'PlusAssignmentNode': [('value', accept_node), ('var_name', None, str)],
4568            'ForeachClauseNode': [('items', accept_node), ('block', accept_node), ('varnames', None, list)],
4569            'IfClauseNode': [('ifs', accept_node_list), ('else', accept_node)],
4570            'IfNode': [('condition', accept_node), ('block', accept_node)],
4571            'UMinusNode': [('right', accept_node)],
4572            'TernaryNode': [('condition', accept_node), ('true', accept_node), ('false', accept_node)],
4573        }
4574
4575        accept_node(res_nb)
4576
4577        for n, c in [('ContinueNode', 2), ('BreakNode', 1), ('NotNode', 3)]:
4578            self.assertIn(n, node_counter)
4579            self.assertEqual(node_counter[n], c)
4580
4581    def test_introspect_dependencies_from_source(self):
4582        testdir = os.path.join(self.unit_test_dir, '57 introspection')
4583        testfile = os.path.join(testdir, 'meson.build')
4584        res_nb = self.introspect_directory(testfile, ['--scan-dependencies'] + self.meson_args)
4585        expected = [
4586            {
4587                'name': 'threads',
4588                'required': True,
4589                'version': [],
4590                'has_fallback': False,
4591                'conditional': False
4592            },
4593            {
4594                'name': 'zlib',
4595                'required': False,
4596                'version': [],
4597                'has_fallback': False,
4598                'conditional': False
4599            },
4600            {
4601                'name': 'bugDep1',
4602                'required': True,
4603                'version': [],
4604                'has_fallback': False,
4605                'conditional': False
4606            },
4607            {
4608                'name': 'somethingthatdoesnotexist',
4609                'required': True,
4610                'version': ['>=1.2.3'],
4611                'has_fallback': False,
4612                'conditional': True
4613            },
4614            {
4615                'name': 'look_i_have_a_fallback',
4616                'required': True,
4617                'version': ['>=1.0.0', '<=99.9.9'],
4618                'has_fallback': True,
4619                'conditional': True
4620            }
4621        ]
4622        self.maxDiff = None
4623        self.assertListEqual(res_nb, expected)
4624
4625    def test_unstable_coredata(self):
4626        testdir = os.path.join(self.common_test_dir, '1 trivial')
4627        self.init(testdir)
4628        # just test that the command does not fail (e.g. because it throws an exception)
4629        self._run([*self.meson_command, 'unstable-coredata', self.builddir])
4630
4631    @skip_if_no_cmake
4632    def test_cmake_prefix_path(self):
4633        testdir = os.path.join(self.unit_test_dir, '64 cmake_prefix_path')
4634        self.init(testdir, extra_args=['-Dcmake_prefix_path=' + os.path.join(testdir, 'prefix')])
4635
4636    @skip_if_no_cmake
4637    def test_cmake_parser(self):
4638        testdir = os.path.join(self.unit_test_dir, '65 cmake parser')
4639        self.init(testdir, extra_args=['-Dcmake_prefix_path=' + os.path.join(testdir, 'prefix')])
4640
4641    def test_alias_target(self):
4642        if self.backend is Backend.vs:
4643            # FIXME: This unit test is broken with vs backend, needs investigation
4644            raise unittest.SkipTest('Skipping alias_target test with {} backend'.format(self.backend.name))
4645        testdir = os.path.join(self.unit_test_dir, '66 alias target')
4646        self.init(testdir)
4647        self.build()
4648        self.assertPathDoesNotExist(os.path.join(self.builddir, 'prog' + exe_suffix))
4649        self.assertPathDoesNotExist(os.path.join(self.builddir, 'hello.txt'))
4650        self.run_target('build-all')
4651        self.assertPathExists(os.path.join(self.builddir, 'prog' + exe_suffix))
4652        self.assertPathExists(os.path.join(self.builddir, 'hello.txt'))
4653
4654    def test_configure(self):
4655        testdir = os.path.join(self.common_test_dir, '2 cpp')
4656        self.init(testdir)
4657        self._run(self.mconf_command + [self.builddir])
4658
4659    def test_summary(self):
4660        testdir = os.path.join(self.unit_test_dir, '73 summary')
4661        out = self.init(testdir)
4662        expected = textwrap.dedent(r'''
4663            Some Subproject 2.0
4664
4665                 string: bar
4666                integer: 1
4667                boolean: True
4668
4669            My Project 1.0
4670
4671              Configuration
4672                   Some boolean: False
4673                Another boolean: True
4674                    Some string: Hello World
4675                         A list: string
4676                                 1
4677                                 True
4678                     empty list:
4679                       A number: 1
4680                            yes: YES
4681                             no: NO
4682                      coma list: a, b, c
4683
4684              Subprojects
4685                            sub: YES
4686                           sub2: NO Problem encountered: This subproject failed
4687            ''')
4688        expected_lines = expected.split('\n')[1:]
4689        out_start = out.find(expected_lines[0])
4690        out_lines = out[out_start:].split('\n')[:len(expected_lines)]
4691        if sys.version_info < (3, 7, 0):
4692            # Dictionary order is not stable in Python <3.7, so sort the lines
4693            # while comparing
4694            self.assertEqual(sorted(expected_lines), sorted(out_lines))
4695        else:
4696            self.assertEqual(expected_lines, out_lines)
4697
4698    def test_meson_compile(self):
4699        """Test the meson compile command."""
4700
4701        def get_exe_name(basename: str) -> str:
4702            if is_windows():
4703                return '{}.exe'.format(basename)
4704            else:
4705                return basename
4706
4707        def get_shared_lib_name(basename: str) -> str:
4708            if mesonbuild.environment.detect_msys2_arch():
4709                return 'lib{}.dll'.format(basename)
4710            elif is_windows():
4711                return '{}.dll'.format(basename)
4712            elif is_cygwin():
4713                return 'cyg{}.dll'.format(basename)
4714            elif is_osx():
4715                return 'lib{}.dylib'.format(basename)
4716            else:
4717                return 'lib{}.so'.format(basename)
4718
4719        def get_static_lib_name(basename: str) -> str:
4720            return 'lib{}.a'.format(basename)
4721
4722        # Base case (no targets or additional arguments)
4723
4724        testdir = os.path.join(self.common_test_dir, '1 trivial')
4725        self.init(testdir)
4726
4727        self._run([*self.meson_command, 'compile', '-C', self.builddir])
4728        self.assertPathExists(os.path.join(self.builddir, get_exe_name('trivialprog')))
4729
4730        # `--clean`
4731
4732        self._run([*self.meson_command, 'compile', '-C', self.builddir, '--clean'])
4733        self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog')))
4734
4735        # Target specified in a project with unique names
4736
4737        testdir = os.path.join(self.common_test_dir, '6 linkshared')
4738        self.init(testdir, extra_args=['--wipe'])
4739        # Multiple targets and target type specified
4740        self._run([*self.meson_command, 'compile', '-C', self.builddir, 'mylib', 'mycpplib:shared_library'])
4741        # Check that we have a shared lib, but not an executable, i.e. check that target actually worked
4742        self.assertPathExists(os.path.join(self.builddir, get_shared_lib_name('mylib')))
4743        self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('prog')))
4744        self.assertPathExists(os.path.join(self.builddir, get_shared_lib_name('mycpplib')))
4745        self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('cppprog')))
4746
4747        # Target specified in a project with non unique names
4748
4749        testdir = os.path.join(self.common_test_dir, '190 same target name')
4750        self.init(testdir, extra_args=['--wipe'])
4751        self._run([*self.meson_command, 'compile', '-C', self.builddir, './foo'])
4752        self.assertPathExists(os.path.join(self.builddir, get_static_lib_name('foo')))
4753        self._run([*self.meson_command, 'compile', '-C', self.builddir, 'sub/foo'])
4754        self.assertPathExists(os.path.join(self.builddir, 'sub', get_static_lib_name('foo')))
4755
4756        # run_target
4757
4758        testdir = os.path.join(self.common_test_dir, '54 run target')
4759        self.init(testdir, extra_args=['--wipe'])
4760        out = self._run([*self.meson_command, 'compile', '-C', self.builddir, 'py3hi'])
4761        self.assertIn('I am Python3.', out)
4762
4763        # `--$BACKEND-args`
4764
4765        testdir = os.path.join(self.common_test_dir, '1 trivial')
4766        if self.backend is Backend.ninja:
4767            self.init(testdir, extra_args=['--wipe'])
4768            # Dry run - should not create a program
4769            self._run([*self.meson_command, 'compile', '-C', self.builddir, '--ninja-args=-n'])
4770            self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog')))
4771        elif self.backend is Backend.vs:
4772            self.init(testdir, extra_args=['--wipe'])
4773            self._run([*self.meson_command, 'compile', '-C', self.builddir])
4774            # Explicitly clean the target through msbuild interface
4775            self._run([*self.meson_command, 'compile', '-C', self.builddir, '--vs-args=-t:{}:Clean'.format(re.sub(r'[\%\$\@\;\.\(\)\']', '_', get_exe_name('trivialprog')))])
4776            self.assertPathDoesNotExist(os.path.join(self.builddir, get_exe_name('trivialprog')))
4777
4778    def test_spurious_reconfigure_built_dep_file(self):
4779        testdir = os.path.join(self.unit_test_dir, '75 dep files')
4780
4781        # Regression test: Spurious reconfigure was happening when build
4782        # directory is inside source directory.
4783        # See https://gitlab.freedesktop.org/gstreamer/gst-build/-/issues/85.
4784        srcdir = os.path.join(self.builddir, 'srctree')
4785        shutil.copytree(testdir, srcdir)
4786        builddir = os.path.join(srcdir, '_build')
4787        self.change_builddir(builddir)
4788
4789        self.init(srcdir)
4790        self.build()
4791
4792        # During first configure the file did not exist so no dependency should
4793        # have been set. A rebuild should not trigger a reconfigure.
4794        self.clean()
4795        out = self.build()
4796        self.assertNotIn('Project configured', out)
4797
4798        self.init(srcdir, extra_args=['--reconfigure'])
4799
4800        # During the reconfigure the file did exist, but is inside build
4801        # directory, so no dependency should have been set. A rebuild should not
4802        # trigger a reconfigure.
4803        self.clean()
4804        out = self.build()
4805        self.assertNotIn('Project configured', out)
4806
4807    def _test_junit(self, case: str) -> None:
4808        try:
4809            import lxml.etree as et
4810        except ImportError:
4811            raise unittest.SkipTest('lxml required, but not found.')
4812
4813        schema = et.XMLSchema(et.parse(str(Path(__file__).parent / 'data' / 'schema.xsd')))
4814
4815        self.init(case)
4816        self.run_tests()
4817
4818        junit = et.parse(str(Path(self.builddir) / 'meson-logs' / 'testlog.junit.xml'))
4819        try:
4820            schema.assertValid(junit)
4821        except et.DocumentInvalid as e:
4822            self.fail(e.error_log)
4823
4824    def test_junit_valid_tap(self):
4825        self._test_junit(os.path.join(self.common_test_dir, '213 tap tests'))
4826
4827    def test_junit_valid_exitcode(self):
4828        self._test_junit(os.path.join(self.common_test_dir, '44 test args'))
4829
4830    def test_junit_valid_gtest(self):
4831        self._test_junit(os.path.join(self.framework_test_dir, '2 gtest'))
4832
4833    def test_link_language_linker(self):
4834        # TODO: there should be some way to query how we're linking things
4835        # without resorting to reading the ninja.build file
4836        if self.backend is not Backend.ninja:
4837            raise unittest.SkipTest('This test reads the ninja file')
4838
4839        testdir = os.path.join(self.common_test_dir, '232 link language')
4840        self.init(testdir)
4841
4842        build_ninja = os.path.join(self.builddir, 'build.ninja')
4843        with open(build_ninja, 'r', encoding='utf-8') as f:
4844            contents = f.read()
4845
4846        self.assertRegex(contents, r'build main(\.exe)?.*: c_LINKER')
4847        self.assertRegex(contents, r'build (lib|cyg)?mylib.*: c_LINKER')
4848
4849    def test_commands_documented(self):
4850        '''
4851        Test that all listed meson commands are documented in Commands.md.
4852        '''
4853
4854        # The docs directory is not in release tarballs.
4855        if not os.path.isdir('docs'):
4856            raise unittest.SkipTest('Doc directory does not exist.')
4857        doc_path = 'docs/markdown_dynamic/Commands.md'
4858
4859        md = None
4860        with open(doc_path, encoding='utf-8') as f:
4861            md = f.read()
4862        self.assertIsNotNone(md)
4863
4864        ## Get command sections
4865
4866        section_pattern = re.compile(r'^### (.+)$', re.MULTILINE)
4867        md_command_section_matches = [i for i in section_pattern.finditer(md)]
4868        md_command_sections = dict()
4869        for i, s in enumerate(md_command_section_matches):
4870            section_end = len(md) if i == len(md_command_section_matches) - 1 else md_command_section_matches[i + 1].start()
4871            md_command_sections[s.group(1)] = (s.start(), section_end)
4872
4873        ## Validate commands
4874
4875        md_commands = set(k for k,v in md_command_sections.items())
4876
4877        help_output = self._run(self.meson_command + ['--help'])
4878        help_commands = set(c.strip() for c in re.findall(r'usage:(?:.+)?{((?:[a-z]+,*)+?)}', help_output, re.MULTILINE|re.DOTALL)[0].split(','))
4879
4880        self.assertEqual(md_commands | {'help'}, help_commands, 'Doc file: `{}`'.format(doc_path))
4881
4882        ## Validate that each section has proper placeholders
4883
4884        def get_data_pattern(command):
4885            return re.compile(
4886                r'^```[\r\n]'
4887                r'{{ cmd_help\[\'' + command + r'\'\]\[\'usage\'\] }}[\r\n]'
4888                r'^```[\r\n]'
4889                r'.*?'
4890                r'^```[\r\n]'
4891                r'{{ cmd_help\[\'' + command + r'\'\]\[\'arguments\'\] }}[\r\n]'
4892                r'^```',
4893                flags = re.MULTILINE|re.DOTALL)
4894
4895        for command in md_commands:
4896            m = get_data_pattern(command).search(md, pos=md_command_sections[command][0], endpos=md_command_sections[command][1])
4897            self.assertIsNotNone(m, 'Command `{}` is missing placeholders for dynamic data. Doc file: `{}`'.format(command, doc_path))
4898
4899    def _check_coverage_files(self, types=('text', 'xml', 'html')):
4900        covdir = Path(self.builddir) / 'meson-logs'
4901        files = []
4902        if 'text' in types:
4903            files.append('coverage.txt')
4904        if 'xml' in types:
4905            files.append('coverage.xml')
4906        if 'html' in types:
4907            files.append('coveragereport/index.html')
4908        for f in files:
4909            self.assertTrue((covdir / f).is_file(), msg='{} is not a file'.format(f))
4910
4911    def test_coverage(self):
4912        if mesonbuild.environment.detect_msys2_arch():
4913            raise unittest.SkipTest('Skipped due to problems with coverage on MSYS2')
4914        gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr()
4915        if not gcovr_exe:
4916            raise unittest.SkipTest('gcovr not found, or too old')
4917        testdir = os.path.join(self.common_test_dir, '1 trivial')
4918        env = get_fake_env(testdir, self.builddir, self.prefix)
4919        cc = env.detect_c_compiler(MachineChoice.HOST)
4920        if cc.get_id() == 'clang':
4921            if not mesonbuild.environment.detect_llvm_cov():
4922                raise unittest.SkipTest('llvm-cov not found')
4923        if cc.get_id() == 'msvc':
4924            raise unittest.SkipTest('Test only applies to non-MSVC compilers')
4925        self.init(testdir, extra_args=['-Db_coverage=true'])
4926        self.build()
4927        self.run_tests()
4928        self.run_target('coverage')
4929        self._check_coverage_files()
4930
4931    def test_coverage_complex(self):
4932        if mesonbuild.environment.detect_msys2_arch():
4933            raise unittest.SkipTest('Skipped due to problems with coverage on MSYS2')
4934        gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr()
4935        if not gcovr_exe:
4936            raise unittest.SkipTest('gcovr not found, or too old')
4937        testdir = os.path.join(self.common_test_dir, '109 generatorcustom')
4938        env = get_fake_env(testdir, self.builddir, self.prefix)
4939        cc = env.detect_c_compiler(MachineChoice.HOST)
4940        if cc.get_id() == 'clang':
4941            if not mesonbuild.environment.detect_llvm_cov():
4942                raise unittest.SkipTest('llvm-cov not found')
4943        if cc.get_id() == 'msvc':
4944            raise unittest.SkipTest('Test only applies to non-MSVC compilers')
4945        self.init(testdir, extra_args=['-Db_coverage=true'])
4946        self.build()
4947        self.run_tests()
4948        self.run_target('coverage')
4949        self._check_coverage_files()
4950
4951    def test_coverage_html(self):
4952        if mesonbuild.environment.detect_msys2_arch():
4953            raise unittest.SkipTest('Skipped due to problems with coverage on MSYS2')
4954        gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr()
4955        if not gcovr_exe:
4956            raise unittest.SkipTest('gcovr not found, or too old')
4957        testdir = os.path.join(self.common_test_dir, '1 trivial')
4958        env = get_fake_env(testdir, self.builddir, self.prefix)
4959        cc = env.detect_c_compiler(MachineChoice.HOST)
4960        if cc.get_id() == 'clang':
4961            if not mesonbuild.environment.detect_llvm_cov():
4962                raise unittest.SkipTest('llvm-cov not found')
4963        if cc.get_id() == 'msvc':
4964            raise unittest.SkipTest('Test only applies to non-MSVC compilers')
4965        self.init(testdir, extra_args=['-Db_coverage=true'])
4966        self.build()
4967        self.run_tests()
4968        self.run_target('coverage-html')
4969        self._check_coverage_files(['html'])
4970
4971    def test_coverage_text(self):
4972        if mesonbuild.environment.detect_msys2_arch():
4973            raise unittest.SkipTest('Skipped due to problems with coverage on MSYS2')
4974        gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr()
4975        if not gcovr_exe:
4976            raise unittest.SkipTest('gcovr not found, or too old')
4977        testdir = os.path.join(self.common_test_dir, '1 trivial')
4978        env = get_fake_env(testdir, self.builddir, self.prefix)
4979        cc = env.detect_c_compiler(MachineChoice.HOST)
4980        if cc.get_id() == 'clang':
4981            if not mesonbuild.environment.detect_llvm_cov():
4982                raise unittest.SkipTest('llvm-cov not found')
4983        if cc.get_id() == 'msvc':
4984            raise unittest.SkipTest('Test only applies to non-MSVC compilers')
4985        self.init(testdir, extra_args=['-Db_coverage=true'])
4986        self.build()
4987        self.run_tests()
4988        self.run_target('coverage-text')
4989        self._check_coverage_files(['text'])
4990
4991    def test_coverage_xml(self):
4992        if mesonbuild.environment.detect_msys2_arch():
4993            raise unittest.SkipTest('Skipped due to problems with coverage on MSYS2')
4994        gcovr_exe, gcovr_new_rootdir = mesonbuild.environment.detect_gcovr()
4995        if not gcovr_exe:
4996            raise unittest.SkipTest('gcovr not found, or too old')
4997        testdir = os.path.join(self.common_test_dir, '1 trivial')
4998        env = get_fake_env(testdir, self.builddir, self.prefix)
4999        cc = env.detect_c_compiler(MachineChoice.HOST)
5000        if cc.get_id() == 'clang':
5001            if not mesonbuild.environment.detect_llvm_cov():
5002                raise unittest.SkipTest('llvm-cov not found')
5003        if cc.get_id() == 'msvc':
5004            raise unittest.SkipTest('Test only applies to non-MSVC compilers')
5005        self.init(testdir, extra_args=['-Db_coverage=true'])
5006        self.build()
5007        self.run_tests()
5008        self.run_target('coverage-xml')
5009        self._check_coverage_files(['xml'])
5010
5011    def test_cross_file_constants(self):
5012        with temp_filename() as crossfile1, temp_filename() as crossfile2:
5013            with open(crossfile1, 'w') as f:
5014                f.write(textwrap.dedent(
5015                    '''
5016                    [constants]
5017                    compiler = 'gcc'
5018                    '''))
5019            with open(crossfile2, 'w') as f:
5020                f.write(textwrap.dedent(
5021                    '''
5022                    [constants]
5023                    toolchain = '/toolchain/'
5024                    common_flags = ['--sysroot=' + toolchain / 'sysroot']
5025
5026                    [properties]
5027                    c_args = common_flags + ['-DSOMETHING']
5028                    cpp_args = c_args + ['-DSOMETHING_ELSE']
5029
5030                    [binaries]
5031                    c = toolchain / compiler
5032                    '''))
5033
5034            values = mesonbuild.coredata.parse_machine_files([crossfile1, crossfile2])
5035            self.assertEqual(values['binaries']['c'], '/toolchain/gcc')
5036            self.assertEqual(values['properties']['c_args'],
5037                             ['--sysroot=/toolchain/sysroot', '-DSOMETHING'])
5038            self.assertEqual(values['properties']['cpp_args'],
5039                             ['--sysroot=/toolchain/sysroot', '-DSOMETHING', '-DSOMETHING_ELSE'])
5040
5041    @unittest.skipIf(is_windows(), 'Directory cleanup fails for some reason')
5042    def test_wrap_git(self):
5043        with tempfile.TemporaryDirectory() as tmpdir:
5044            srcdir = os.path.join(tmpdir, 'src')
5045            shutil.copytree(os.path.join(self.unit_test_dir, '78 wrap-git'), srcdir)
5046            upstream = os.path.join(srcdir, 'subprojects', 'wrap_git_upstream')
5047            upstream_uri = Path(upstream).as_uri()
5048            _git_init(upstream)
5049            with open(os.path.join(srcdir, 'subprojects', 'wrap_git.wrap'), 'w') as f:
5050                f.write(textwrap.dedent('''
5051                  [wrap-git]
5052                  url = {}
5053                  patch_directory = wrap_git_builddef
5054                  revision = master
5055                '''.format(upstream_uri)))
5056            self.init(srcdir)
5057            self.build()
5058            self.run_tests()
5059
5060    def test_multi_output_custom_target_no_warning(self):
5061        testdir = os.path.join(self.common_test_dir, '235 custom_target source')
5062
5063        out = self.init(testdir)
5064        self.assertNotRegex(out, 'WARNING:.*Using the first one.')
5065        self.build()
5066        self.run_tests()
5067
5068class FailureTests(BasePlatformTests):
5069    '''
5070    Tests that test failure conditions. Build files here should be dynamically
5071    generated and static tests should go into `test cases/failing*`.
5072    This is useful because there can be many ways in which a particular
5073    function can fail, and creating failing tests for all of them is tedious
5074    and slows down testing.
5075    '''
5076    dnf = "[Dd]ependency.*not found(:.*)?"
5077    nopkg = '[Pp]kg-config.*not found'
5078
5079    def setUp(self):
5080        super().setUp()
5081        self.srcdir = os.path.realpath(tempfile.mkdtemp())
5082        self.mbuild = os.path.join(self.srcdir, 'meson.build')
5083        self.moptions = os.path.join(self.srcdir, 'meson_options.txt')
5084
5085    def tearDown(self):
5086        super().tearDown()
5087        windows_proof_rmtree(self.srcdir)
5088
5089    def assertMesonRaises(self, contents, match, *,
5090                          extra_args=None,
5091                          langs=None,
5092                          meson_version=None,
5093                          options=None,
5094                          override_envvars=None):
5095        '''
5096        Assert that running meson configure on the specified @contents raises
5097        a error message matching regex @match.
5098        '''
5099        if langs is None:
5100            langs = []
5101        with open(self.mbuild, 'w') as f:
5102            f.write("project('failure test', 'c', 'cpp'")
5103            if meson_version:
5104                f.write(", meson_version: '{}'".format(meson_version))
5105            f.write(")\n")
5106            for lang in langs:
5107                f.write("add_languages('{}', required : false)\n".format(lang))
5108            f.write(contents)
5109        if options is not None:
5110            with open(self.moptions, 'w') as f:
5111                f.write(options)
5112        o = {'MESON_FORCE_BACKTRACE': '1'}
5113        if override_envvars is None:
5114            override_envvars = o
5115        else:
5116            override_envvars.update(o)
5117        # Force tracebacks so we can detect them properly
5118        with self.assertRaisesRegex(MesonException, match, msg=contents):
5119            # Must run in-process or we'll get a generic CalledProcessError
5120            self.init(self.srcdir, extra_args=extra_args,
5121                      inprocess=True,
5122                      override_envvars = override_envvars)
5123
5124    def obtainMesonOutput(self, contents, match, extra_args, langs, meson_version=None):
5125        if langs is None:
5126            langs = []
5127        with open(self.mbuild, 'w') as f:
5128            f.write("project('output test', 'c', 'cpp'")
5129            if meson_version:
5130                f.write(", meson_version: '{}'".format(meson_version))
5131            f.write(")\n")
5132            for lang in langs:
5133                f.write("add_languages('{}', required : false)\n".format(lang))
5134            f.write(contents)
5135        # Run in-process for speed and consistency with assertMesonRaises
5136        return self.init(self.srcdir, extra_args=extra_args, inprocess=True)
5137
5138    def assertMesonOutputs(self, contents, match, extra_args=None, langs=None, meson_version=None):
5139        '''
5140        Assert that running meson configure on the specified @contents outputs
5141        something that matches regex @match.
5142        '''
5143        out = self.obtainMesonOutput(contents, match, extra_args, langs, meson_version)
5144        self.assertRegex(out, match)
5145
5146    def assertMesonDoesNotOutput(self, contents, match, extra_args=None, langs=None, meson_version=None):
5147        '''
5148        Assert that running meson configure on the specified @contents does not output
5149        something that matches regex @match.
5150        '''
5151        out = self.obtainMesonOutput(contents, match, extra_args, langs, meson_version)
5152        self.assertNotRegex(out, match)
5153
5154    @skipIfNoPkgconfig
5155    def test_dependency(self):
5156        if subprocess.call(['pkg-config', '--exists', 'zlib']) != 0:
5157            raise unittest.SkipTest('zlib not found with pkg-config')
5158        a = (("dependency('zlib', method : 'fail')", "'fail' is invalid"),
5159             ("dependency('zlib', static : '1')", "[Ss]tatic.*boolean"),
5160             ("dependency('zlib', version : 1)", "Item must be a list or one of <class 'str'>"),
5161             ("dependency('zlib', required : 1)", "[Rr]equired.*boolean"),
5162             ("dependency('zlib', method : 1)", "[Mm]ethod.*string"),
5163             ("dependency('zlibfail')", self.dnf),)
5164        for contents, match in a:
5165            self.assertMesonRaises(contents, match)
5166
5167    def test_apple_frameworks_dependency(self):
5168        if not is_osx():
5169            raise unittest.SkipTest('only run on macOS')
5170        self.assertMesonRaises("dependency('appleframeworks')",
5171                               "requires at least one module")
5172
5173    def test_extraframework_dependency_method(self):
5174        code = "dependency('python', method : 'extraframework')"
5175        if not is_osx():
5176            self.assertMesonRaises(code, self.dnf)
5177        else:
5178            # Python2 framework is always available on macOS
5179            self.assertMesonOutputs(code, '[Dd]ependency.*python.*found.*YES')
5180
5181    def test_sdl2_notfound_dependency(self):
5182        # Want to test failure, so skip if available
5183        if shutil.which('sdl2-config'):
5184            raise unittest.SkipTest('sdl2-config found')
5185        self.assertMesonRaises("dependency('sdl2', method : 'sdlconfig')", self.dnf)
5186        if shutil.which('pkg-config'):
5187            self.assertMesonRaises("dependency('sdl2', method : 'pkg-config')", self.dnf)
5188        with no_pkgconfig():
5189            # Look for pkg-config, cache it, then
5190            # Use cached pkg-config without erroring out, then
5191            # Use cached pkg-config to error out
5192            code = "dependency('foobarrr', method : 'pkg-config', required : false)\n" \
5193                "dependency('foobarrr2', method : 'pkg-config', required : false)\n" \
5194                "dependency('sdl2', method : 'pkg-config')"
5195            self.assertMesonRaises(code, self.nopkg)
5196
5197    def test_gnustep_notfound_dependency(self):
5198        # Want to test failure, so skip if available
5199        if shutil.which('gnustep-config'):
5200            raise unittest.SkipTest('gnustep-config found')
5201        self.assertMesonRaises("dependency('gnustep')",
5202                               "(requires a Objc compiler|{})".format(self.dnf),
5203                               langs = ['objc'])
5204
5205    def test_wx_notfound_dependency(self):
5206        # Want to test failure, so skip if available
5207        if shutil.which('wx-config-3.0') or shutil.which('wx-config') or shutil.which('wx-config-gtk3'):
5208            raise unittest.SkipTest('wx-config, wx-config-3.0 or wx-config-gtk3 found')
5209        self.assertMesonRaises("dependency('wxwidgets')", self.dnf)
5210        self.assertMesonOutputs("dependency('wxwidgets', required : false)",
5211                                "Run-time dependency .*WxWidgets.* found: .*NO.*")
5212
5213    def test_wx_dependency(self):
5214        if not shutil.which('wx-config-3.0') and not shutil.which('wx-config') and not shutil.which('wx-config-gtk3'):
5215            raise unittest.SkipTest('Neither wx-config, wx-config-3.0 nor wx-config-gtk3 found')
5216        self.assertMesonRaises("dependency('wxwidgets', modules : 1)",
5217                               "module argument is not a string")
5218
5219    def test_llvm_dependency(self):
5220        self.assertMesonRaises("dependency('llvm', modules : 'fail')",
5221                               "(required.*fail|{})".format(self.dnf))
5222
5223    def test_boost_notfound_dependency(self):
5224        # Can be run even if Boost is found or not
5225        self.assertMesonRaises("dependency('boost', modules : 1)",
5226                               "module.*not a string")
5227        self.assertMesonRaises("dependency('boost', modules : 'fail')",
5228                               "(fail.*not found|{})".format(self.dnf))
5229
5230    def test_boost_BOOST_ROOT_dependency(self):
5231        # Test BOOST_ROOT; can be run even if Boost is found or not
5232        self.assertMesonRaises("dependency('boost')",
5233                               "(BOOST_ROOT.*absolute|{})".format(self.dnf),
5234                               override_envvars = {'BOOST_ROOT': 'relative/path'})
5235
5236    def test_dependency_invalid_method(self):
5237        code = '''zlib_dep = dependency('zlib', required : false)
5238        zlib_dep.get_configtool_variable('foo')
5239        '''
5240        self.assertMesonRaises(code, ".* is not a config-tool dependency")
5241        code = '''zlib_dep = dependency('zlib', required : false)
5242        dep = declare_dependency(dependencies : zlib_dep)
5243        dep.get_pkgconfig_variable('foo')
5244        '''
5245        self.assertMesonRaises(code, "Method.*pkgconfig.*is invalid.*internal")
5246        code = '''zlib_dep = dependency('zlib', required : false)
5247        dep = declare_dependency(dependencies : zlib_dep)
5248        dep.get_configtool_variable('foo')
5249        '''
5250        self.assertMesonRaises(code, "Method.*configtool.*is invalid.*internal")
5251
5252    def test_objc_cpp_detection(self):
5253        '''
5254        Test that when we can't detect objc or objcpp, we fail gracefully.
5255        '''
5256        env = get_fake_env()
5257        try:
5258            env.detect_objc_compiler(MachineChoice.HOST)
5259            env.detect_objcpp_compiler(MachineChoice.HOST)
5260        except EnvironmentException:
5261            code = "add_languages('objc')\nadd_languages('objcpp')"
5262            self.assertMesonRaises(code, "Unknown compiler")
5263            return
5264        raise unittest.SkipTest("objc and objcpp found, can't test detection failure")
5265
5266    def test_subproject_variables(self):
5267        '''
5268        Test that:
5269        1. The correct message is outputted when a not-required dep is not
5270           found and the fallback subproject is also not found.
5271        2. A not-required fallback dependency is not found because the
5272           subproject failed to parse.
5273        3. A not-found not-required dep with a fallback subproject outputs the
5274           correct message when the fallback subproject is found but the
5275           variable inside it is not.
5276        4. A fallback dependency is found from the subproject parsed in (3)
5277        5. The correct message is outputted when the .wrap file is missing for
5278           a sub-subproject.
5279        '''
5280        tdir = os.path.join(self.unit_test_dir, '20 subproj dep variables')
5281        out = self.init(tdir, inprocess=True)
5282        self.assertRegex(out, r"Subproject directory not found and .*nosubproj.wrap.* file not found")
5283        self.assertRegex(out, r'Function does not take positional arguments.')
5284        self.assertRegex(out, r'WARNING:.* Dependency .*subsubproject.* not found but it is available in a sub-subproject.')
5285        self.assertRegex(out, r'Subproject directory not found and .*subsubproject.wrap.* file not found')
5286        self.assertRegex(out, r'Dependency .*zlibproxy.* from subproject .*subprojects.*somesubproj.* found: .*YES.*')
5287
5288    def test_exception_exit_status(self):
5289        '''
5290        Test exit status on python exception
5291        '''
5292        tdir = os.path.join(self.unit_test_dir, '21 exit status')
5293        with self.assertRaises(subprocess.CalledProcessError) as cm:
5294            self.init(tdir, inprocess=False, override_envvars = {'MESON_UNIT_TEST': '1'})
5295        self.assertEqual(cm.exception.returncode, 2)
5296        self.wipe()
5297
5298    def test_dict_requires_key_value_pairs(self):
5299        self.assertMesonRaises("dict = {3, 'foo': 'bar'}",
5300                               'Only key:value pairs are valid in dict construction.')
5301        self.assertMesonRaises("{'foo': 'bar', 3}",
5302                               'Only key:value pairs are valid in dict construction.')
5303
5304    def test_dict_forbids_duplicate_keys(self):
5305        self.assertMesonRaises("dict = {'a': 41, 'a': 42}",
5306                               'Duplicate dictionary key: a.*')
5307
5308    def test_dict_forbids_integer_key(self):
5309        self.assertMesonRaises("dict = {3: 'foo'}",
5310                               'Key must be a string.*')
5311
5312    def test_using_too_recent_feature(self):
5313        # Here we use a dict, which was introduced in 0.47.0
5314        self.assertMesonOutputs("dict = {}",
5315                                ".*WARNING.*Project targeting.*but.*",
5316                                meson_version='>= 0.46.0')
5317
5318    def test_using_recent_feature(self):
5319        # Same as above, except the meson version is now appropriate
5320        self.assertMesonDoesNotOutput("dict = {}",
5321                                      ".*WARNING.*Project targeting.*but.*",
5322                                      meson_version='>= 0.47')
5323
5324    def test_using_too_recent_feature_dependency(self):
5325        self.assertMesonOutputs("dependency('pcap', required: false)",
5326                                ".*WARNING.*Project targeting.*but.*",
5327                                meson_version='>= 0.41.0')
5328
5329    def test_vcs_tag_featurenew_build_always_stale(self):
5330        'https://github.com/mesonbuild/meson/issues/3904'
5331        vcs_tag = '''version_data = configuration_data()
5332        version_data.set('PROJVER', '@VCS_TAG@')
5333        vf = configure_file(output : 'version.h.in', configuration: version_data)
5334        f = vcs_tag(input : vf, output : 'version.h')
5335        '''
5336        msg = '.*WARNING:.*feature.*build_always_stale.*custom_target.*'
5337        self.assertMesonDoesNotOutput(vcs_tag, msg, meson_version='>=0.43')
5338
5339    def test_missing_subproject_not_required_and_required(self):
5340        self.assertMesonRaises("sub1 = subproject('not-found-subproject', required: false)\n" +
5341                               "sub2 = subproject('not-found-subproject', required: true)",
5342                               """.*Subproject "subprojects/not-found-subproject" required but not found.*""")
5343
5344    def test_get_variable_on_not_found_project(self):
5345        self.assertMesonRaises("sub1 = subproject('not-found-subproject', required: false)\n" +
5346                               "sub1.get_variable('naaa')",
5347                               """Subproject "subprojects/not-found-subproject" disabled can't get_variable on it.""")
5348
5349    def test_version_checked_before_parsing_options(self):
5350        '''
5351        https://github.com/mesonbuild/meson/issues/5281
5352        '''
5353        options = "option('some-option', type: 'foo', value: '')"
5354        match = 'Meson version is.*but project requires >=2000'
5355        self.assertMesonRaises("", match, meson_version='>=2000', options=options)
5356
5357    def test_assert_default_message(self):
5358        self.assertMesonRaises("k1 = 'a'\n" +
5359                               "assert({\n" +
5360                               "  k1: 1,\n" +
5361                               "}['a'] == 2)\n",
5362                               r"Assert failed: {k1 : 1}\['a'\] == 2")
5363
5364    def test_wrap_nofallback(self):
5365        self.assertMesonRaises("dependency('notfound', fallback : ['foo', 'foo_dep'])",
5366                               r"Dependency \'notfound\' not found and fallback is disabled",
5367                               extra_args=['--wrap-mode=nofallback'])
5368
5369    def test_message(self):
5370        self.assertMesonOutputs("message('Array:', ['a', 'b'])",
5371                                r"Message:.* Array: \['a', 'b'\]")
5372
5373    def test_warning(self):
5374        self.assertMesonOutputs("warning('Array:', ['a', 'b'])",
5375                                r"WARNING:.* Array: \['a', 'b'\]")
5376
5377    def test_override_dependency_twice(self):
5378        self.assertMesonRaises("meson.override_dependency('foo', declare_dependency())\n" +
5379                               "meson.override_dependency('foo', declare_dependency())",
5380                               """Tried to override dependency 'foo' which has already been resolved or overridden""")
5381
5382    @unittest.skipIf(is_windows(), 'zlib is not available on Windows')
5383    def test_override_resolved_dependency(self):
5384        self.assertMesonRaises("dependency('zlib')\n" +
5385                               "meson.override_dependency('zlib', declare_dependency())",
5386                               """Tried to override dependency 'zlib' which has already been resolved or overridden""")
5387
5388@unittest.skipUnless(is_windows() or is_cygwin(), "requires Windows (or Windows via Cygwin)")
5389class WindowsTests(BasePlatformTests):
5390    '''
5391    Tests that should run on Cygwin, MinGW, and MSVC
5392    '''
5393
5394    def setUp(self):
5395        super().setUp()
5396        self.platform_test_dir = os.path.join(self.src_root, 'test cases/windows')
5397
5398    @unittest.skipIf(is_cygwin(), 'Test only applicable to Windows')
5399    def test_find_program(self):
5400        '''
5401        Test that Windows-specific edge-cases in find_program are functioning
5402        correctly. Cannot be an ordinary test because it involves manipulating
5403        PATH to point to a directory with Python scripts.
5404        '''
5405        testdir = os.path.join(self.platform_test_dir, '8 find program')
5406        # Find `cmd` and `cmd.exe`
5407        prog1 = ExternalProgram('cmd')
5408        self.assertTrue(prog1.found(), msg='cmd not found')
5409        prog2 = ExternalProgram('cmd.exe')
5410        self.assertTrue(prog2.found(), msg='cmd.exe not found')
5411        self.assertPathEqual(prog1.get_path(), prog2.get_path())
5412        # Find cmd.exe with args without searching
5413        prog = ExternalProgram('cmd', command=['cmd', '/C'])
5414        self.assertTrue(prog.found(), msg='cmd not found with args')
5415        self.assertPathEqual(prog.get_command()[0], 'cmd')
5416        # Find cmd with an absolute path that's missing the extension
5417        cmd_path = prog2.get_path()[:-4]
5418        prog = ExternalProgram(cmd_path)
5419        self.assertTrue(prog.found(), msg='{!r} not found'.format(cmd_path))
5420        # Finding a script with no extension inside a directory works
5421        prog = ExternalProgram(os.path.join(testdir, 'test-script'))
5422        self.assertTrue(prog.found(), msg='test-script not found')
5423        # Finding a script with an extension inside a directory works
5424        prog = ExternalProgram(os.path.join(testdir, 'test-script-ext.py'))
5425        self.assertTrue(prog.found(), msg='test-script-ext.py not found')
5426        # Finding a script in PATH
5427        os.environ['PATH'] += os.pathsep + testdir
5428        # If `.PY` is in PATHEXT, scripts can be found as programs
5429        if '.PY' in [ext.upper() for ext in os.environ['PATHEXT'].split(';')]:
5430            # Finding a script in PATH w/o extension works and adds the interpreter
5431            prog = ExternalProgram('test-script-ext')
5432            self.assertTrue(prog.found(), msg='test-script-ext not found in PATH')
5433            self.assertPathEqual(prog.get_command()[0], python_command[0])
5434            self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py')
5435        # Finding a script in PATH with extension works and adds the interpreter
5436        prog = ExternalProgram('test-script-ext.py')
5437        self.assertTrue(prog.found(), msg='test-script-ext.py not found in PATH')
5438        self.assertPathEqual(prog.get_command()[0], python_command[0])
5439        self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py')
5440        # Using a script with an extension directly via command= works and adds the interpreter
5441        prog = ExternalProgram('test-script-ext.py', command=[os.path.join(testdir, 'test-script-ext.py'), '--help'])
5442        self.assertTrue(prog.found(), msg='test-script-ext.py with full path not picked up via command=')
5443        self.assertPathEqual(prog.get_command()[0], python_command[0])
5444        self.assertPathEqual(prog.get_command()[2], '--help')
5445        self.assertPathBasenameEqual(prog.get_path(), 'test-script-ext.py')
5446        # Using a script without an extension directly via command= works and adds the interpreter
5447        prog = ExternalProgram('test-script', command=[os.path.join(testdir, 'test-script'), '--help'])
5448        self.assertTrue(prog.found(), msg='test-script with full path not picked up via command=')
5449        self.assertPathEqual(prog.get_command()[0], python_command[0])
5450        self.assertPathEqual(prog.get_command()[2], '--help')
5451        self.assertPathBasenameEqual(prog.get_path(), 'test-script')
5452        # Ensure that WindowsApps gets removed from PATH
5453        path = os.environ['PATH']
5454        if 'WindowsApps' not in path:
5455            username = os.environ['USERNAME']
5456            appstore_dir = r'C:\Users\{}\AppData\Local\Microsoft\WindowsApps'.format(username)
5457            path = os.pathsep + appstore_dir
5458        path = ExternalProgram._windows_sanitize_path(path)
5459        self.assertNotIn('WindowsApps', path)
5460
5461    def test_ignore_libs(self):
5462        '''
5463        Test that find_library on libs that are to be ignored returns an empty
5464        array of arguments. Must be a unit test because we cannot inspect
5465        ExternalLibraryHolder from build files.
5466        '''
5467        testdir = os.path.join(self.platform_test_dir, '1 basic')
5468        env = get_fake_env(testdir, self.builddir, self.prefix)
5469        cc = env.detect_c_compiler(MachineChoice.HOST)
5470        if cc.get_argument_syntax() != 'msvc':
5471            raise unittest.SkipTest('Not using MSVC')
5472        # To force people to update this test, and also test
5473        self.assertEqual(set(cc.ignore_libs), {'c', 'm', 'pthread', 'dl', 'rt', 'execinfo'})
5474        for l in cc.ignore_libs:
5475            self.assertEqual(cc.find_library(l, env, []), [])
5476
5477    def test_rc_depends_files(self):
5478        testdir = os.path.join(self.platform_test_dir, '5 resources')
5479
5480        # resource compiler depfile generation is not yet implemented for msvc
5481        env = get_fake_env(testdir, self.builddir, self.prefix)
5482        depfile_works = env.detect_c_compiler(MachineChoice.HOST).get_id() not in {'msvc', 'clang-cl', 'intel-cl'}
5483
5484        self.init(testdir)
5485        self.build()
5486        # Immediately rebuilding should not do anything
5487        self.assertBuildIsNoop()
5488        # Test compile_resources(depend_file:)
5489        # Changing mtime of sample.ico should rebuild prog
5490        self.utime(os.path.join(testdir, 'res', 'sample.ico'))
5491        self.assertRebuiltTarget('prog')
5492        # Test depfile generation by compile_resources
5493        # Changing mtime of resource.h should rebuild myres.rc and then prog
5494        if depfile_works:
5495            self.utime(os.path.join(testdir, 'inc', 'resource', 'resource.h'))
5496            self.assertRebuiltTarget('prog')
5497        self.wipe()
5498
5499        if depfile_works:
5500            testdir = os.path.join(self.platform_test_dir, '12 resources with custom targets')
5501            self.init(testdir)
5502            self.build()
5503            # Immediately rebuilding should not do anything
5504            self.assertBuildIsNoop()
5505            # Changing mtime of resource.h should rebuild myres_1.rc and then prog_1
5506            self.utime(os.path.join(testdir, 'res', 'resource.h'))
5507            self.assertRebuiltTarget('prog_1')
5508
5509    def test_msvc_cpp17(self):
5510        testdir = os.path.join(self.unit_test_dir, '45 vscpp17')
5511
5512        env = get_fake_env(testdir, self.builddir, self.prefix)
5513        cc = env.detect_c_compiler(MachineChoice.HOST)
5514        if cc.get_argument_syntax() != 'msvc':
5515            raise unittest.SkipTest('Test only applies to MSVC-like compilers')
5516
5517        try:
5518            self.init(testdir)
5519        except subprocess.CalledProcessError:
5520            # According to Python docs, output is only stored when
5521            # using check_output. We don't use it, so we can't check
5522            # that the output is correct (i.e. that it failed due
5523            # to the right reason).
5524            return
5525        self.build()
5526
5527    def test_install_pdb_introspection(self):
5528        testdir = os.path.join(self.platform_test_dir, '1 basic')
5529
5530        env = get_fake_env(testdir, self.builddir, self.prefix)
5531        cc = env.detect_c_compiler(MachineChoice.HOST)
5532        if cc.get_argument_syntax() != 'msvc':
5533            raise unittest.SkipTest('Test only applies to MSVC-like compilers')
5534
5535        self.init(testdir)
5536        installed = self.introspect('--installed')
5537        files = [os.path.basename(path) for path in installed.values()]
5538
5539        self.assertTrue('prog.pdb' in files)
5540
5541    def _check_ld(self, name: str, lang: str, expected: str) -> None:
5542        if not shutil.which(name):
5543            raise unittest.SkipTest('Could not find {}.'.format(name))
5544        envvars = [mesonbuild.envconfig.BinaryTable.evarMap['{}_ld'.format(lang)]]
5545
5546        # Also test a deprecated variable if there is one.
5547        if envvars[0] in mesonbuild.envconfig.BinaryTable.DEPRECATION_MAP:
5548            envvars.append(
5549                mesonbuild.envconfig.BinaryTable.DEPRECATION_MAP[envvars[0]])
5550
5551        for envvar in envvars:
5552            with mock.patch.dict(os.environ, {envvar: name}):
5553                env = get_fake_env()
5554                try:
5555                    comp = getattr(env, 'detect_{}_compiler'.format(lang))(MachineChoice.HOST)
5556                except EnvironmentException:
5557                    raise unittest.SkipTest('Could not find a compiler for {}'.format(lang))
5558                self.assertEqual(comp.linker.id, expected)
5559
5560    def test_link_environment_variable_lld_link(self):
5561        env = get_fake_env()
5562        comp = getattr(env, 'detect_c_compiler')(MachineChoice.HOST)
5563        if isinstance(comp, mesonbuild.compilers.GnuLikeCompiler):
5564            raise unittest.SkipTest('GCC cannot be used with link compatible linkers.')
5565        self._check_ld('lld-link', 'c', 'lld-link')
5566
5567    def test_link_environment_variable_link(self):
5568        env = get_fake_env()
5569        comp = getattr(env, 'detect_c_compiler')(MachineChoice.HOST)
5570        if isinstance(comp, mesonbuild.compilers.GnuLikeCompiler):
5571            raise unittest.SkipTest('GCC cannot be used with link compatible linkers.')
5572        self._check_ld('link', 'c', 'link')
5573
5574    def test_link_environment_variable_optlink(self):
5575        env = get_fake_env()
5576        comp = getattr(env, 'detect_c_compiler')(MachineChoice.HOST)
5577        if isinstance(comp, mesonbuild.compilers.GnuLikeCompiler):
5578            raise unittest.SkipTest('GCC cannot be used with link compatible linkers.')
5579        self._check_ld('optlink', 'c', 'optlink')
5580
5581    @skip_if_not_language('rust')
5582    def test_link_environment_variable_rust(self):
5583        self._check_ld('link', 'rust', 'link')
5584
5585    @skip_if_not_language('d')
5586    def test_link_environment_variable_d(self):
5587        env = get_fake_env()
5588        comp = getattr(env, 'detect_d_compiler')(MachineChoice.HOST)
5589        if comp.id == 'dmd':
5590            raise unittest.SkipTest('meson cannot reliably make DMD use a different linker.')
5591        self._check_ld('lld-link', 'd', 'lld-link')
5592
5593    def test_pefile_checksum(self):
5594        try:
5595            import pefile
5596        except ImportError:
5597            if is_ci():
5598                raise
5599            raise unittest.SkipTest('pefile module not found')
5600        testdir = os.path.join(self.common_test_dir, '6 linkshared')
5601        self.init(testdir, extra_args=['--buildtype=release'])
5602        self.build()
5603        # Test that binaries have a non-zero checksum
5604        env = get_fake_env()
5605        cc = env.detect_c_compiler(MachineChoice.HOST)
5606        cc_id = cc.get_id()
5607        ld_id = cc.get_linker_id()
5608        dll = glob(os.path.join(self.builddir, '*mycpplib.dll'))[0]
5609        exe = os.path.join(self.builddir, 'cppprog.exe')
5610        for f in (dll, exe):
5611            pe = pefile.PE(f)
5612            msg = 'PE file: {!r}, compiler: {!r}, linker: {!r}'.format(f, cc_id, ld_id)
5613            if cc_id == 'clang-cl':
5614                # Latest clang-cl tested (7.0) does not write checksums out
5615                self.assertFalse(pe.verify_checksum(), msg=msg)
5616            else:
5617                # Verify that a valid checksum was written by all other compilers
5618                self.assertTrue(pe.verify_checksum(), msg=msg)
5619
5620    def test_qt5dependency_vscrt(self):
5621        '''
5622        Test that qt5 dependencies use the debug module suffix when b_vscrt is
5623        set to 'mdd'
5624        '''
5625        # Verify that the `b_vscrt` option is available
5626        env = get_fake_env()
5627        cc = env.detect_c_compiler(MachineChoice.HOST)
5628        if 'b_vscrt' not in cc.base_options:
5629            raise unittest.SkipTest('Compiler does not support setting the VS CRT')
5630        # Verify that qmake is for Qt5
5631        if not shutil.which('qmake-qt5'):
5632            if not shutil.which('qmake') and not is_ci():
5633                raise unittest.SkipTest('QMake not found')
5634            output = subprocess.getoutput('qmake --version')
5635            if 'Qt version 5' not in output and not is_ci():
5636                raise unittest.SkipTest('Qmake found, but it is not for Qt 5.')
5637        # Setup with /MDd
5638        testdir = os.path.join(self.framework_test_dir, '4 qt')
5639        self.init(testdir, extra_args=['-Db_vscrt=mdd'])
5640        # Verify that we're linking to the debug versions of Qt DLLs
5641        build_ninja = os.path.join(self.builddir, 'build.ninja')
5642        with open(build_ninja, 'r', encoding='utf-8') as f:
5643            contents = f.read()
5644            m = re.search('build qt5core.exe: cpp_LINKER.*Qt5Cored.lib', contents)
5645        self.assertIsNotNone(m, msg=contents)
5646
5647    def test_compiler_checks_vscrt(self):
5648        '''
5649        Test that the correct VS CRT is used when running compiler checks
5650        '''
5651        # Verify that the `b_vscrt` option is available
5652        env = get_fake_env()
5653        cc = env.detect_c_compiler(MachineChoice.HOST)
5654        if 'b_vscrt' not in cc.base_options:
5655            raise unittest.SkipTest('Compiler does not support setting the VS CRT')
5656
5657        def sanitycheck_vscrt(vscrt):
5658            checks = self.get_meson_log_sanitychecks()
5659            self.assertTrue(len(checks) > 0)
5660            for check in checks:
5661                self.assertIn(vscrt, check)
5662
5663        testdir = os.path.join(self.common_test_dir, '1 trivial')
5664        self.init(testdir)
5665        sanitycheck_vscrt('/MDd')
5666
5667        self.new_builddir()
5668        self.init(testdir, extra_args=['-Dbuildtype=debugoptimized'])
5669        sanitycheck_vscrt('/MD')
5670
5671        self.new_builddir()
5672        self.init(testdir, extra_args=['-Dbuildtype=release'])
5673        sanitycheck_vscrt('/MD')
5674
5675        self.new_builddir()
5676        self.init(testdir, extra_args=['-Db_vscrt=md'])
5677        sanitycheck_vscrt('/MD')
5678
5679        self.new_builddir()
5680        self.init(testdir, extra_args=['-Db_vscrt=mdd'])
5681        sanitycheck_vscrt('/MDd')
5682
5683        self.new_builddir()
5684        self.init(testdir, extra_args=['-Db_vscrt=mt'])
5685        sanitycheck_vscrt('/MT')
5686
5687        self.new_builddir()
5688        self.init(testdir, extra_args=['-Db_vscrt=mtd'])
5689        sanitycheck_vscrt('/MTd')
5690
5691
5692@unittest.skipUnless(is_osx(), "requires Darwin")
5693class DarwinTests(BasePlatformTests):
5694    '''
5695    Tests that should run on macOS
5696    '''
5697
5698    def setUp(self):
5699        super().setUp()
5700        self.platform_test_dir = os.path.join(self.src_root, 'test cases/osx')
5701
5702    def test_apple_bitcode(self):
5703        '''
5704        Test that -fembed-bitcode is correctly added while compiling and
5705        -bitcode_bundle is added while linking when b_bitcode is true and not
5706        when it is false.  This can't be an ordinary test case because we need
5707        to inspect the compiler database.
5708        '''
5709        testdir = os.path.join(self.platform_test_dir, '7 bitcode')
5710        env = get_fake_env(testdir, self.builddir, self.prefix)
5711        cc = env.detect_c_compiler(MachineChoice.HOST)
5712        if cc.id != 'clang':
5713            raise unittest.SkipTest('Not using Clang on OSX')
5714        # Try with bitcode enabled
5715        out = self.init(testdir, extra_args='-Db_bitcode=true')
5716        # Warning was printed
5717        self.assertRegex(out, 'WARNING:.*b_bitcode')
5718        # Compiler options were added
5719        for compdb in self.get_compdb():
5720            if 'module' in compdb['file']:
5721                self.assertNotIn('-fembed-bitcode', compdb['command'])
5722            else:
5723                self.assertIn('-fembed-bitcode', compdb['command'])
5724        build_ninja = os.path.join(self.builddir, 'build.ninja')
5725        # Linker options were added
5726        with open(build_ninja, 'r', encoding='utf-8') as f:
5727            contents = f.read()
5728            m = re.search('LINK_ARGS =.*-bitcode_bundle', contents)
5729        self.assertIsNotNone(m, msg=contents)
5730        # Try with bitcode disabled
5731        self.setconf('-Db_bitcode=false')
5732        # Regenerate build
5733        self.build()
5734        for compdb in self.get_compdb():
5735            self.assertNotIn('-fembed-bitcode', compdb['command'])
5736        build_ninja = os.path.join(self.builddir, 'build.ninja')
5737        with open(build_ninja, 'r', encoding='utf-8') as f:
5738            contents = f.read()
5739            m = re.search('LINK_ARGS =.*-bitcode_bundle', contents)
5740        self.assertIsNone(m, msg=contents)
5741
5742    def test_apple_bitcode_modules(self):
5743        '''
5744        Same as above, just for shared_module()
5745        '''
5746        testdir = os.path.join(self.common_test_dir, '152 shared module resolving symbol in executable')
5747        # Ensure that it builds even with bitcode enabled
5748        self.init(testdir, extra_args='-Db_bitcode=true')
5749        self.build()
5750        self.run_tests()
5751
5752    def _get_darwin_versions(self, fname):
5753        fname = os.path.join(self.builddir, fname)
5754        out = subprocess.check_output(['otool', '-L', fname], universal_newlines=True)
5755        m = re.match(r'.*version (.*), current version (.*)\)', out.split('\n')[1])
5756        self.assertIsNotNone(m, msg=out)
5757        return m.groups()
5758
5759    @skipIfNoPkgconfig
5760    def test_library_versioning(self):
5761        '''
5762        Ensure that compatibility_version and current_version are set correctly
5763        '''
5764        testdir = os.path.join(self.platform_test_dir, '2 library versions')
5765        self.init(testdir)
5766        self.build()
5767        targets = {}
5768        for t in self.introspect('--targets'):
5769            targets[t['name']] = t['filename'][0] if isinstance(t['filename'], list) else t['filename']
5770        self.assertEqual(self._get_darwin_versions(targets['some']), ('7.0.0', '7.0.0'))
5771        self.assertEqual(self._get_darwin_versions(targets['noversion']), ('0.0.0', '0.0.0'))
5772        self.assertEqual(self._get_darwin_versions(targets['onlyversion']), ('1.0.0', '1.0.0'))
5773        self.assertEqual(self._get_darwin_versions(targets['onlysoversion']), ('5.0.0', '5.0.0'))
5774        self.assertEqual(self._get_darwin_versions(targets['intver']), ('2.0.0', '2.0.0'))
5775        self.assertEqual(self._get_darwin_versions(targets['stringver']), ('2.3.0', '2.3.0'))
5776        self.assertEqual(self._get_darwin_versions(targets['stringlistver']), ('2.4.0', '2.4.0'))
5777        self.assertEqual(self._get_darwin_versions(targets['intstringver']), ('1111.0.0', '2.5.0'))
5778        self.assertEqual(self._get_darwin_versions(targets['stringlistvers']), ('2.6.0', '2.6.1'))
5779
5780    def test_duplicate_rpath(self):
5781        testdir = os.path.join(self.unit_test_dir, '10 build_rpath')
5782        # We purposely pass a duplicate rpath to Meson, in order
5783        # to ascertain that Meson does not call install_name_tool
5784        # with duplicate -delete_rpath arguments, which would
5785        # lead to erroring out on installation
5786        env = {"LDFLAGS": "-Wl,-rpath,/foo/bar"}
5787        self.init(testdir, override_envvars=env)
5788        self.build()
5789        self.install()
5790
5791    def test_removing_unused_linker_args(self):
5792        testdir = os.path.join(self.common_test_dir, '108 has arg')
5793        env = {'CFLAGS': '-L/tmp -L /var/tmp -headerpad_max_install_names -Wl,-export_dynamic -framework Foundation'}
5794        self.init(testdir, override_envvars=env)
5795
5796
5797@unittest.skipUnless(not is_windows(), "requires something Unix-like")
5798class LinuxlikeTests(BasePlatformTests):
5799    '''
5800    Tests that should run on Linux, macOS, and *BSD
5801    '''
5802
5803    def test_basic_soname(self):
5804        '''
5805        Test that the soname is set correctly for shared libraries. This can't
5806        be an ordinary test case because we need to run `readelf` and actually
5807        check the soname.
5808        https://github.com/mesonbuild/meson/issues/785
5809        '''
5810        testdir = os.path.join(self.common_test_dir, '4 shared')
5811        self.init(testdir)
5812        self.build()
5813        lib1 = os.path.join(self.builddir, 'libmylib.so')
5814        soname = get_soname(lib1)
5815        self.assertEqual(soname, 'libmylib.so')
5816
5817    def test_custom_soname(self):
5818        '''
5819        Test that the soname is set correctly for shared libraries when
5820        a custom prefix and/or suffix is used. This can't be an ordinary test
5821        case because we need to run `readelf` and actually check the soname.
5822        https://github.com/mesonbuild/meson/issues/785
5823        '''
5824        testdir = os.path.join(self.common_test_dir, '25 library versions')
5825        self.init(testdir)
5826        self.build()
5827        lib1 = os.path.join(self.builddir, 'prefixsomelib.suffix')
5828        soname = get_soname(lib1)
5829        self.assertEqual(soname, 'prefixsomelib.suffix')
5830
5831    def test_pic(self):
5832        '''
5833        Test that -fPIC is correctly added to static libraries when b_staticpic
5834        is true and not when it is false. This can't be an ordinary test case
5835        because we need to inspect the compiler database.
5836        '''
5837        if is_windows() or is_cygwin() or is_osx():
5838            raise unittest.SkipTest('PIC not relevant')
5839
5840        testdir = os.path.join(self.common_test_dir, '3 static')
5841        self.init(testdir)
5842        compdb = self.get_compdb()
5843        self.assertIn('-fPIC', compdb[0]['command'])
5844        self.setconf('-Db_staticpic=false')
5845        # Regenerate build
5846        self.build()
5847        compdb = self.get_compdb()
5848        self.assertNotIn('-fPIC', compdb[0]['command'])
5849
5850    def test_pkgconfig_gen(self):
5851        '''
5852        Test that generated pkg-config files can be found and have the correct
5853        version and link args. This can't be an ordinary test case because we
5854        need to run pkg-config outside of a Meson build file.
5855        https://github.com/mesonbuild/meson/issues/889
5856        '''
5857        testdir = os.path.join(self.common_test_dir, '47 pkgconfig-gen')
5858        self.init(testdir)
5859        env = get_fake_env(testdir, self.builddir, self.prefix)
5860        kwargs = {'required': True, 'silent': True}
5861        os.environ['PKG_CONFIG_LIBDIR'] = self.privatedir
5862        foo_dep = PkgConfigDependency('libfoo', env, kwargs)
5863        self.assertTrue(foo_dep.found())
5864        self.assertEqual(foo_dep.get_version(), '1.0')
5865        self.assertIn('-lfoo', foo_dep.get_link_args())
5866        self.assertEqual(foo_dep.get_pkgconfig_variable('foo', {}), 'bar')
5867        self.assertPathEqual(foo_dep.get_pkgconfig_variable('datadir', {}), '/usr/data')
5868
5869        libhello_nolib = PkgConfigDependency('libhello_nolib', env, kwargs)
5870        self.assertTrue(libhello_nolib.found())
5871        self.assertEqual(libhello_nolib.get_link_args(), [])
5872        self.assertEqual(libhello_nolib.get_compile_args(), [])
5873
5874    def test_pkgconfig_gen_deps(self):
5875        '''
5876        Test that generated pkg-config files correctly handle dependencies
5877        '''
5878        testdir = os.path.join(self.common_test_dir, '47 pkgconfig-gen')
5879        self.init(testdir)
5880        privatedir1 = self.privatedir
5881
5882        self.new_builddir()
5883        testdir = os.path.join(self.common_test_dir, '47 pkgconfig-gen', 'dependencies')
5884        self.init(testdir, override_envvars={'PKG_CONFIG_LIBDIR': privatedir1})
5885        privatedir2 = self.privatedir
5886
5887        os.environ
5888        env = {
5889            'PKG_CONFIG_LIBDIR': os.pathsep.join([privatedir1, privatedir2]),
5890            'PKG_CONFIG_SYSTEM_LIBRARY_PATH': '/usr/lib',
5891        }
5892        self._run(['pkg-config', 'dependency-test', '--validate'], override_envvars=env)
5893
5894        # pkg-config strips some duplicated flags so we have to parse the
5895        # generated file ourself.
5896        expected = {
5897            'Requires': 'libexposed',
5898            'Requires.private': 'libfoo >= 1.0',
5899            'Libs': '-L${libdir} -llibmain -pthread -lcustom',
5900            'Libs.private': '-lcustom2 -L${libdir} -llibinternal',
5901            'Cflags': '-I${includedir} -pthread -DCUSTOM',
5902        }
5903        if is_osx() or is_haiku():
5904            expected['Cflags'] = expected['Cflags'].replace('-pthread ', '')
5905        with open(os.path.join(privatedir2, 'dependency-test.pc')) as f:
5906            matched_lines = 0
5907            for line in f:
5908                parts = line.split(':', 1)
5909                if parts[0] in expected:
5910                    key = parts[0]
5911                    val = parts[1].strip()
5912                    expected_val = expected[key]
5913                    self.assertEqual(expected_val, val)
5914                    matched_lines += 1
5915            self.assertEqual(len(expected), matched_lines)
5916
5917        cmd = ['pkg-config', 'requires-test']
5918        out = self._run(cmd + ['--print-requires'], override_envvars=env).strip().split('\n')
5919        if not is_openbsd():
5920            self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo >= 1.0', 'libhello']))
5921        else:
5922            self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo>=1.0', 'libhello']))
5923
5924        cmd = ['pkg-config', 'requires-private-test']
5925        out = self._run(cmd + ['--print-requires-private'], override_envvars=env).strip().split('\n')
5926        if not is_openbsd():
5927            self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo >= 1.0', 'libhello']))
5928        else:
5929            self.assertEqual(sorted(out), sorted(['libexposed', 'libfoo>=1.0', 'libhello']))
5930
5931        cmd = ['pkg-config', 'pub-lib-order']
5932        out = self._run(cmd + ['--libs'], override_envvars=env).strip().split()
5933        self.assertEqual(out, ['-llibmain2', '-llibinternal'])
5934
5935    def test_pkgconfig_uninstalled(self):
5936        testdir = os.path.join(self.common_test_dir, '47 pkgconfig-gen')
5937        self.init(testdir)
5938        self.build()
5939
5940        os.environ['PKG_CONFIG_LIBDIR'] = os.path.join(self.builddir, 'meson-uninstalled')
5941        if is_cygwin():
5942            os.environ['PATH'] += os.pathsep + self.builddir
5943
5944        self.new_builddir()
5945        testdir = os.path.join(self.common_test_dir, '47 pkgconfig-gen', 'dependencies')
5946        self.init(testdir)
5947        self.build()
5948        self.run_tests()
5949
5950    def test_pkg_unfound(self):
5951        testdir = os.path.join(self.unit_test_dir, '23 unfound pkgconfig')
5952        self.init(testdir)
5953        with open(os.path.join(self.privatedir, 'somename.pc')) as f:
5954            pcfile = f.read()
5955        self.assertFalse('blub_blob_blib' in pcfile)
5956
5957    def test_vala_c_warnings(self):
5958        '''
5959        Test that no warnings are emitted for C code generated by Vala. This
5960        can't be an ordinary test case because we need to inspect the compiler
5961        database.
5962        https://github.com/mesonbuild/meson/issues/864
5963        '''
5964        if not shutil.which('valac'):
5965            raise unittest.SkipTest('valac not installed.')
5966        testdir = os.path.join(self.vala_test_dir, '5 target glib')
5967        self.init(testdir)
5968        compdb = self.get_compdb()
5969        vala_command = None
5970        c_command = None
5971        for each in compdb:
5972            if each['file'].endswith('GLib.Thread.c'):
5973                vala_command = each['command']
5974            elif each['file'].endswith('GLib.Thread.vala'):
5975                continue
5976            elif each['file'].endswith('retcode.c'):
5977                c_command = each['command']
5978            else:
5979                m = 'Unknown file {!r} in vala_c_warnings test'.format(each['file'])
5980                raise AssertionError(m)
5981        self.assertIsNotNone(vala_command)
5982        self.assertIsNotNone(c_command)
5983        # -w suppresses all warnings, should be there in Vala but not in C
5984        self.assertIn(" -w ", vala_command)
5985        self.assertNotIn(" -w ", c_command)
5986        # -Wall enables all warnings, should be there in C but not in Vala
5987        self.assertNotIn(" -Wall ", vala_command)
5988        self.assertIn(" -Wall ", c_command)
5989        # -Werror converts warnings to errors, should always be there since it's
5990        # injected by an unrelated piece of code and the project has werror=true
5991        self.assertIn(" -Werror ", vala_command)
5992        self.assertIn(" -Werror ", c_command)
5993
5994    @skipIfNoPkgconfig
5995    def test_qtdependency_pkgconfig_detection(self):
5996        '''
5997        Test that qt4 and qt5 detection with pkgconfig works.
5998        '''
5999        # Verify Qt4 or Qt5 can be found with pkg-config
6000        qt4 = subprocess.call(['pkg-config', '--exists', 'QtCore'])
6001        qt5 = subprocess.call(['pkg-config', '--exists', 'Qt5Core'])
6002        testdir = os.path.join(self.framework_test_dir, '4 qt')
6003        self.init(testdir, extra_args=['-Dmethod=pkg-config'])
6004        # Confirm that the dependency was found with pkg-config
6005        mesonlog = self.get_meson_log()
6006        if qt4 == 0:
6007            self.assertRegex('\n'.join(mesonlog),
6008                             r'Run-time dependency qt4 \(modules: Core\) found: YES 4.* \(pkg-config\)\n')
6009        if qt5 == 0:
6010            self.assertRegex('\n'.join(mesonlog),
6011                             r'Run-time dependency qt5 \(modules: Core\) found: YES 5.* \(pkg-config\)\n')
6012
6013    @skip_if_not_base_option('b_sanitize')
6014    def test_generate_gir_with_address_sanitizer(self):
6015        if is_cygwin():
6016            raise unittest.SkipTest('asan not available on Cygwin')
6017        if is_openbsd():
6018            raise unittest.SkipTest('-fsanitize=address is not supported on OpenBSD')
6019
6020        testdir = os.path.join(self.framework_test_dir, '7 gnome')
6021        self.init(testdir, extra_args=['-Db_sanitize=address', '-Db_lundef=false'])
6022        self.build()
6023
6024    def test_qt5dependency_qmake_detection(self):
6025        '''
6026        Test that qt5 detection with qmake works. This can't be an ordinary
6027        test case because it involves setting the environment.
6028        '''
6029        # Verify that qmake is for Qt5
6030        if not shutil.which('qmake-qt5'):
6031            if not shutil.which('qmake'):
6032                raise unittest.SkipTest('QMake not found')
6033            output = subprocess.getoutput('qmake --version')
6034            if 'Qt version 5' not in output:
6035                raise unittest.SkipTest('Qmake found, but it is not for Qt 5.')
6036        # Disable pkg-config codepath and force searching with qmake/qmake-qt5
6037        testdir = os.path.join(self.framework_test_dir, '4 qt')
6038        self.init(testdir, extra_args=['-Dmethod=qmake'])
6039        # Confirm that the dependency was found with qmake
6040        mesonlog = self.get_meson_log()
6041        self.assertRegex('\n'.join(mesonlog),
6042                         r'Run-time dependency qt5 \(modules: Core\) found: YES .* \((qmake|qmake-qt5)\)\n')
6043
6044    def glob_sofiles_without_privdir(self, g):
6045        files = glob(g)
6046        return [f for f in files if not f.endswith('.p')]
6047
6048    def _test_soname_impl(self, libpath, install):
6049        if is_cygwin() or is_osx():
6050            raise unittest.SkipTest('Test only applicable to ELF and linuxlike sonames')
6051
6052        testdir = os.path.join(self.unit_test_dir, '1 soname')
6053        self.init(testdir)
6054        self.build()
6055        if install:
6056            self.install()
6057
6058        # File without aliases set.
6059        nover = os.path.join(libpath, 'libnover.so')
6060        self.assertPathExists(nover)
6061        self.assertFalse(os.path.islink(nover))
6062        self.assertEqual(get_soname(nover), 'libnover.so')
6063        self.assertEqual(len(self.glob_sofiles_without_privdir(nover[:-3] + '*')), 1)
6064
6065        # File with version set
6066        verset = os.path.join(libpath, 'libverset.so')
6067        self.assertPathExists(verset + '.4.5.6')
6068        self.assertEqual(os.readlink(verset), 'libverset.so.4')
6069        self.assertEqual(get_soname(verset), 'libverset.so.4')
6070        self.assertEqual(len(self.glob_sofiles_without_privdir(verset[:-3] + '*')), 3)
6071
6072        # File with soversion set
6073        soverset = os.path.join(libpath, 'libsoverset.so')
6074        self.assertPathExists(soverset + '.1.2.3')
6075        self.assertEqual(os.readlink(soverset), 'libsoverset.so.1.2.3')
6076        self.assertEqual(get_soname(soverset), 'libsoverset.so.1.2.3')
6077        self.assertEqual(len(self.glob_sofiles_without_privdir(soverset[:-3] + '*')), 2)
6078
6079        # File with version and soversion set to same values
6080        settosame = os.path.join(libpath, 'libsettosame.so')
6081        self.assertPathExists(settosame + '.7.8.9')
6082        self.assertEqual(os.readlink(settosame), 'libsettosame.so.7.8.9')
6083        self.assertEqual(get_soname(settosame), 'libsettosame.so.7.8.9')
6084        self.assertEqual(len(self.glob_sofiles_without_privdir(settosame[:-3] + '*')), 2)
6085
6086        # File with version and soversion set to different values
6087        bothset = os.path.join(libpath, 'libbothset.so')
6088        self.assertPathExists(bothset + '.1.2.3')
6089        self.assertEqual(os.readlink(bothset), 'libbothset.so.1.2.3')
6090        self.assertEqual(os.readlink(bothset + '.1.2.3'), 'libbothset.so.4.5.6')
6091        self.assertEqual(get_soname(bothset), 'libbothset.so.1.2.3')
6092        self.assertEqual(len(self.glob_sofiles_without_privdir(bothset[:-3] + '*')), 3)
6093
6094    def test_soname(self):
6095        self._test_soname_impl(self.builddir, False)
6096
6097    def test_installed_soname(self):
6098        libdir = self.installdir + os.path.join(self.prefix, self.libdir)
6099        self._test_soname_impl(libdir, True)
6100
6101    def test_compiler_check_flags_order(self):
6102        '''
6103        Test that compiler check flags override all other flags. This can't be
6104        an ordinary test case because it needs the environment to be set.
6105        '''
6106        testdir = os.path.join(self.common_test_dir, '39 has function')
6107        env = get_fake_env(testdir, self.builddir, self.prefix)
6108        cpp = env.detect_cpp_compiler(MachineChoice.HOST)
6109        Oflag = '-O3'
6110        OflagCPP = Oflag
6111        if cpp.get_id() in ('clang', 'gcc'):
6112            # prevent developers from adding "int main(int argc, char **argv)"
6113            # to small Meson checks unless these parameters are actually used
6114            OflagCPP += ' -Werror=unused-parameter'
6115        env = {'CFLAGS': Oflag,
6116               'CXXFLAGS': OflagCPP}
6117        self.init(testdir, override_envvars=env)
6118        cmds = self.get_meson_log_compiler_checks()
6119        for cmd in cmds:
6120            if cmd[0] == 'ccache':
6121                cmd = cmd[1:]
6122            # Verify that -I flags from the `args` kwarg are first
6123            # This is set in the '39 has function' test case
6124            self.assertEqual(cmd[1], '-I/tmp')
6125            # Verify that -O3 set via the environment is overridden by -O0
6126            Oargs = [arg for arg in cmd if arg.startswith('-O')]
6127            self.assertEqual(Oargs, [Oflag, '-O0'])
6128
6129    def _test_stds_impl(self, testdir, compiler, p: str):
6130        has_cpp17 = (compiler.get_id() not in {'clang', 'gcc'} or
6131                     compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=5.0.0', '>=9.1') or
6132                     compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=5.0.0'))
6133        has_cpp2a_c17 = (compiler.get_id() not in {'clang', 'gcc'} or
6134                         compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=6.0.0', '>=10.0') or
6135                         compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=8.0.0'))
6136        has_c18 = (compiler.get_id() not in {'clang', 'gcc'} or
6137                   compiler.get_id() == 'clang' and _clang_at_least(compiler, '>=8.0.0', '>=11.0') or
6138                   compiler.get_id() == 'gcc' and version_compare(compiler.version, '>=8.0.0'))
6139        # Check that all the listed -std=xxx options for this compiler work just fine when used
6140        # https://en.wikipedia.org/wiki/Xcode#Latest_versions
6141        # https://www.gnu.org/software/gcc/projects/cxx-status.html
6142        for v in compiler.get_options()['std'].choices:
6143            lang_std = p + '_std'
6144            # we do it like this to handle gnu++17,c++17 and gnu17,c17 cleanly
6145            # thus, C++ first
6146            if '++17' in v and not has_cpp17:
6147                continue
6148            elif '++2a' in v and not has_cpp2a_c17:  # https://en.cppreference.com/w/cpp/compiler_support
6149                continue
6150            # now C
6151            elif '17' in v and not has_cpp2a_c17:
6152                continue
6153            elif '18' in v and not has_c18:
6154                continue
6155            std_opt = '{}={}'.format(lang_std, v)
6156            self.init(testdir, extra_args=['-D' + std_opt])
6157            cmd = self.get_compdb()[0]['command']
6158            # c++03 and gnu++03 are not understood by ICC, don't try to look for them
6159            skiplist = frozenset([
6160                ('intel', 'c++03'),
6161                ('intel', 'gnu++03')])
6162            if v != 'none' and not (compiler.get_id(), v) in skiplist:
6163                cmd_std = " -std={} ".format(v)
6164                self.assertIn(cmd_std, cmd)
6165            try:
6166                self.build()
6167            except Exception:
6168                print('{} was {!r}'.format(lang_std, v))
6169                raise
6170            self.wipe()
6171        # Check that an invalid std option in CFLAGS/CPPFLAGS fails
6172        # Needed because by default ICC ignores invalid options
6173        cmd_std = '-std=FAIL'
6174        if p == 'c':
6175            env_flag_name = 'CFLAGS'
6176        elif p == 'cpp':
6177            env_flag_name = 'CXXFLAGS'
6178        else:
6179            raise NotImplementedError('Language {} not defined.'.format(p))
6180        env = {}
6181        env[env_flag_name] = cmd_std
6182        with self.assertRaises((subprocess.CalledProcessError, mesonbuild.mesonlib.EnvironmentException),
6183                               msg='C compiler should have failed with -std=FAIL'):
6184            self.init(testdir, override_envvars = env)
6185            # ICC won't fail in the above because additional flags are needed to
6186            # make unknown -std=... options errors.
6187            self.build()
6188
6189    def test_compiler_c_stds(self):
6190        '''
6191        Test that C stds specified for this compiler can all be used. Can't be
6192        an ordinary test because it requires passing options to meson.
6193        '''
6194        testdir = os.path.join(self.common_test_dir, '1 trivial')
6195        env = get_fake_env(testdir, self.builddir, self.prefix)
6196        cc = env.detect_c_compiler(MachineChoice.HOST)
6197        self._test_stds_impl(testdir, cc, 'c')
6198
6199    def test_compiler_cpp_stds(self):
6200        '''
6201        Test that C++ stds specified for this compiler can all be used. Can't
6202        be an ordinary test because it requires passing options to meson.
6203        '''
6204        testdir = os.path.join(self.common_test_dir, '2 cpp')
6205        env = get_fake_env(testdir, self.builddir, self.prefix)
6206        cpp = env.detect_cpp_compiler(MachineChoice.HOST)
6207        self._test_stds_impl(testdir, cpp, 'cpp')
6208
6209    def test_unity_subproj(self):
6210        testdir = os.path.join(self.common_test_dir, '45 subproject')
6211        self.init(testdir, extra_args='--unity=subprojects')
6212        pdirs = glob(os.path.join(self.builddir, 'subprojects/sublib/simpletest*.p'))
6213        self.assertEqual(len(pdirs), 1)
6214        self.assertPathExists(os.path.join(pdirs[0], 'simpletest-unity0.c'))
6215        sdirs = glob(os.path.join(self.builddir, 'subprojects/sublib/*sublib*.p'))
6216        self.assertEqual(len(sdirs), 1)
6217        self.assertPathExists(os.path.join(sdirs[0], 'sublib-unity0.c'))
6218        self.assertPathDoesNotExist(os.path.join(self.builddir, 'user@exe/user-unity.c'))
6219        self.build()
6220
6221    def test_installed_modes(self):
6222        '''
6223        Test that files installed by these tests have the correct permissions.
6224        Can't be an ordinary test because our installed_files.txt is very basic.
6225        '''
6226        # Test file modes
6227        testdir = os.path.join(self.common_test_dir, '12 data')
6228        self.init(testdir)
6229        self.install()
6230
6231        f = os.path.join(self.installdir, 'etc', 'etcfile.dat')
6232        found_mode = stat.filemode(os.stat(f).st_mode)
6233        want_mode = 'rw------T'
6234        self.assertEqual(want_mode, found_mode[1:])
6235
6236        f = os.path.join(self.installdir, 'usr', 'bin', 'runscript.sh')
6237        statf = os.stat(f)
6238        found_mode = stat.filemode(statf.st_mode)
6239        want_mode = 'rwxr-sr-x'
6240        self.assertEqual(want_mode, found_mode[1:])
6241        if os.getuid() == 0:
6242            # The chown failed nonfatally if we're not root
6243            self.assertEqual(0, statf.st_uid)
6244            self.assertEqual(0, statf.st_gid)
6245
6246        f = os.path.join(self.installdir, 'usr', 'share', 'progname',
6247                         'fileobject_datafile.dat')
6248        orig = os.path.join(testdir, 'fileobject_datafile.dat')
6249        statf = os.stat(f)
6250        statorig = os.stat(orig)
6251        found_mode = stat.filemode(statf.st_mode)
6252        orig_mode = stat.filemode(statorig.st_mode)
6253        self.assertEqual(orig_mode[1:], found_mode[1:])
6254        self.assertEqual(os.getuid(), statf.st_uid)
6255        if os.getuid() == 0:
6256            # The chown failed nonfatally if we're not root
6257            self.assertEqual(0, statf.st_gid)
6258
6259        self.wipe()
6260        # Test directory modes
6261        testdir = os.path.join(self.common_test_dir, '62 install subdir')
6262        self.init(testdir)
6263        self.install()
6264
6265        f = os.path.join(self.installdir, 'usr', 'share', 'sub1', 'second.dat')
6266        statf = os.stat(f)
6267        found_mode = stat.filemode(statf.st_mode)
6268        want_mode = 'rwxr-x--t'
6269        self.assertEqual(want_mode, found_mode[1:])
6270        if os.getuid() == 0:
6271            # The chown failed nonfatally if we're not root
6272            self.assertEqual(0, statf.st_uid)
6273
6274    def test_installed_modes_extended(self):
6275        '''
6276        Test that files are installed with correct permissions using install_mode.
6277        '''
6278        testdir = os.path.join(self.common_test_dir, '195 install_mode')
6279        self.init(testdir)
6280        self.build()
6281        self.install()
6282
6283        for fsobj, want_mode in [
6284                ('bin', 'drwxr-x---'),
6285                ('bin/runscript.sh', '-rwxr-sr-x'),
6286                ('bin/trivialprog', '-rwxr-sr-x'),
6287                ('include', 'drwxr-x---'),
6288                ('include/config.h', '-rw-rwSr--'),
6289                ('include/rootdir.h', '-r--r--r-T'),
6290                ('lib', 'drwxr-x---'),
6291                ('lib/libstat.a', '-rw---Sr--'),
6292                ('share', 'drwxr-x---'),
6293                ('share/man', 'drwxr-x---'),
6294                ('share/man/man1', 'drwxr-x---'),
6295                ('share/man/man1/foo.1', '-r--r--r-T'),
6296                ('share/sub1', 'drwxr-x---'),
6297                ('share/sub1/second.dat', '-rwxr-x--t'),
6298                ('subdir', 'drwxr-x---'),
6299                ('subdir/data.dat', '-rw-rwSr--'),
6300        ]:
6301            f = os.path.join(self.installdir, 'usr', *fsobj.split('/'))
6302            found_mode = stat.filemode(os.stat(f).st_mode)
6303            self.assertEqual(want_mode, found_mode,
6304                             msg=('Expected file %s to have mode %s but found %s instead.' %
6305                                  (fsobj, want_mode, found_mode)))
6306        # Ensure that introspect --installed works on all types of files
6307        # FIXME: also verify the files list
6308        self.introspect('--installed')
6309
6310    def test_install_umask(self):
6311        '''
6312        Test that files are installed with correct permissions using default
6313        install umask of 022, regardless of the umask at time the worktree
6314        was checked out or the build was executed.
6315        '''
6316        # Copy source tree to a temporary directory and change permissions
6317        # there to simulate a checkout with umask 002.
6318        orig_testdir = os.path.join(self.unit_test_dir, '26 install umask')
6319        # Create a new testdir under tmpdir.
6320        tmpdir = os.path.realpath(tempfile.mkdtemp())
6321        self.addCleanup(windows_proof_rmtree, tmpdir)
6322        testdir = os.path.join(tmpdir, '26 install umask')
6323        # Copy the tree using shutil.copyfile, which will use the current umask
6324        # instead of preserving permissions of the old tree.
6325        save_umask = os.umask(0o002)
6326        self.addCleanup(os.umask, save_umask)
6327        shutil.copytree(orig_testdir, testdir, copy_function=shutil.copyfile)
6328        # Preserve the executable status of subdir/sayhello though.
6329        os.chmod(os.path.join(testdir, 'subdir', 'sayhello'), 0o775)
6330        self.init(testdir)
6331        # Run the build under a 027 umask now.
6332        os.umask(0o027)
6333        self.build()
6334        # And keep umask 027 for the install step too.
6335        self.install()
6336
6337        for executable in [
6338                'bin/prog',
6339                'share/subdir/sayhello',
6340        ]:
6341            f = os.path.join(self.installdir, 'usr', *executable.split('/'))
6342            found_mode = stat.filemode(os.stat(f).st_mode)
6343            want_mode = '-rwxr-xr-x'
6344            self.assertEqual(want_mode, found_mode,
6345                             msg=('Expected file %s to have mode %s but found %s instead.' %
6346                                  (executable, want_mode, found_mode)))
6347
6348        for directory in [
6349                'usr',
6350                'usr/bin',
6351                'usr/include',
6352                'usr/share',
6353                'usr/share/man',
6354                'usr/share/man/man1',
6355                'usr/share/subdir',
6356        ]:
6357            f = os.path.join(self.installdir, *directory.split('/'))
6358            found_mode = stat.filemode(os.stat(f).st_mode)
6359            want_mode = 'drwxr-xr-x'
6360            self.assertEqual(want_mode, found_mode,
6361                             msg=('Expected directory %s to have mode %s but found %s instead.' %
6362                                  (directory, want_mode, found_mode)))
6363
6364        for datafile in [
6365                'include/sample.h',
6366                'share/datafile.cat',
6367                'share/file.dat',
6368                'share/man/man1/prog.1',
6369                'share/subdir/datafile.dog',
6370        ]:
6371            f = os.path.join(self.installdir, 'usr', *datafile.split('/'))
6372            found_mode = stat.filemode(os.stat(f).st_mode)
6373            want_mode = '-rw-r--r--'
6374            self.assertEqual(want_mode, found_mode,
6375                             msg=('Expected file %s to have mode %s but found %s instead.' %
6376                                  (datafile, want_mode, found_mode)))
6377
6378    def test_cpp_std_override(self):
6379        testdir = os.path.join(self.unit_test_dir, '6 std override')
6380        self.init(testdir)
6381        compdb = self.get_compdb()
6382        # Don't try to use -std=c++03 as a check for the
6383        # presence of a compiler flag, as ICC does not
6384        # support it.
6385        for i in compdb:
6386            if 'prog98' in i['file']:
6387                c98_comp = i['command']
6388            if 'prog11' in i['file']:
6389                c11_comp = i['command']
6390            if 'progp' in i['file']:
6391                plain_comp = i['command']
6392        self.assertNotEqual(len(plain_comp), 0)
6393        self.assertIn('-std=c++98', c98_comp)
6394        self.assertNotIn('-std=c++11', c98_comp)
6395        self.assertIn('-std=c++11', c11_comp)
6396        self.assertNotIn('-std=c++98', c11_comp)
6397        self.assertNotIn('-std=c++98', plain_comp)
6398        self.assertNotIn('-std=c++11', plain_comp)
6399        # Now werror
6400        self.assertIn('-Werror', plain_comp)
6401        self.assertNotIn('-Werror', c98_comp)
6402
6403    def test_run_installed(self):
6404        if is_cygwin() or is_osx():
6405            raise unittest.SkipTest('LD_LIBRARY_PATH and RPATH not applicable')
6406
6407        testdir = os.path.join(self.unit_test_dir, '7 run installed')
6408        self.init(testdir)
6409        self.build()
6410        self.install()
6411        installed_exe = os.path.join(self.installdir, 'usr/bin/prog')
6412        installed_libdir = os.path.join(self.installdir, 'usr/foo')
6413        installed_lib = os.path.join(installed_libdir, 'libfoo.so')
6414        self.assertTrue(os.path.isfile(installed_exe))
6415        self.assertTrue(os.path.isdir(installed_libdir))
6416        self.assertTrue(os.path.isfile(installed_lib))
6417        # Must fail when run without LD_LIBRARY_PATH to ensure that
6418        # rpath has been properly stripped rather than pointing to the builddir.
6419        self.assertNotEqual(subprocess.call(installed_exe, stderr=subprocess.DEVNULL), 0)
6420        # When LD_LIBRARY_PATH is set it should start working.
6421        # For some reason setting LD_LIBRARY_PATH in os.environ fails
6422        # when all tests are run (but works when only this test is run),
6423        # but doing this explicitly works.
6424        env = os.environ.copy()
6425        env['LD_LIBRARY_PATH'] = ':'.join([installed_libdir, env.get('LD_LIBRARY_PATH', '')])
6426        self.assertEqual(subprocess.call(installed_exe, env=env), 0)
6427        # Ensure that introspect --installed works
6428        installed = self.introspect('--installed')
6429        for v in installed.values():
6430            self.assertTrue('prog' in v or 'foo' in v)
6431
6432    @skipIfNoPkgconfig
6433    def test_order_of_l_arguments(self):
6434        testdir = os.path.join(self.unit_test_dir, '8 -L -l order')
6435        self.init(testdir, override_envvars={'PKG_CONFIG_PATH': testdir})
6436        # NOTE: .pc file has -Lfoo -lfoo -Lbar -lbar but pkg-config reorders
6437        # the flags before returning them to -Lfoo -Lbar -lfoo -lbar
6438        # but pkgconf seems to not do that. Sigh. Support both.
6439        expected_order = [('-L/me/first', '-lfoo1'),
6440                          ('-L/me/second', '-lfoo2'),
6441                          ('-L/me/first', '-L/me/second'),
6442                          ('-lfoo1', '-lfoo2'),
6443                          ('-L/me/second', '-L/me/third'),
6444                          ('-L/me/third', '-L/me/fourth',),
6445                          ('-L/me/third', '-lfoo3'),
6446                          ('-L/me/fourth', '-lfoo4'),
6447                          ('-lfoo3', '-lfoo4'),
6448                          ]
6449        with open(os.path.join(self.builddir, 'build.ninja')) as ifile:
6450            for line in ifile:
6451                if expected_order[0][0] in line:
6452                    for first, second in expected_order:
6453                        self.assertLess(line.index(first), line.index(second))
6454                    return
6455        raise RuntimeError('Linker entries not found in the Ninja file.')
6456
6457    def test_introspect_dependencies(self):
6458        '''
6459        Tests that mesonintrospect --dependencies returns expected output.
6460        '''
6461        testdir = os.path.join(self.framework_test_dir, '7 gnome')
6462        self.init(testdir)
6463        glib_found = False
6464        gobject_found = False
6465        deps = self.introspect('--dependencies')
6466        self.assertIsInstance(deps, list)
6467        for dep in deps:
6468            self.assertIsInstance(dep, dict)
6469            self.assertIn('name', dep)
6470            self.assertIn('compile_args', dep)
6471            self.assertIn('link_args', dep)
6472            if dep['name'] == 'glib-2.0':
6473                glib_found = True
6474            elif dep['name'] == 'gobject-2.0':
6475                gobject_found = True
6476        self.assertTrue(glib_found)
6477        self.assertTrue(gobject_found)
6478        if subprocess.call(['pkg-config', '--exists', 'glib-2.0 >= 2.56.2']) != 0:
6479            raise unittest.SkipTest('glib >= 2.56.2 needed for the rest')
6480        targets = self.introspect('--targets')
6481        docbook_target = None
6482        for t in targets:
6483            if t['name'] == 'generated-gdbus-docbook':
6484                docbook_target = t
6485                break
6486        self.assertIsInstance(docbook_target, dict)
6487        self.assertEqual(os.path.basename(t['filename'][0]), 'generated-gdbus-doc-' + os.path.basename(t['target_sources'][0]['sources'][0]))
6488
6489    def test_introspect_installed(self):
6490        testdir = os.path.join(self.linuxlike_test_dir, '7 library versions')
6491        self.init(testdir)
6492
6493        install = self.introspect('--installed')
6494        install = {os.path.basename(k): v for k, v in install.items()}
6495        print(install)
6496        if is_osx():
6497            the_truth = {
6498                'libmodule.dylib': '/usr/lib/libmodule.dylib',
6499                'libnoversion.dylib': '/usr/lib/libnoversion.dylib',
6500                'libonlysoversion.5.dylib': '/usr/lib/libonlysoversion.5.dylib',
6501                'libonlysoversion.dylib': '/usr/lib/libonlysoversion.dylib',
6502                'libonlyversion.1.dylib': '/usr/lib/libonlyversion.1.dylib',
6503                'libonlyversion.dylib': '/usr/lib/libonlyversion.dylib',
6504                'libsome.0.dylib': '/usr/lib/libsome.0.dylib',
6505                'libsome.dylib': '/usr/lib/libsome.dylib',
6506            }
6507            the_truth_2 = {'/usr/lib/libsome.dylib',
6508                           '/usr/lib/libsome.0.dylib',
6509            }
6510        else:
6511            the_truth = {
6512                'libmodule.so': '/usr/lib/libmodule.so',
6513                'libnoversion.so': '/usr/lib/libnoversion.so',
6514                'libonlysoversion.so': '/usr/lib/libonlysoversion.so',
6515                'libonlysoversion.so.5': '/usr/lib/libonlysoversion.so.5',
6516                'libonlyversion.so': '/usr/lib/libonlyversion.so',
6517                'libonlyversion.so.1': '/usr/lib/libonlyversion.so.1',
6518                'libonlyversion.so.1.4.5': '/usr/lib/libonlyversion.so.1.4.5',
6519                'libsome.so': '/usr/lib/libsome.so',
6520                'libsome.so.0': '/usr/lib/libsome.so.0',
6521                'libsome.so.1.2.3': '/usr/lib/libsome.so.1.2.3',
6522            }
6523            the_truth_2 = {'/usr/lib/libsome.so',
6524                           '/usr/lib/libsome.so.0',
6525                           '/usr/lib/libsome.so.1.2.3'}
6526        self.assertDictEqual(install, the_truth)
6527
6528        targets = self.introspect('--targets')
6529        for t in targets:
6530            if t['name'] != 'some':
6531                continue
6532            self.assertSetEqual(the_truth_2, set(t['install_filename']))
6533
6534    def test_build_rpath(self):
6535        if is_cygwin():
6536            raise unittest.SkipTest('Windows PE/COFF binaries do not use RPATH')
6537        testdir = os.path.join(self.unit_test_dir, '10 build_rpath')
6538        self.init(testdir)
6539        self.build()
6540        # C program RPATH
6541        build_rpath = get_rpath(os.path.join(self.builddir, 'prog'))
6542        self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar')
6543        self.install()
6544        install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/prog'))
6545        self.assertEqual(install_rpath, '/baz')
6546        # C++ program RPATH
6547        build_rpath = get_rpath(os.path.join(self.builddir, 'progcxx'))
6548        self.assertEqual(build_rpath, '$ORIGIN/sub:/foo/bar')
6549        self.install()
6550        install_rpath = get_rpath(os.path.join(self.installdir, 'usr/bin/progcxx'))
6551        self.assertEqual(install_rpath, 'baz')
6552
6553    def test_global_rpath(self):
6554        if is_cygwin():
6555            raise unittest.SkipTest('Windows PE/COFF binaries do not use RPATH')
6556        if is_osx():
6557            raise unittest.SkipTest('Global RPATHs via LDFLAGS not yet supported on MacOS (does anybody need it?)')
6558
6559        testdir = os.path.join(self.unit_test_dir, '77 global-rpath')
6560        oldinstalldir = self.installdir
6561
6562        # Build and install an external library without DESTDIR.
6563        # The external library generates a .pc file without an rpath.
6564        yonder_dir = os.path.join(testdir, 'yonder')
6565        yonder_prefix = os.path.join(oldinstalldir, 'yonder')
6566        yonder_libdir = os.path.join(yonder_prefix, self.libdir)
6567        self.prefix = yonder_prefix
6568        self.installdir = yonder_prefix
6569        self.init(yonder_dir)
6570        self.build()
6571        self.install(use_destdir=False)
6572
6573        # Since rpath has multiple valid formats we need to
6574        # test that they are all properly used.
6575        rpath_formats = [
6576            ('-Wl,-rpath=', False),
6577            ('-Wl,-rpath,', False),
6578            ('-Wl,--just-symbols=', True),
6579            ('-Wl,--just-symbols,', True),
6580            ('-Wl,-R', False),
6581            ('-Wl,-R,', False)
6582        ]
6583        for rpath_format, exception in rpath_formats:
6584            # Build an app that uses that installed library.
6585            # Supply the rpath to the installed library via LDFLAGS
6586            # (as systems like buildroot and guix are wont to do)
6587            # and verify install preserves that rpath.
6588            self.new_builddir()
6589            env = {'LDFLAGS': rpath_format + yonder_libdir,
6590                   'PKG_CONFIG_PATH': os.path.join(yonder_libdir, 'pkgconfig')}
6591            if exception:
6592                with self.assertRaises(subprocess.CalledProcessError):
6593                    self.init(testdir, override_envvars=env)
6594                break
6595            self.init(testdir, override_envvars=env)
6596            self.build()
6597            self.install(use_destdir=False)
6598            got_rpath = get_rpath(os.path.join(yonder_prefix, 'bin/rpathified'))
6599            self.assertEqual(got_rpath, yonder_libdir, rpath_format)
6600
6601    @skip_if_not_base_option('b_sanitize')
6602    def test_pch_with_address_sanitizer(self):
6603        if is_cygwin():
6604            raise unittest.SkipTest('asan not available on Cygwin')
6605        if is_openbsd():
6606            raise unittest.SkipTest('-fsanitize=address is not supported on OpenBSD')
6607
6608        testdir = os.path.join(self.common_test_dir, '13 pch')
6609        self.init(testdir, extra_args=['-Db_sanitize=address', '-Db_lundef=false'])
6610        self.build()
6611        compdb = self.get_compdb()
6612        for i in compdb:
6613            self.assertIn("-fsanitize=address", i["command"])
6614
6615    def test_cross_find_program(self):
6616        testdir = os.path.join(self.unit_test_dir, '11 cross prog')
6617        crossfile = tempfile.NamedTemporaryFile(mode='w')
6618        print(os.path.join(testdir, 'some_cross_tool.py'))
6619        crossfile.write(textwrap.dedent('''\
6620            [binaries]
6621            c = '/usr/bin/{1}'
6622            ar = '/usr/bin/ar'
6623            strip = '/usr/bin/ar'
6624            sometool.py = ['{0}']
6625            someothertool.py = '{0}'
6626
6627            [properties]
6628
6629            [host_machine]
6630            system = 'linux'
6631            cpu_family = 'arm'
6632            cpu = 'armv7' # Not sure if correct.
6633            endian = 'little'
6634            ''').format(os.path.join(testdir, 'some_cross_tool.py'),
6635                        'gcc' if is_sunos() else 'cc'))
6636        crossfile.flush()
6637        self.meson_cross_file = crossfile.name
6638        self.init(testdir)
6639
6640    def test_reconfigure(self):
6641        testdir = os.path.join(self.unit_test_dir, '13 reconfigure')
6642        self.init(testdir, extra_args=['-Db_coverage=true'], default_args=False)
6643        self.build('reconfigure')
6644
6645    def test_vala_generated_source_buildir_inside_source_tree(self):
6646        '''
6647        Test that valac outputs generated C files in the expected location when
6648        the builddir is a subdir of the source tree.
6649        '''
6650        if not shutil.which('valac'):
6651            raise unittest.SkipTest('valac not installed.')
6652
6653        testdir = os.path.join(self.vala_test_dir, '8 generated sources')
6654        newdir = os.path.join(self.builddir, 'srctree')
6655        shutil.copytree(testdir, newdir)
6656        testdir = newdir
6657        # New builddir
6658        builddir = os.path.join(testdir, 'subdir/_build')
6659        os.makedirs(builddir, exist_ok=True)
6660        self.change_builddir(builddir)
6661        self.init(testdir)
6662        self.build()
6663
6664    def test_old_gnome_module_codepaths(self):
6665        '''
6666        A lot of code in the GNOME module is conditional on the version of the
6667        glib tools that are installed, and breakages in the old code can slip
6668        by once the CI has a newer glib version. So we force the GNOME module
6669        to pretend that it's running on an ancient glib so the fallback code is
6670        also tested.
6671        '''
6672        testdir = os.path.join(self.framework_test_dir, '7 gnome')
6673        mesonbuild.modules.gnome.native_glib_version = '2.20'
6674        env = {'MESON_UNIT_TEST_PRETEND_GLIB_OLD': "1"}
6675        try:
6676            self.init(testdir,
6677                      inprocess=True,
6678                      override_envvars=env)
6679            self.build(override_envvars=env)
6680        finally:
6681            mesonbuild.modules.gnome.native_glib_version = None
6682
6683    @skipIfNoPkgconfig
6684    def test_pkgconfig_usage(self):
6685        testdir1 = os.path.join(self.unit_test_dir, '27 pkgconfig usage/dependency')
6686        testdir2 = os.path.join(self.unit_test_dir, '27 pkgconfig usage/dependee')
6687        if subprocess.call(['pkg-config', '--cflags', 'glib-2.0'],
6688                           stdout=subprocess.DEVNULL,
6689                           stderr=subprocess.DEVNULL) != 0:
6690            raise unittest.SkipTest('Glib 2.0 dependency not available.')
6691        with tempfile.TemporaryDirectory() as tempdirname:
6692            self.init(testdir1, extra_args=['--prefix=' + tempdirname, '--libdir=lib'], default_args=False)
6693            self.install(use_destdir=False)
6694            shutil.rmtree(self.builddir)
6695            os.mkdir(self.builddir)
6696            pkg_dir = os.path.join(tempdirname, 'lib/pkgconfig')
6697            self.assertTrue(os.path.exists(os.path.join(pkg_dir, 'libpkgdep.pc')))
6698            lib_dir = os.path.join(tempdirname, 'lib')
6699            myenv = os.environ.copy()
6700            myenv['PKG_CONFIG_PATH'] = pkg_dir
6701            # Private internal libraries must not leak out.
6702            pkg_out = subprocess.check_output(['pkg-config', '--static', '--libs', 'libpkgdep'], env=myenv)
6703            self.assertFalse(b'libpkgdep-int' in pkg_out, 'Internal library leaked out.')
6704            # Dependencies must not leak to cflags when building only a shared library.
6705            pkg_out = subprocess.check_output(['pkg-config', '--cflags', 'libpkgdep'], env=myenv)
6706            self.assertFalse(b'glib' in pkg_out, 'Internal dependency leaked to headers.')
6707            # Test that the result is usable.
6708            self.init(testdir2, override_envvars=myenv)
6709            self.build(override_envvars=myenv)
6710            myenv = os.environ.copy()
6711            myenv['LD_LIBRARY_PATH'] = ':'.join([lib_dir, myenv.get('LD_LIBRARY_PATH', '')])
6712            if is_cygwin():
6713                bin_dir = os.path.join(tempdirname, 'bin')
6714                myenv['PATH'] = bin_dir + os.pathsep + myenv['PATH']
6715            self.assertTrue(os.path.isdir(lib_dir))
6716            test_exe = os.path.join(self.builddir, 'pkguser')
6717            self.assertTrue(os.path.isfile(test_exe))
6718            subprocess.check_call(test_exe, env=myenv)
6719
6720    @skipIfNoPkgconfig
6721    def test_pkgconfig_relative_paths(self):
6722        testdir = os.path.join(self.unit_test_dir, '62 pkgconfig relative paths')
6723        pkg_dir = os.path.join(testdir, 'pkgconfig')
6724        self.assertTrue(os.path.exists(os.path.join(pkg_dir, 'librelativepath.pc')))
6725
6726        env = get_fake_env(testdir, self.builddir, self.prefix)
6727        env.coredata.set_options({'pkg_config_path': pkg_dir}, subproject='')
6728        kwargs = {'required': True, 'silent': True}
6729        relative_path_dep = PkgConfigDependency('librelativepath', env, kwargs)
6730        self.assertTrue(relative_path_dep.found())
6731
6732        # Ensure link_args are properly quoted
6733        libpath = Path(self.builddir) / '../relativepath/lib'
6734        link_args = ['-L' + libpath.as_posix(), '-lrelativepath']
6735        self.assertEqual(relative_path_dep.get_link_args(), link_args)
6736
6737    @skipIfNoPkgconfig
6738    def test_pkgconfig_internal_libraries(self):
6739        '''
6740        '''
6741        with tempfile.TemporaryDirectory() as tempdirname:
6742            # build library
6743            testdirbase = os.path.join(self.unit_test_dir, '32 pkgconfig use libraries')
6744            testdirlib = os.path.join(testdirbase, 'lib')
6745            self.init(testdirlib, extra_args=['--prefix=' + tempdirname,
6746                                              '--libdir=lib',
6747                                              '--default-library=static'], default_args=False)
6748            self.build()
6749            self.install(use_destdir=False)
6750
6751            # build user of library
6752            pkg_dir = os.path.join(tempdirname, 'lib/pkgconfig')
6753            self.new_builddir()
6754            self.init(os.path.join(testdirbase, 'app'),
6755                      override_envvars={'PKG_CONFIG_PATH': pkg_dir})
6756            self.build()
6757
6758    @skipIfNoPkgconfig
6759    def test_static_archive_stripping(self):
6760        '''
6761        Check that Meson produces valid static archives with --strip enabled
6762        '''
6763        with tempfile.TemporaryDirectory() as tempdirname:
6764            testdirbase = os.path.join(self.unit_test_dir, '67 static archive stripping')
6765
6766            # build lib
6767            self.new_builddir()
6768            testdirlib = os.path.join(testdirbase, 'lib')
6769            testlibprefix = os.path.join(tempdirname, 'libprefix')
6770            self.init(testdirlib, extra_args=['--prefix=' + testlibprefix,
6771                                              '--libdir=lib',
6772                                              '--default-library=static',
6773                                              '--buildtype=debug',
6774                                              '--strip'], default_args=False)
6775            self.build()
6776            self.install(use_destdir=False)
6777
6778            # build executable (uses lib, fails if static archive has been stripped incorrectly)
6779            pkg_dir = os.path.join(testlibprefix, 'lib/pkgconfig')
6780            self.new_builddir()
6781            self.init(os.path.join(testdirbase, 'app'),
6782                      override_envvars={'PKG_CONFIG_PATH': pkg_dir})
6783            self.build()
6784
6785    @skipIfNoPkgconfig
6786    def test_pkgconfig_formatting(self):
6787        testdir = os.path.join(self.unit_test_dir, '38 pkgconfig format')
6788        self.init(testdir)
6789        myenv = os.environ.copy()
6790        myenv['PKG_CONFIG_PATH'] = self.privatedir
6791        stdo = subprocess.check_output(['pkg-config', '--libs-only-l', 'libsomething'], env=myenv)
6792        deps = [b'-lgobject-2.0', b'-lgio-2.0', b'-lglib-2.0', b'-lsomething']
6793        if is_windows() or is_cygwin() or is_osx() or is_openbsd():
6794            # On Windows, libintl is a separate library
6795            deps.append(b'-lintl')
6796        self.assertEqual(set(deps), set(stdo.split()))
6797
6798    @skipIfNoPkgconfig
6799    @skip_if_not_language('cs')
6800    def test_pkgconfig_csharp_library(self):
6801        testdir = os.path.join(self.unit_test_dir, '50 pkgconfig csharp library')
6802        self.init(testdir)
6803        myenv = os.environ.copy()
6804        myenv['PKG_CONFIG_PATH'] = self.privatedir
6805        stdo = subprocess.check_output(['pkg-config', '--libs', 'libsomething'], env=myenv)
6806
6807        self.assertEqual("-r/usr/lib/libsomething.dll", str(stdo.decode('ascii')).strip())
6808
6809    @skipIfNoPkgconfig
6810    def test_pkgconfig_link_order(self):
6811        '''
6812        Test that libraries are listed before their dependencies.
6813        '''
6814        testdir = os.path.join(self.unit_test_dir, '53 pkgconfig static link order')
6815        self.init(testdir)
6816        myenv = os.environ.copy()
6817        myenv['PKG_CONFIG_PATH'] = self.privatedir
6818        stdo = subprocess.check_output(['pkg-config', '--libs', 'libsomething'], env=myenv)
6819        deps = stdo.split()
6820        self.assertTrue(deps.index(b'-lsomething') < deps.index(b'-ldependency'))
6821
6822    def test_deterministic_dep_order(self):
6823        '''
6824        Test that the dependencies are always listed in a deterministic order.
6825        '''
6826        testdir = os.path.join(self.unit_test_dir, '43 dep order')
6827        self.init(testdir)
6828        with open(os.path.join(self.builddir, 'build.ninja')) as bfile:
6829            for line in bfile:
6830                if 'build myexe:' in line or 'build myexe.exe:' in line:
6831                    self.assertIn('liblib1.a liblib2.a', line)
6832                    return
6833        raise RuntimeError('Could not find the build rule')
6834
6835    def test_deterministic_rpath_order(self):
6836        '''
6837        Test that the rpaths are always listed in a deterministic order.
6838        '''
6839        if is_cygwin():
6840            raise unittest.SkipTest('rpath are not used on Cygwin')
6841        testdir = os.path.join(self.unit_test_dir, '42 rpath order')
6842        self.init(testdir)
6843        if is_osx():
6844            rpathre = re.compile(r'-rpath,.*/subprojects/sub1.*-rpath,.*/subprojects/sub2')
6845        else:
6846            rpathre = re.compile(r'-rpath,\$\$ORIGIN/subprojects/sub1:\$\$ORIGIN/subprojects/sub2')
6847        with open(os.path.join(self.builddir, 'build.ninja')) as bfile:
6848            for line in bfile:
6849                if '-rpath' in line:
6850                    self.assertRegex(line, rpathre)
6851                    return
6852        raise RuntimeError('Could not find the rpath')
6853
6854    def test_override_with_exe_dep(self):
6855        '''
6856        Test that we produce the correct dependencies when a program is overridden with an executable.
6857        '''
6858        testdir = os.path.join(self.common_test_dir, '201 override with exe')
6859        self.init(testdir)
6860        with open(os.path.join(self.builddir, 'build.ninja')) as bfile:
6861            for line in bfile:
6862                if 'main1.c:' in line or 'main2.c:' in line:
6863                    self.assertIn('| subprojects/sub/foobar', line)
6864
6865    @skipIfNoPkgconfig
6866    def test_usage_external_library(self):
6867        '''
6868        Test that uninstalled usage of an external library (from the system or
6869        PkgConfigDependency) works. On macOS, this workflow works out of the
6870        box. On Linux, BSDs, Windows, etc, you need to set extra arguments such
6871        as LD_LIBRARY_PATH, etc, so this test is skipped.
6872
6873        The system library is found with cc.find_library() and pkg-config deps.
6874        '''
6875        oldprefix = self.prefix
6876        # Install external library so we can find it
6877        testdir = os.path.join(self.unit_test_dir, '40 external, internal library rpath', 'external library')
6878        # install into installdir without using DESTDIR
6879        installdir = self.installdir
6880        self.prefix = installdir
6881        self.init(testdir)
6882        self.prefix = oldprefix
6883        self.build()
6884        self.install(use_destdir=False)
6885        ## New builddir for the consumer
6886        self.new_builddir()
6887        env = {'LIBRARY_PATH': os.path.join(installdir, self.libdir),
6888               'PKG_CONFIG_PATH': os.path.join(installdir, self.libdir, 'pkgconfig')}
6889        testdir = os.path.join(self.unit_test_dir, '40 external, internal library rpath', 'built library')
6890        # install into installdir without using DESTDIR
6891        self.prefix = self.installdir
6892        self.init(testdir, override_envvars=env)
6893        self.prefix = oldprefix
6894        self.build(override_envvars=env)
6895        # test uninstalled
6896        self.run_tests(override_envvars=env)
6897        if not (is_osx() or is_linux()):
6898            return
6899        # test running after installation
6900        self.install(use_destdir=False)
6901        prog = os.path.join(self.installdir, 'bin', 'prog')
6902        self._run([prog])
6903        if not is_osx():
6904            # Rest of the workflow only works on macOS
6905            return
6906        out = self._run(['otool', '-L', prog])
6907        self.assertNotIn('@rpath', out)
6908        ## New builddir for testing that DESTDIR is not added to install_name
6909        self.new_builddir()
6910        # install into installdir with DESTDIR
6911        self.init(testdir, override_envvars=env)
6912        self.build(override_envvars=env)
6913        # test running after installation
6914        self.install(override_envvars=env)
6915        prog = self.installdir + os.path.join(self.prefix, 'bin', 'prog')
6916        lib = self.installdir + os.path.join(self.prefix, 'lib', 'libbar_built.dylib')
6917        for f in prog, lib:
6918            out = self._run(['otool', '-L', f])
6919            # Ensure that the otool output does not contain self.installdir
6920            self.assertNotRegex(out, self.installdir + '.*dylib ')
6921
6922    @skipIfNoPkgconfig
6923    def test_usage_pkgconfig_prefixes(self):
6924        '''
6925        Build and install two external libraries, to different prefixes,
6926        then build and install a client program that finds them via pkgconfig,
6927        and verify the installed client program runs.
6928        '''
6929        oldinstalldir = self.installdir
6930
6931        # Build and install both external libraries without DESTDIR
6932        val1dir = os.path.join(self.unit_test_dir, '76 pkgconfig prefixes', 'val1')
6933        val1prefix = os.path.join(oldinstalldir, 'val1')
6934        self.prefix = val1prefix
6935        self.installdir = val1prefix
6936        self.init(val1dir)
6937        self.build()
6938        self.install(use_destdir=False)
6939        self.new_builddir()
6940
6941        env1 = {}
6942        env1['PKG_CONFIG_PATH'] = os.path.join(val1prefix, self.libdir, 'pkgconfig')
6943        val2dir = os.path.join(self.unit_test_dir, '76 pkgconfig prefixes', 'val2')
6944        val2prefix = os.path.join(oldinstalldir, 'val2')
6945        self.prefix = val2prefix
6946        self.installdir = val2prefix
6947        self.init(val2dir, override_envvars=env1)
6948        self.build()
6949        self.install(use_destdir=False)
6950        self.new_builddir()
6951
6952        # Build, install, and run the client program
6953        env2 = {}
6954        env2['PKG_CONFIG_PATH'] = os.path.join(val2prefix, self.libdir, 'pkgconfig')
6955        testdir = os.path.join(self.unit_test_dir, '76 pkgconfig prefixes', 'client')
6956        testprefix = os.path.join(oldinstalldir, 'client')
6957        self.prefix = testprefix
6958        self.installdir = testprefix
6959        self.init(testdir, override_envvars=env2)
6960        self.build()
6961        self.install(use_destdir=False)
6962        prog = os.path.join(self.installdir, 'bin', 'client')
6963        env3 = {}
6964        if is_cygwin():
6965            env3['PATH'] = os.path.join(val1prefix, 'bin') + \
6966                os.pathsep + \
6967                os.path.join(val2prefix, 'bin') + \
6968                os.pathsep + os.environ['PATH']
6969        out = self._run([prog], override_envvars=env3).strip()
6970        # Expected output is val1 + val2 = 3
6971        self.assertEqual(out, '3')
6972
6973    def install_subdir_invalid_symlinks(self, testdir, subdir_path):
6974        '''
6975        Test that installation of broken symlinks works fine.
6976        https://github.com/mesonbuild/meson/issues/3914
6977        '''
6978        testdir = os.path.join(self.common_test_dir, testdir)
6979        subdir = os.path.join(testdir, subdir_path)
6980        with chdir(subdir):
6981            # Can't distribute broken symlinks in the source tree because it breaks
6982            # the creation of zipapps. Create it dynamically and run the test by
6983            # hand.
6984            src = '../../nonexistent.txt'
6985            os.symlink(src, 'invalid-symlink.txt')
6986            try:
6987                self.init(testdir)
6988                self.build()
6989                self.install()
6990                install_path = subdir_path.split(os.path.sep)[-1]
6991                link = os.path.join(self.installdir, 'usr', 'share', install_path, 'invalid-symlink.txt')
6992                self.assertTrue(os.path.islink(link), msg=link)
6993                self.assertEqual(src, os.readlink(link))
6994                self.assertFalse(os.path.isfile(link), msg=link)
6995            finally:
6996                os.remove(os.path.join(subdir, 'invalid-symlink.txt'))
6997
6998    def test_install_subdir_symlinks(self):
6999        self.install_subdir_invalid_symlinks('62 install subdir', os.path.join('sub', 'sub1'))
7000
7001    def test_install_subdir_symlinks_with_default_umask(self):
7002        self.install_subdir_invalid_symlinks('195 install_mode', 'sub2')
7003
7004    def test_install_subdir_symlinks_with_default_umask_and_mode(self):
7005        self.install_subdir_invalid_symlinks('195 install_mode', 'sub1')
7006
7007    @skipIfNoPkgconfigDep('gmodule-2.0')
7008    def test_ldflag_dedup(self):
7009        testdir = os.path.join(self.unit_test_dir, '52 ldflagdedup')
7010        if is_cygwin() or is_osx():
7011            raise unittest.SkipTest('Not applicable on Cygwin or OSX.')
7012        env = get_fake_env()
7013        cc = env.detect_c_compiler(MachineChoice.HOST)
7014        linker = cc.linker
7015        if not linker.export_dynamic_args(env):
7016            raise unittest.SkipTest('Not applicable for linkers without --export-dynamic')
7017        self.init(testdir)
7018        build_ninja = os.path.join(self.builddir, 'build.ninja')
7019        max_count = 0
7020        search_term = '-Wl,--export-dynamic'
7021        with open(build_ninja, 'r', encoding='utf-8') as f:
7022            for line in f:
7023                max_count = max(max_count, line.count(search_term))
7024        self.assertEqual(max_count, 1, 'Export dynamic incorrectly deduplicated.')
7025
7026    def test_compiler_libs_static_dedup(self):
7027        testdir = os.path.join(self.unit_test_dir, '56 dedup compiler libs')
7028        self.init(testdir)
7029        build_ninja = os.path.join(self.builddir, 'build.ninja')
7030        with open(build_ninja, 'r', encoding='utf-8') as f:
7031            lines = f.readlines()
7032        for lib in ('-ldl', '-lm', '-lc', '-lrt'):
7033            for line in lines:
7034                if lib not in line:
7035                    continue
7036                # Assert that
7037                self.assertEqual(len(line.split(lib)), 2, msg=(lib, line))
7038
7039    @skipIfNoPkgconfig
7040    def test_noncross_options(self):
7041        # C_std defined in project options must be in effect also when native compiling.
7042        testdir = os.path.join(self.unit_test_dir, '51 noncross options')
7043        self.init(testdir, extra_args=['-Dpkg_config_path=' + testdir])
7044        compdb = self.get_compdb()
7045        self.assertEqual(len(compdb), 2)
7046        self.assertRegex(compdb[0]['command'], '-std=c99')
7047        self.assertRegex(compdb[1]['command'], '-std=c99')
7048        self.build()
7049
7050    def test_identity_cross(self):
7051        testdir = os.path.join(self.unit_test_dir, '61 identity cross')
7052
7053        nativefile = tempfile.NamedTemporaryFile(mode='w')
7054        nativefile.write('''[binaries]
7055c = ['{0}']
7056'''.format(os.path.join(testdir, 'build_wrapper.py')))
7057        nativefile.flush()
7058        self.meson_native_file = nativefile.name
7059
7060        crossfile = tempfile.NamedTemporaryFile(mode='w')
7061        crossfile.write('''[binaries]
7062c = ['{0}']
7063'''.format(os.path.join(testdir, 'host_wrapper.py')))
7064        crossfile.flush()
7065        self.meson_cross_file = crossfile.name
7066
7067        # TODO should someday be explicit about build platform only here
7068        self.init(testdir)
7069
7070    def test_identity_cross_env(self):
7071        testdir = os.path.join(self.unit_test_dir, '61 identity cross')
7072        env = {
7073            'CC_FOR_BUILD': '"' + os.path.join(testdir, 'build_wrapper.py') + '"',
7074        }
7075        crossfile = tempfile.NamedTemporaryFile(mode='w')
7076        crossfile.write('''[binaries]
7077c = ['{0}']
7078'''.format(os.path.join(testdir, 'host_wrapper.py')))
7079        crossfile.flush()
7080        self.meson_cross_file = crossfile.name
7081        # TODO should someday be explicit about build platform only here
7082        self.init(testdir, override_envvars=env)
7083
7084    @skipIfNoPkgconfig
7085    def test_static_link(self):
7086        if is_cygwin():
7087            raise unittest.SkipTest("Cygwin doesn't support LD_LIBRARY_PATH.")
7088
7089        # Build some libraries and install them
7090        testdir = os.path.join(self.unit_test_dir, '68 static link/lib')
7091        libdir = os.path.join(self.installdir, self.libdir)
7092        oldprefix = self.prefix
7093        self.prefix = self.installdir
7094        self.init(testdir)
7095        self.install(use_destdir=False)
7096
7097        # Test that installed libraries works
7098        self.new_builddir()
7099        self.prefix = oldprefix
7100        meson_args = ['-Dc_link_args=-L{}'.format(libdir),
7101                      '--fatal-meson-warnings']
7102        testdir = os.path.join(self.unit_test_dir, '68 static link')
7103        env = {'PKG_CONFIG_LIBDIR': os.path.join(libdir, 'pkgconfig')}
7104        self.init(testdir, extra_args=meson_args, override_envvars=env)
7105        self.build()
7106        self.run_tests()
7107
7108    def _check_ld(self, check: str, name: str, lang: str, expected: str) -> None:
7109        if is_sunos():
7110            raise unittest.SkipTest('Solaris currently cannot override the linker.')
7111        if not shutil.which(check):
7112            raise unittest.SkipTest('Could not find {}.'.format(check))
7113        envvars = [mesonbuild.envconfig.BinaryTable.evarMap['{}_ld'.format(lang)]]
7114
7115        # Also test a deprecated variable if there is one.
7116        if envvars[0] in mesonbuild.envconfig.BinaryTable.DEPRECATION_MAP:
7117            envvars.append(
7118                mesonbuild.envconfig.BinaryTable.DEPRECATION_MAP[envvars[0]])
7119
7120        for envvar in envvars:
7121            with mock.patch.dict(os.environ, {envvar: name}):
7122                env = get_fake_env()
7123                comp = getattr(env, 'detect_{}_compiler'.format(lang))(MachineChoice.HOST)
7124                if lang != 'rust' and comp.use_linker_args('bfd') == []:
7125                    raise unittest.SkipTest(
7126                        'Compiler {} does not support using alternative linkers'.format(comp.id))
7127                self.assertEqual(comp.linker.id, expected)
7128
7129    def test_ld_environment_variable_bfd(self):
7130        self._check_ld('ld.bfd', 'bfd', 'c', 'ld.bfd')
7131
7132    def test_ld_environment_variable_gold(self):
7133        self._check_ld('ld.gold', 'gold', 'c', 'ld.gold')
7134
7135    def test_ld_environment_variable_lld(self):
7136        self._check_ld('ld.lld', 'lld', 'c', 'ld.lld')
7137
7138    @skip_if_not_language('rust')
7139    def test_ld_environment_variable_rust(self):
7140        self._check_ld('ld.gold', 'gold', 'rust', 'ld.gold')
7141
7142    def test_ld_environment_variable_cpp(self):
7143        self._check_ld('ld.gold', 'gold', 'cpp', 'ld.gold')
7144
7145    @skip_if_not_language('objc')
7146    def test_ld_environment_variable_objc(self):
7147        self._check_ld('ld.gold', 'gold', 'objc', 'ld.gold')
7148
7149    @skip_if_not_language('objcpp')
7150    def test_ld_environment_variable_objcpp(self):
7151        self._check_ld('ld.gold', 'gold', 'objcpp', 'ld.gold')
7152
7153    @skip_if_not_language('fortran')
7154    def test_ld_environment_variable_fortran(self):
7155        self._check_ld('ld.gold', 'gold', 'fortran', 'ld.gold')
7156
7157    @skip_if_not_language('d')
7158    def test_ld_environment_variable_d(self):
7159        # At least for me, ldc defaults to gold, and gdc defaults to bfd, so
7160        # let's pick lld, which isn't the default for either (currently)
7161        self._check_ld('ld.lld', 'lld', 'd', 'ld.lld')
7162
7163    def compute_sha256(self, filename):
7164        with open(filename, 'rb') as f:
7165            return hashlib.sha256(f.read()).hexdigest()
7166
7167    def test_wrap_with_file_url(self):
7168        testdir = os.path.join(self.unit_test_dir, '74 wrap file url')
7169        source_filename = os.path.join(testdir, 'subprojects', 'foo.tar.xz')
7170        patch_filename = os.path.join(testdir, 'subprojects', 'foo-patch.tar.xz')
7171        wrap_filename = os.path.join(testdir, 'subprojects', 'foo.wrap')
7172        source_hash = self.compute_sha256(source_filename)
7173        patch_hash = self.compute_sha256(patch_filename)
7174        wrap = textwrap.dedent("""\
7175            [wrap-file]
7176            directory = foo
7177
7178            source_url = http://server.invalid/foo
7179            source_fallback_url = file://{}
7180            source_filename = foo.tar.xz
7181            source_hash = {}
7182
7183            patch_url = http://server.invalid/foo
7184            patch_fallback_url = file://{}
7185            patch_filename = foo-patch.tar.xz
7186            patch_hash = {}
7187            """.format(source_filename, source_hash, patch_filename, patch_hash))
7188        with open(wrap_filename, 'w') as f:
7189            f.write(wrap)
7190        self.init(testdir)
7191        self.build()
7192        self.run_tests()
7193
7194        windows_proof_rmtree(os.path.join(testdir, 'subprojects', 'packagecache'))
7195        windows_proof_rmtree(os.path.join(testdir, 'subprojects', 'foo'))
7196        os.unlink(wrap_filename)
7197
7198    def test_no_rpath_for_static(self):
7199        testdir = os.path.join(self.common_test_dir, '5 linkstatic')
7200        self.init(testdir)
7201        self.build()
7202        build_rpath = get_rpath(os.path.join(self.builddir, 'prog'))
7203        self.assertIsNone(build_rpath)
7204
7205    def test_lookup_system_after_broken_fallback(self):
7206        # Just to generate libfoo.pc so we can test system dependency lookup.
7207        testdir = os.path.join(self.common_test_dir, '47 pkgconfig-gen')
7208        self.init(testdir)
7209        privatedir = self.privatedir
7210
7211        # Write test project where the first dependency() returns not-found
7212        # because 'broken' subproject does not exit, but that should not prevent
7213        # the 2nd dependency() to lookup on system.
7214        self.new_builddir()
7215        with tempfile.TemporaryDirectory() as d:
7216            with open(os.path.join(d, 'meson.build'), 'w') as f:
7217                f.write(textwrap.dedent('''\
7218                    project('test')
7219                    dependency('notfound', fallback: 'broken', required: false)
7220                    dependency('libfoo', fallback: 'broken', required: true)
7221                    '''))
7222            self.init(d, override_envvars={'PKG_CONFIG_LIBDIR': privatedir})
7223
7224class BaseLinuxCrossTests(BasePlatformTests):
7225    # Don't pass --libdir when cross-compiling. We have tests that
7226    # check whether meson auto-detects it correctly.
7227    libdir = None
7228
7229
7230def should_run_cross_arm_tests():
7231    return shutil.which('arm-linux-gnueabihf-gcc') and not platform.machine().lower().startswith('arm')
7232
7233@unittest.skipUnless(not is_windows() and should_run_cross_arm_tests(), "requires ability to cross compile to ARM")
7234class LinuxCrossArmTests(BaseLinuxCrossTests):
7235    '''
7236    Tests that cross-compilation to Linux/ARM works
7237    '''
7238
7239    def setUp(self):
7240        super().setUp()
7241        src_root = os.path.dirname(__file__)
7242        self.meson_cross_file = os.path.join(src_root, 'cross', 'ubuntu-armhf.txt')
7243
7244    def test_cflags_cross_environment_pollution(self):
7245        '''
7246        Test that the CFLAGS environment variable does not pollute the cross
7247        environment. This can't be an ordinary test case because we need to
7248        inspect the compiler database.
7249        '''
7250        testdir = os.path.join(self.common_test_dir, '3 static')
7251        self.init(testdir, override_envvars={'CFLAGS': '-DBUILD_ENVIRONMENT_ONLY'})
7252        compdb = self.get_compdb()
7253        self.assertNotIn('-DBUILD_ENVIRONMENT_ONLY', compdb[0]['command'])
7254
7255    def test_cross_file_overrides_always_args(self):
7256        '''
7257        Test that $lang_args in cross files always override get_always_args().
7258        Needed for overriding the default -D_FILE_OFFSET_BITS=64 on some
7259        architectures such as some Android versions and Raspbian.
7260        https://github.com/mesonbuild/meson/issues/3049
7261        https://github.com/mesonbuild/meson/issues/3089
7262        '''
7263        testdir = os.path.join(self.unit_test_dir, '33 cross file overrides always args')
7264        self.meson_cross_file = os.path.join(testdir, 'ubuntu-armhf-overrides.txt')
7265        self.init(testdir)
7266        compdb = self.get_compdb()
7267        self.assertRegex(compdb[0]['command'], '-D_FILE_OFFSET_BITS=64.*-U_FILE_OFFSET_BITS')
7268        self.build()
7269
7270    def test_cross_libdir(self):
7271        # When cross compiling "libdir" should default to "lib"
7272        # rather than "lib/x86_64-linux-gnu" or something like that.
7273        testdir = os.path.join(self.common_test_dir, '1 trivial')
7274        self.init(testdir)
7275        for i in self.introspect('--buildoptions'):
7276            if i['name'] == 'libdir':
7277                self.assertEqual(i['value'], 'lib')
7278                return
7279        self.assertTrue(False, 'Option libdir not in introspect data.')
7280
7281    def test_cross_libdir_subproject(self):
7282        # Guard against a regression where calling "subproject"
7283        # would reset the value of libdir to its default value.
7284        testdir = os.path.join(self.unit_test_dir, '76 subdir libdir')
7285        self.init(testdir, extra_args=['--libdir=fuf'])
7286        for i in self.introspect('--buildoptions'):
7287            if i['name'] == 'libdir':
7288                self.assertEqual(i['value'], 'fuf')
7289                return
7290        self.assertTrue(False, 'Libdir specified on command line gets reset.')
7291
7292    def test_std_remains(self):
7293        # C_std defined in project options must be in effect also when cross compiling.
7294        testdir = os.path.join(self.unit_test_dir, '51 noncross options')
7295        self.init(testdir)
7296        compdb = self.get_compdb()
7297        self.assertRegex(compdb[0]['command'], '-std=c99')
7298        self.build()
7299
7300    @skipIfNoPkgconfig
7301    def test_pkg_config_option(self):
7302        if not shutil.which('arm-linux-gnueabihf-pkg-config'):
7303            raise unittest.SkipTest('Cross-pkgconfig not found.')
7304        testdir = os.path.join(self.unit_test_dir, '58 pkg_config_path option')
7305        self.init(testdir, extra_args=[
7306            '-Dbuild.pkg_config_path=' + os.path.join(testdir, 'build_extra_path'),
7307            '-Dpkg_config_path=' + os.path.join(testdir, 'host_extra_path'),
7308        ])
7309
7310
7311def should_run_cross_mingw_tests():
7312    return shutil.which('x86_64-w64-mingw32-gcc') and not (is_windows() or is_cygwin())
7313
7314@unittest.skipUnless(not is_windows() and should_run_cross_mingw_tests(), "requires ability to cross compile with MinGW")
7315class LinuxCrossMingwTests(BaseLinuxCrossTests):
7316    '''
7317    Tests that cross-compilation to Windows/MinGW works
7318    '''
7319
7320    def setUp(self):
7321        super().setUp()
7322        src_root = os.path.dirname(__file__)
7323        self.meson_cross_file = os.path.join(src_root, 'cross', 'linux-mingw-w64-64bit.txt')
7324
7325    def test_exe_wrapper_behaviour(self):
7326        '''
7327        Test that an exe wrapper that isn't found doesn't cause compiler sanity
7328        checks and compiler checks to fail, but causes configure to fail if it
7329        requires running a cross-built executable (custom_target or run_target)
7330        and causes the tests to be skipped if they are run.
7331        '''
7332        testdir = os.path.join(self.unit_test_dir, '36 exe_wrapper behaviour')
7333        # Configures, builds, and tests fine by default
7334        self.init(testdir)
7335        self.build()
7336        self.run_tests()
7337        self.wipe()
7338        os.mkdir(self.builddir)
7339        # Change cross file to use a non-existing exe_wrapper and it should fail
7340        self.meson_cross_file = os.path.join(testdir, 'broken-cross.txt')
7341        # Force tracebacks so we can detect them properly
7342        env = {'MESON_FORCE_BACKTRACE': '1'}
7343        with self.assertRaisesRegex(MesonException, 'exe_wrapper.*target.*use-exe-wrapper'):
7344            # Must run in-process or we'll get a generic CalledProcessError
7345            self.init(testdir, extra_args='-Drun-target=false',
7346                      inprocess=True,
7347                      override_envvars=env)
7348        with self.assertRaisesRegex(MesonException, 'exe_wrapper.*run target.*run-prog'):
7349            # Must run in-process or we'll get a generic CalledProcessError
7350            self.init(testdir, extra_args='-Dcustom-target=false',
7351                      inprocess=True,
7352                      override_envvars=env)
7353        self.init(testdir, extra_args=['-Dcustom-target=false', '-Drun-target=false'],
7354                  override_envvars=env)
7355        self.build()
7356        with self.assertRaisesRegex(MesonException, 'exe_wrapper.*PATH'):
7357            # Must run in-process or we'll get a generic CalledProcessError
7358            self.run_tests(inprocess=True, override_envvars=env)
7359
7360    @skipIfNoPkgconfig
7361    def test_cross_pkg_config_option(self):
7362        testdir = os.path.join(self.unit_test_dir, '58 pkg_config_path option')
7363        self.init(testdir, extra_args=[
7364            '-Dbuild.pkg_config_path=' + os.path.join(testdir, 'build_extra_path'),
7365            '-Dpkg_config_path=' + os.path.join(testdir, 'host_extra_path'),
7366        ])
7367
7368
7369class PythonTests(BasePlatformTests):
7370    '''
7371    Tests that verify compilation of python extension modules
7372    '''
7373
7374    def test_versions(self):
7375        if self.backend is not Backend.ninja:
7376            raise unittest.SkipTest('Skipping python tests with {} backend'.format(self.backend.name))
7377
7378        testdir = os.path.join(self.src_root, 'test cases', 'unit', '39 python extmodule')
7379
7380        # No python version specified, this will use meson's python
7381        self.init(testdir)
7382        self.build()
7383        self.run_tests()
7384        self.wipe()
7385
7386        # When specifying a known name, (python2 / python3) the module
7387        # will also try 'python' as a fallback and use it if the major
7388        # version matches
7389        try:
7390            self.init(testdir, extra_args=['-Dpython=python2'])
7391            self.build()
7392            self.run_tests()
7393        except unittest.SkipTest:
7394            # python2 is not necessarily installed on the test machine,
7395            # if it is not, or the python headers can't be found, the test
7396            # will raise MESON_SKIP_TEST, we could check beforehand what version
7397            # of python is available, but it's a bit of a chicken and egg situation,
7398            # as that is the job of the module, so we just ask for forgiveness rather
7399            # than permission.
7400            pass
7401
7402        self.wipe()
7403
7404        for py in ('pypy', 'pypy3'):
7405            try:
7406                self.init(testdir, extra_args=['-Dpython=%s' % py])
7407            except unittest.SkipTest:
7408                # Same as above, pypy2 and pypy3 are not expected to be present
7409                # on the test system, the test project only raises in these cases
7410                continue
7411
7412            # We have a pypy, this is expected to work
7413            self.build()
7414            self.run_tests()
7415            self.wipe()
7416
7417        # The test is configured to error out with MESON_SKIP_TEST
7418        # in case it could not find python
7419        with self.assertRaises(unittest.SkipTest):
7420            self.init(testdir, extra_args=['-Dpython=not-python'])
7421        self.wipe()
7422
7423        # While dir is an external command on both Windows and Linux,
7424        # it certainly isn't python
7425        with self.assertRaises(unittest.SkipTest):
7426            self.init(testdir, extra_args=['-Dpython=dir'])
7427        self.wipe()
7428
7429
7430class RewriterTests(BasePlatformTests):
7431    def setUp(self):
7432        super().setUp()
7433        self.maxDiff = None
7434
7435    def prime(self, dirname):
7436        copy_tree(os.path.join(self.rewrite_test_dir, dirname), self.builddir)
7437
7438    def rewrite_raw(self, directory, args):
7439        if isinstance(args, str):
7440            args = [args]
7441        command = self.rewrite_command + ['--verbose', '--skip', '--sourcedir', directory] + args
7442        p = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
7443                           universal_newlines=True, timeout=60)
7444        print('STDOUT:')
7445        print(p.stdout)
7446        print('STDERR:')
7447        print(p.stderr)
7448        if p.returncode != 0:
7449            if 'MESON_SKIP_TEST' in p.stdout:
7450                raise unittest.SkipTest('Project requested skipping.')
7451            raise subprocess.CalledProcessError(p.returncode, command, output=p.stdout)
7452        if not p.stderr:
7453            return {}
7454        return json.loads(p.stderr)
7455
7456    def rewrite(self, directory, args):
7457        if isinstance(args, str):
7458            args = [args]
7459        return self.rewrite_raw(directory, ['command'] + args)
7460
7461    def test_target_source_list(self):
7462        self.prime('1 basic')
7463        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7464        expected = {
7465            'target': {
7466                'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['main.cpp', 'fileA.cpp', 'fileB.cpp', 'fileC.cpp']},
7467                'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp', 'fileA.cpp']},
7468                'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileB.cpp', 'fileC.cpp']},
7469                'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp', 'fileA.cpp']},
7470                'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp', 'fileA.cpp']},
7471                'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileB.cpp', 'fileC.cpp']},
7472                'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp']},
7473                'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'main.cpp', 'fileA.cpp']},
7474                'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp', 'fileA.cpp']},
7475                'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp', 'fileA.cpp']},
7476            }
7477        }
7478        self.assertDictEqual(out, expected)
7479
7480    def test_target_add_sources(self):
7481        self.prime('1 basic')
7482        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addSrc.json'))
7483        expected = {
7484            'target': {
7485                'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp', 'a7.cpp', 'fileB.cpp', 'fileC.cpp']},
7486                'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp']},
7487                'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['a7.cpp', 'fileB.cpp', 'fileC.cpp']},
7488                'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['a5.cpp', 'fileA.cpp', 'main.cpp']},
7489                'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['a5.cpp', 'main.cpp', 'fileA.cpp']},
7490                'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['a3.cpp', 'main.cpp', 'a7.cpp', 'fileB.cpp', 'fileC.cpp']},
7491                'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp', 'a4.cpp']},
7492                'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp']},
7493                'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp']},
7494                'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp']},
7495            }
7496        }
7497        self.assertDictEqual(out, expected)
7498
7499        # Check the written file
7500        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7501        self.assertDictEqual(out, expected)
7502
7503    def test_target_add_sources_abs(self):
7504        self.prime('1 basic')
7505        abs_src = [os.path.join(self.builddir, x) for x in ['a1.cpp', 'a2.cpp', 'a6.cpp']]
7506        add = json.dumps([{"type": "target", "target": "trivialprog1", "operation": "src_add", "sources": abs_src}])
7507        inf = json.dumps([{"type": "target", "target": "trivialprog1", "operation": "info"}])
7508        self.rewrite(self.builddir, add)
7509        out = self.rewrite(self.builddir, inf)
7510        expected = {'target': {'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['a1.cpp', 'a2.cpp', 'a6.cpp', 'fileA.cpp', 'main.cpp']}}}
7511        self.assertDictEqual(out, expected)
7512
7513    def test_target_remove_sources(self):
7514        self.prime('1 basic')
7515        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'rmSrc.json'))
7516        expected = {
7517            'target': {
7518                'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['main.cpp', 'fileC.cpp']},
7519                'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp']},
7520                'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileC.cpp']},
7521                'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp']},
7522                'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp']},
7523                'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileC.cpp']},
7524                'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp']},
7525                'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileC.cpp', 'main.cpp']},
7526                'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp']},
7527                'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp']},
7528            }
7529        }
7530        self.assertDictEqual(out, expected)
7531
7532        # Check the written file
7533        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7534        self.assertDictEqual(out, expected)
7535
7536    def test_target_subdir(self):
7537        self.prime('2 subdirs')
7538        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addSrc.json'))
7539        expected = {'name': 'something', 'sources': ['first.c', 'second.c', 'third.c']}
7540        self.assertDictEqual(list(out['target'].values())[0], expected)
7541
7542        # Check the written file
7543        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7544        self.assertDictEqual(list(out['target'].values())[0], expected)
7545
7546    def test_target_remove(self):
7547        self.prime('1 basic')
7548        self.rewrite(self.builddir, os.path.join(self.builddir, 'rmTgt.json'))
7549        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7550
7551        expected = {
7552            'target': {
7553                'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileB.cpp', 'fileC.cpp']},
7554                'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp', 'fileA.cpp']},
7555                'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp', 'fileA.cpp']},
7556                'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileB.cpp', 'fileC.cpp']},
7557                'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp']},
7558                'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'main.cpp', 'fileA.cpp']},
7559                'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp', 'fileA.cpp']},
7560            }
7561        }
7562        self.assertDictEqual(out, expected)
7563
7564    def test_tatrget_add(self):
7565        self.prime('1 basic')
7566        self.rewrite(self.builddir, os.path.join(self.builddir, 'addTgt.json'))
7567        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7568
7569        expected = {
7570            'target': {
7571                'trivialprog0@exe': {'name': 'trivialprog0', 'sources': ['main.cpp', 'fileA.cpp', 'fileB.cpp', 'fileC.cpp']},
7572                'trivialprog1@exe': {'name': 'trivialprog1', 'sources': ['main.cpp', 'fileA.cpp']},
7573                'trivialprog2@exe': {'name': 'trivialprog2', 'sources': ['fileB.cpp', 'fileC.cpp']},
7574                'trivialprog3@exe': {'name': 'trivialprog3', 'sources': ['main.cpp', 'fileA.cpp']},
7575                'trivialprog4@exe': {'name': 'trivialprog4', 'sources': ['main.cpp', 'fileA.cpp']},
7576                'trivialprog5@exe': {'name': 'trivialprog5', 'sources': ['main.cpp', 'fileB.cpp', 'fileC.cpp']},
7577                'trivialprog6@exe': {'name': 'trivialprog6', 'sources': ['main.cpp', 'fileA.cpp']},
7578                'trivialprog7@exe': {'name': 'trivialprog7', 'sources': ['fileB.cpp', 'fileC.cpp', 'main.cpp', 'fileA.cpp']},
7579                'trivialprog8@exe': {'name': 'trivialprog8', 'sources': ['main.cpp', 'fileA.cpp']},
7580                'trivialprog9@exe': {'name': 'trivialprog9', 'sources': ['main.cpp', 'fileA.cpp']},
7581                'trivialprog10@sha': {'name': 'trivialprog10', 'sources': ['new1.cpp', 'new2.cpp']},
7582            }
7583        }
7584        self.assertDictEqual(out, expected)
7585
7586    def test_target_remove_subdir(self):
7587        self.prime('2 subdirs')
7588        self.rewrite(self.builddir, os.path.join(self.builddir, 'rmTgt.json'))
7589        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7590        self.assertDictEqual(out, {})
7591
7592    def test_target_add_subdir(self):
7593        self.prime('2 subdirs')
7594        self.rewrite(self.builddir, os.path.join(self.builddir, 'addTgt.json'))
7595        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7596        expected = {'name': 'something', 'sources': ['first.c', 'second.c']}
7597        self.assertDictEqual(out['target']['94b671c@@something@exe'], expected)
7598
7599    def test_target_source_sorting(self):
7600        self.prime('5 sorting')
7601        add_json = json.dumps([{'type': 'target', 'target': 'exe1', 'operation': 'src_add', 'sources': ['a666.c']}])
7602        inf_json = json.dumps([{'type': 'target', 'target': 'exe1', 'operation': 'info'}])
7603        out = self.rewrite(self.builddir, add_json)
7604        out = self.rewrite(self.builddir, inf_json)
7605        expected = {
7606            'target': {
7607                'exe1@exe': {
7608                    'name': 'exe1',
7609                    'sources': [
7610                        'aaa/a/a1.c',
7611                        'aaa/b/b1.c',
7612                        'aaa/b/b2.c',
7613                        'aaa/f1.c',
7614                        'aaa/f2.c',
7615                        'aaa/f3.c',
7616                        'bbb/a/b1.c',
7617                        'bbb/b/b2.c',
7618                        'bbb/c1/b5.c',
7619                        'bbb/c2/b7.c',
7620                        'bbb/c10/b6.c',
7621                        'bbb/a4.c',
7622                        'bbb/b3.c',
7623                        'bbb/b4.c',
7624                        'bbb/b5.c',
7625                        'a1.c',
7626                        'a2.c',
7627                        'a3.c',
7628                        'a10.c',
7629                        'a20.c',
7630                        'a30.c',
7631                        'a100.c',
7632                        'a101.c',
7633                        'a110.c',
7634                        'a210.c',
7635                        'a666.c',
7636                        'b1.c',
7637                        'c2.c'
7638                    ]
7639                }
7640            }
7641        }
7642        self.assertDictEqual(out, expected)
7643
7644    def test_target_same_name_skip(self):
7645        self.prime('4 same name targets')
7646        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'addSrc.json'))
7647        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7648        expected = {'name': 'myExe', 'sources': ['main.cpp']}
7649        self.assertEqual(len(out['target']), 2)
7650        for val in out['target'].values():
7651            self.assertDictEqual(expected, val)
7652
7653    def test_kwargs_info(self):
7654        self.prime('3 kwargs')
7655        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7656        expected = {
7657            'kwargs': {
7658                'project#/': {'version': '0.0.1'},
7659                'target#tgt1': {'build_by_default': True},
7660                'dependency#dep1': {'required': False}
7661            }
7662        }
7663        self.assertDictEqual(out, expected)
7664
7665    def test_kwargs_set(self):
7666        self.prime('3 kwargs')
7667        self.rewrite(self.builddir, os.path.join(self.builddir, 'set.json'))
7668        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7669        expected = {
7670            'kwargs': {
7671                'project#/': {'version': '0.0.2', 'meson_version': '0.50.0', 'license': ['GPL', 'MIT']},
7672                'target#tgt1': {'build_by_default': False, 'build_rpath': '/usr/local', 'dependencies': 'dep1'},
7673                'dependency#dep1': {'required': True, 'method': 'cmake'}
7674            }
7675        }
7676        self.assertDictEqual(out, expected)
7677
7678    def test_kwargs_add(self):
7679        self.prime('3 kwargs')
7680        self.rewrite(self.builddir, os.path.join(self.builddir, 'add.json'))
7681        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7682        expected = {
7683            'kwargs': {
7684                'project#/': {'version': '0.0.1', 'license': ['GPL', 'MIT', 'BSD']},
7685                'target#tgt1': {'build_by_default': True},
7686                'dependency#dep1': {'required': False}
7687            }
7688        }
7689        self.assertDictEqual(out, expected)
7690
7691    def test_kwargs_remove(self):
7692        self.prime('3 kwargs')
7693        self.rewrite(self.builddir, os.path.join(self.builddir, 'remove.json'))
7694        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7695        expected = {
7696            'kwargs': {
7697                'project#/': {'version': '0.0.1', 'license': 'GPL'},
7698                'target#tgt1': {'build_by_default': True},
7699                'dependency#dep1': {'required': False}
7700            }
7701        }
7702        self.assertDictEqual(out, expected)
7703
7704    def test_kwargs_remove_regex(self):
7705        self.prime('3 kwargs')
7706        self.rewrite(self.builddir, os.path.join(self.builddir, 'remove_regex.json'))
7707        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7708        expected = {
7709            'kwargs': {
7710                'project#/': {'version': '0.0.1', 'default_options': ['buildtype=release', 'debug=true']},
7711                'target#tgt1': {'build_by_default': True},
7712                'dependency#dep1': {'required': False}
7713            }
7714        }
7715        self.assertDictEqual(out, expected)
7716
7717    def test_kwargs_delete(self):
7718        self.prime('3 kwargs')
7719        self.rewrite(self.builddir, os.path.join(self.builddir, 'delete.json'))
7720        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7721        expected = {
7722            'kwargs': {
7723                'project#/': {},
7724                'target#tgt1': {},
7725                'dependency#dep1': {'required': False}
7726            }
7727        }
7728        self.assertDictEqual(out, expected)
7729
7730    def test_default_options_set(self):
7731        self.prime('3 kwargs')
7732        self.rewrite(self.builddir, os.path.join(self.builddir, 'defopts_set.json'))
7733        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7734        expected = {
7735            'kwargs': {
7736                'project#/': {'version': '0.0.1', 'default_options': ['buildtype=release', 'debug=True', 'cpp_std=c++11']},
7737                'target#tgt1': {'build_by_default': True},
7738                'dependency#dep1': {'required': False}
7739            }
7740        }
7741        self.assertDictEqual(out, expected)
7742
7743    def test_default_options_delete(self):
7744        self.prime('3 kwargs')
7745        self.rewrite(self.builddir, os.path.join(self.builddir, 'defopts_delete.json'))
7746        out = self.rewrite(self.builddir, os.path.join(self.builddir, 'info.json'))
7747        expected = {
7748            'kwargs': {
7749                'project#/': {'version': '0.0.1', 'default_options': ['cpp_std=c++14', 'debug=true']},
7750                'target#tgt1': {'build_by_default': True},
7751                'dependency#dep1': {'required': False}
7752            }
7753        }
7754        self.assertDictEqual(out, expected)
7755
7756class NativeFileTests(BasePlatformTests):
7757
7758    def setUp(self):
7759        super().setUp()
7760        self.testcase = os.path.join(self.unit_test_dir, '47 native file binary')
7761        self.current_config = 0
7762        self.current_wrapper = 0
7763
7764    def helper_create_native_file(self, values):
7765        """Create a config file as a temporary file.
7766
7767        values should be a nested dictionary structure of {section: {key:
7768        value}}
7769        """
7770        filename = os.path.join(self.builddir, 'generated{}.config'.format(self.current_config))
7771        self.current_config += 1
7772        with open(filename, 'wt') as f:
7773            for section, entries in values.items():
7774                f.write('[{}]\n'.format(section))
7775                for k, v in entries.items():
7776                    f.write("{}='{}'\n".format(k, v))
7777        return filename
7778
7779    def helper_create_binary_wrapper(self, binary, dir_=None, extra_args=None, **kwargs):
7780        """Creates a wrapper around a binary that overrides specific values."""
7781        filename = os.path.join(dir_ or self.builddir, 'binary_wrapper{}.py'.format(self.current_wrapper))
7782        extra_args = extra_args or {}
7783        self.current_wrapper += 1
7784        if is_haiku():
7785            chbang = '#!/bin/env python3'
7786        else:
7787            chbang = '#!/usr/bin/env python3'
7788
7789        with open(filename, 'wt') as f:
7790            f.write(textwrap.dedent('''\
7791                {}
7792                import argparse
7793                import subprocess
7794                import sys
7795
7796                def main():
7797                    parser = argparse.ArgumentParser()
7798                '''.format(chbang)))
7799            for name in chain(extra_args, kwargs):
7800                f.write('    parser.add_argument("-{0}", "--{0}", action="store_true")\n'.format(name))
7801            f.write('    args, extra_args = parser.parse_known_args()\n')
7802            for name, value in chain(extra_args.items(), kwargs.items()):
7803                f.write('    if args.{}:\n'.format(name))
7804                f.write('        print("{}", file=sys.{})\n'.format(value, kwargs.get('outfile', 'stdout')))
7805                f.write('        sys.exit(0)\n')
7806            f.write(textwrap.dedent('''
7807                    ret = subprocess.run(
7808                        ["{}"] + extra_args,
7809                        stdout=subprocess.PIPE,
7810                        stderr=subprocess.PIPE)
7811                    print(ret.stdout.decode('utf-8'))
7812                    print(ret.stderr.decode('utf-8'), file=sys.stderr)
7813                    sys.exit(ret.returncode)
7814
7815                if __name__ == '__main__':
7816                    main()
7817                '''.format(binary)))
7818
7819        if not is_windows():
7820            os.chmod(filename, 0o755)
7821            return filename
7822
7823        # On windows we need yet another level of indirection, as cmd cannot
7824        # invoke python files itself, so instead we generate a .bat file, which
7825        # invokes our python wrapper
7826        batfile = os.path.join(self.builddir, 'binary_wrapper{}.bat'.format(self.current_wrapper))
7827        with open(batfile, 'wt') as f:
7828            f.write(r'@{} {} %*'.format(sys.executable, filename))
7829        return batfile
7830
7831    def helper_for_compiler(self, lang, cb, for_machine = MachineChoice.HOST):
7832        """Helper for generating tests for overriding compilers for langaugages
7833        with more than one implementation, such as C, C++, ObjC, ObjC++, and D.
7834        """
7835        env = get_fake_env()
7836        getter = getattr(env, 'detect_{}_compiler'.format(lang))
7837        getter = functools.partial(getter, for_machine)
7838        cc = getter()
7839        binary, newid = cb(cc)
7840        env.binaries[for_machine].binaries[lang] = binary
7841        compiler = getter()
7842        self.assertEqual(compiler.id, newid)
7843
7844    def test_multiple_native_files_override(self):
7845        wrapper = self.helper_create_binary_wrapper('bash', version='foo')
7846        config = self.helper_create_native_file({'binaries': {'bash': wrapper}})
7847        wrapper = self.helper_create_binary_wrapper('bash', version='12345')
7848        config2 = self.helper_create_native_file({'binaries': {'bash': wrapper}})
7849        self.init(self.testcase, extra_args=[
7850            '--native-file', config, '--native-file', config2,
7851            '-Dcase=find_program'])
7852
7853    # This test hangs on cygwin.
7854    @unittest.skipIf(os.name != 'posix' or is_cygwin(), 'Uses fifos, which are not available on non Unix OSes.')
7855    def test_native_file_is_pipe(self):
7856        fifo = os.path.join(self.builddir, 'native.file')
7857        os.mkfifo(fifo)
7858        with tempfile.TemporaryDirectory() as d:
7859            wrapper = self.helper_create_binary_wrapper('bash', d, version='12345')
7860
7861            def filler():
7862                with open(fifo, 'w') as f:
7863                    f.write('[binaries]\n')
7864                    f.write("bash = '{}'\n".format(wrapper))
7865
7866            thread = threading.Thread(target=filler)
7867            thread.start()
7868
7869            self.init(self.testcase, extra_args=['--native-file', fifo, '-Dcase=find_program'])
7870
7871            thread.join()
7872            os.unlink(fifo)
7873
7874            self.init(self.testcase, extra_args=['--wipe'])
7875
7876    def test_multiple_native_files(self):
7877        wrapper = self.helper_create_binary_wrapper('bash', version='12345')
7878        config = self.helper_create_native_file({'binaries': {'bash': wrapper}})
7879        wrapper = self.helper_create_binary_wrapper('python')
7880        config2 = self.helper_create_native_file({'binaries': {'python': wrapper}})
7881        self.init(self.testcase, extra_args=[
7882            '--native-file', config, '--native-file', config2,
7883            '-Dcase=find_program'])
7884
7885    def _simple_test(self, case, binary, entry=None):
7886        wrapper = self.helper_create_binary_wrapper(binary, version='12345')
7887        config = self.helper_create_native_file({'binaries': {entry or binary: wrapper}})
7888        self.init(self.testcase, extra_args=['--native-file', config, '-Dcase={}'.format(case)])
7889
7890    def test_find_program(self):
7891        self._simple_test('find_program', 'bash')
7892
7893    def test_config_tool_dep(self):
7894        # Do the skip at this level to avoid screwing up the cache
7895        if mesonbuild.environment.detect_msys2_arch():
7896            raise unittest.SkipTest('Skipped due to problems with LLVM on MSYS2')
7897        if not shutil.which('llvm-config'):
7898            raise unittest.SkipTest('No llvm-installed, cannot test')
7899        self._simple_test('config_dep', 'llvm-config')
7900
7901    def test_python3_module(self):
7902        self._simple_test('python3', 'python3')
7903
7904    def test_python_module(self):
7905        if is_windows():
7906            # Bat adds extra crap to stdout, so the version check logic in the
7907            # python module breaks. This is fine on other OSes because they
7908            # don't need the extra indirection.
7909            raise unittest.SkipTest('bat indirection breaks internal sanity checks.')
7910        elif is_osx():
7911            binary = 'python'
7912        else:
7913            binary = 'python2'
7914
7915            # We not have python2, check for it
7916            for v in ['2', '2.7', '-2.7']:
7917                rc = subprocess.call(['pkg-config', '--cflags', 'python{}'.format(v)],
7918                                     stdout=subprocess.DEVNULL,
7919                                     stderr=subprocess.DEVNULL)
7920                if rc == 0:
7921                    break
7922            else:
7923                raise unittest.SkipTest('Not running Python 2 tests because dev packages not installed.')
7924        self._simple_test('python', binary, entry='python')
7925
7926    @unittest.skipIf(is_windows(), 'Setting up multiple compilers on windows is hard')
7927    @skip_if_env_set('CC')
7928    def test_c_compiler(self):
7929        def cb(comp):
7930            if comp.id == 'gcc':
7931                if not shutil.which('clang'):
7932                    raise unittest.SkipTest('Only one compiler found, cannot test.')
7933                return 'clang', 'clang'
7934            if not is_real_gnu_compiler(shutil.which('gcc')):
7935                raise unittest.SkipTest('Only one compiler found, cannot test.')
7936            return 'gcc', 'gcc'
7937        self.helper_for_compiler('c', cb)
7938
7939    @unittest.skipIf(is_windows(), 'Setting up multiple compilers on windows is hard')
7940    @skip_if_env_set('CXX')
7941    def test_cpp_compiler(self):
7942        def cb(comp):
7943            if comp.id == 'gcc':
7944                if not shutil.which('clang++'):
7945                    raise unittest.SkipTest('Only one compiler found, cannot test.')
7946                return 'clang++', 'clang'
7947            if not is_real_gnu_compiler(shutil.which('g++')):
7948                raise unittest.SkipTest('Only one compiler found, cannot test.')
7949            return 'g++', 'gcc'
7950        self.helper_for_compiler('cpp', cb)
7951
7952    @skip_if_not_language('objc')
7953    @skip_if_env_set('OBJC')
7954    def test_objc_compiler(self):
7955        def cb(comp):
7956            if comp.id == 'gcc':
7957                if not shutil.which('clang'):
7958                    raise unittest.SkipTest('Only one compiler found, cannot test.')
7959                return 'clang', 'clang'
7960            if not is_real_gnu_compiler(shutil.which('gcc')):
7961                raise unittest.SkipTest('Only one compiler found, cannot test.')
7962            return 'gcc', 'gcc'
7963        self.helper_for_compiler('objc', cb)
7964
7965    @skip_if_not_language('objcpp')
7966    @skip_if_env_set('OBJCXX')
7967    def test_objcpp_compiler(self):
7968        def cb(comp):
7969            if comp.id == 'gcc':
7970                if not shutil.which('clang++'):
7971                    raise unittest.SkipTest('Only one compiler found, cannot test.')
7972                return 'clang++', 'clang'
7973            if not is_real_gnu_compiler(shutil.which('g++')):
7974                raise unittest.SkipTest('Only one compiler found, cannot test.')
7975            return 'g++', 'gcc'
7976        self.helper_for_compiler('objcpp', cb)
7977
7978    @skip_if_not_language('d')
7979    @skip_if_env_set('DC')
7980    def test_d_compiler(self):
7981        def cb(comp):
7982            if comp.id == 'dmd':
7983                if shutil.which('ldc'):
7984                    return 'ldc', 'ldc'
7985                elif shutil.which('gdc'):
7986                    return 'gdc', 'gdc'
7987                else:
7988                    raise unittest.SkipTest('No alternative dlang compiler found.')
7989            if shutil.which('dmd'):
7990                return 'dmd', 'dmd'
7991            raise unittest.SkipTest('No alternative dlang compiler found.')
7992        self.helper_for_compiler('d', cb)
7993
7994    @skip_if_not_language('cs')
7995    @skip_if_env_set('CSC')
7996    def test_cs_compiler(self):
7997        def cb(comp):
7998            if comp.id == 'csc':
7999                if not shutil.which('mcs'):
8000                    raise unittest.SkipTest('No alternate C# implementation.')
8001                return 'mcs', 'mcs'
8002            if not shutil.which('csc'):
8003                raise unittest.SkipTest('No alternate C# implementation.')
8004            return 'csc', 'csc'
8005        self.helper_for_compiler('cs', cb)
8006
8007    @skip_if_not_language('fortran')
8008    @skip_if_env_set('FC')
8009    def test_fortran_compiler(self):
8010        def cb(comp):
8011            if comp.id == 'lcc':
8012                if shutil.which('lfortran'):
8013                    return 'lfortran', 'lcc'
8014                raise unittest.SkipTest('No alternate Fortran implementation.')
8015            elif comp.id == 'gcc':
8016                if shutil.which('ifort'):
8017                    # There is an ICC for windows (windows build, linux host),
8018                    # but we don't support that ATM so lets not worry about it.
8019                    if is_windows():
8020                        return 'ifort', 'intel-cl'
8021                    return 'ifort', 'intel'
8022                elif shutil.which('flang'):
8023                    return 'flang', 'flang'
8024                elif shutil.which('pgfortran'):
8025                    return 'pgfortran', 'pgi'
8026                # XXX: there are several other fortran compilers meson
8027                # supports, but I don't have any of them to test with
8028                raise unittest.SkipTest('No alternate Fortran implementation.')
8029            if not shutil.which('gfortran'):
8030                raise unittest.SkipTest('No alternate Fortran implementation.')
8031            return 'gfortran', 'gcc'
8032        self.helper_for_compiler('fortran', cb)
8033
8034    def _single_implementation_compiler(self, lang, binary, version_str, version):
8035        """Helper for languages with a single (supported) implementation.
8036
8037        Builds a wrapper around the compiler to override the version.
8038        """
8039        wrapper = self.helper_create_binary_wrapper(binary, version=version_str)
8040        env = get_fake_env()
8041        getter = getattr(env, 'detect_{}_compiler'.format(lang))
8042        getter = functools.partial(getter, MachineChoice.HOST)
8043        env.binaries.host.binaries[lang] = wrapper
8044        compiler = getter()
8045        self.assertEqual(compiler.version, version)
8046
8047    @skip_if_not_language('vala')
8048    @skip_if_env_set('VALAC')
8049    def test_vala_compiler(self):
8050        self._single_implementation_compiler(
8051            'vala', 'valac', 'Vala 1.2345', '1.2345')
8052
8053    @skip_if_not_language('rust')
8054    @skip_if_env_set('RUSTC')
8055    def test_rust_compiler(self):
8056        self._single_implementation_compiler(
8057            'rust', 'rustc', 'rustc 1.2345', '1.2345')
8058
8059    @skip_if_not_language('java')
8060    def test_java_compiler(self):
8061        self._single_implementation_compiler(
8062            'java', 'javac', 'javac 9.99.77', '9.99.77')
8063
8064    @skip_if_not_language('swift')
8065    def test_swift_compiler(self):
8066        wrapper = self.helper_create_binary_wrapper(
8067            'swiftc', version='Swift 1.2345', outfile='stderr',
8068            extra_args={'Xlinker': 'macosx_version. PROJECT:ld - 1.2.3'})
8069        env = get_fake_env()
8070        env.binaries.host.binaries['swift'] = wrapper
8071        compiler = env.detect_swift_compiler(MachineChoice.HOST)
8072        self.assertEqual(compiler.version, '1.2345')
8073
8074    def test_native_file_dirs(self):
8075        testcase = os.path.join(self.unit_test_dir, '60 native file override')
8076        self.init(testcase, default_args=False,
8077                  extra_args=['--native-file', os.path.join(testcase, 'nativefile')])
8078
8079    def test_native_file_dirs_overriden(self):
8080        testcase = os.path.join(self.unit_test_dir, '60 native file override')
8081        self.init(testcase, default_args=False,
8082                  extra_args=['--native-file', os.path.join(testcase, 'nativefile'),
8083                              '-Ddef_libdir=liblib', '-Dlibdir=liblib'])
8084
8085    def test_compile_sys_path(self):
8086        """Compiling with a native file stored in a system path works.
8087
8088        There was a bug which caused the paths to be stored incorrectly and
8089        would result in ninja invoking meson in an infinite loop. This tests
8090        for that by actually invoking ninja.
8091        """
8092        testcase = os.path.join(self.common_test_dir, '1 trivial')
8093
8094        # It really doesn't matter what's in the native file, just that it exists
8095        config = self.helper_create_native_file({'binaries': {'bash': 'false'}})
8096
8097        self.init(testcase, extra_args=['--native-file', config])
8098        self.build()
8099
8100
8101class CrossFileTests(BasePlatformTests):
8102
8103    """Tests for cross file functionality not directly related to
8104    cross compiling.
8105
8106    This is mainly aimed to testing overrides from cross files.
8107    """
8108
8109    def _cross_file_generator(self, *, needs_exe_wrapper: bool = False,
8110                              exe_wrapper: T.Optional[T.List[str]] = None) -> str:
8111        if is_windows():
8112            raise unittest.SkipTest('Cannot run this test on non-mingw/non-cygwin windows')
8113        if is_sunos():
8114            cc = 'gcc'
8115        else:
8116            cc = 'cc'
8117
8118        return textwrap.dedent("""\
8119            [binaries]
8120            c = '/usr/bin/{}'
8121            ar = '/usr/bin/ar'
8122            strip = '/usr/bin/ar'
8123            {}
8124
8125            [properties]
8126            needs_exe_wrapper = {}
8127
8128            [host_machine]
8129            system = 'linux'
8130            cpu_family = 'x86'
8131            cpu = 'i686'
8132            endian = 'little'
8133            """.format(cc,
8134                       'exe_wrapper = {}'.format(str(exe_wrapper)) if exe_wrapper is not None else '',
8135                       needs_exe_wrapper))
8136
8137    def _stub_exe_wrapper(self) -> str:
8138        return textwrap.dedent('''\
8139            #!/usr/bin/env python3
8140            import subprocess
8141            import sys
8142
8143            sys.exit(subprocess.run(sys.argv[1:]).returncode)
8144            ''')
8145
8146    def test_needs_exe_wrapper_true(self):
8147        testdir = os.path.join(self.unit_test_dir, '72 cross test passed')
8148        with tempfile.TemporaryDirectory() as d:
8149            p = Path(d) / 'crossfile'
8150            with p.open('wt') as f:
8151                f.write(self._cross_file_generator(needs_exe_wrapper=True))
8152            self.init(testdir, extra_args=['--cross-file=' + str(p)])
8153            out = self.run_target('test')
8154            self.assertRegex(out, r'Skipped:\s*1\s*\n')
8155
8156    def test_needs_exe_wrapper_false(self):
8157        testdir = os.path.join(self.unit_test_dir, '72 cross test passed')
8158        with tempfile.TemporaryDirectory() as d:
8159            p = Path(d) / 'crossfile'
8160            with p.open('wt') as f:
8161                f.write(self._cross_file_generator(needs_exe_wrapper=False))
8162            self.init(testdir, extra_args=['--cross-file=' + str(p)])
8163            out = self.run_target('test')
8164            self.assertNotRegex(out, r'Skipped:\s*1\n')
8165
8166    def test_needs_exe_wrapper_true_wrapper(self):
8167        testdir = os.path.join(self.unit_test_dir, '72 cross test passed')
8168        with tempfile.TemporaryDirectory() as d:
8169            s = Path(d) / 'wrapper.py'
8170            with s.open('wt') as f:
8171                f.write(self._stub_exe_wrapper())
8172            s.chmod(0o774)
8173            p = Path(d) / 'crossfile'
8174            with p.open('wt') as f:
8175                f.write(self._cross_file_generator(
8176                    needs_exe_wrapper=True,
8177                    exe_wrapper=[str(s)]))
8178
8179            self.init(testdir, extra_args=['--cross-file=' + str(p), '-Dexpect=true'])
8180            out = self.run_target('test')
8181            self.assertRegex(out, r'Ok:\s*3\s*\n')
8182
8183    def test_cross_exe_passed_no_wrapper(self):
8184        testdir = os.path.join(self.unit_test_dir, '72 cross test passed')
8185        with tempfile.TemporaryDirectory() as d:
8186            p = Path(d) / 'crossfile'
8187            with p.open('wt') as f:
8188                f.write(self._cross_file_generator(needs_exe_wrapper=True))
8189
8190            self.init(testdir, extra_args=['--cross-file=' + str(p)])
8191            self.build()
8192            out = self.run_target('test')
8193            self.assertRegex(out, r'Skipped:\s*1\s*\n')
8194
8195    # The test uses mocking and thus requires that the current process is the
8196    # one to run the Meson steps. If we are using an external test executable
8197    # (most commonly in Debian autopkgtests) then the mocking won't work.
8198    @unittest.skipIf('MESON_EXE' in os.environ, 'MESON_EXE is defined, can not use mocking.')
8199    def test_cross_file_system_paths(self):
8200        if is_windows():
8201            raise unittest.SkipTest('system crossfile paths not defined for Windows (yet)')
8202
8203        testdir = os.path.join(self.common_test_dir, '1 trivial')
8204        cross_content = self._cross_file_generator()
8205        with tempfile.TemporaryDirectory() as d:
8206            dir_ = os.path.join(d, 'meson', 'cross')
8207            os.makedirs(dir_)
8208            with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f:
8209                f.write(cross_content)
8210            name = os.path.basename(f.name)
8211
8212            with mock.patch.dict(os.environ, {'XDG_DATA_HOME': d}):
8213                self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
8214                self.wipe()
8215
8216            with mock.patch.dict(os.environ, {'XDG_DATA_DIRS': d}):
8217                os.environ.pop('XDG_DATA_HOME', None)
8218                self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
8219                self.wipe()
8220
8221        with tempfile.TemporaryDirectory() as d:
8222            dir_ = os.path.join(d, '.local', 'share', 'meson', 'cross')
8223            os.makedirs(dir_)
8224            with tempfile.NamedTemporaryFile('w', dir=dir_, delete=False) as f:
8225                f.write(cross_content)
8226            name = os.path.basename(f.name)
8227
8228            # If XDG_DATA_HOME is set in the environment running the
8229            # tests this test will fail, os mock the environment, pop
8230            # it, then test
8231            with mock.patch.dict(os.environ):
8232                os.environ.pop('XDG_DATA_HOME', None)
8233                with mock.patch('mesonbuild.coredata.os.path.expanduser', lambda x: x.replace('~', d)):
8234                    self.init(testdir, extra_args=['--cross-file=' + name], inprocess=True)
8235                    self.wipe()
8236
8237    def test_cross_file_dirs(self):
8238        testcase = os.path.join(self.unit_test_dir, '60 native file override')
8239        self.init(testcase, default_args=False,
8240                  extra_args=['--native-file', os.path.join(testcase, 'nativefile'),
8241                              '--cross-file', os.path.join(testcase, 'crossfile'),
8242                              '-Ddef_bindir=binbar',
8243                              '-Ddef_datadir=databar',
8244                              '-Ddef_includedir=includebar',
8245                              '-Ddef_infodir=infobar',
8246                              '-Ddef_libdir=libbar',
8247                              '-Ddef_libexecdir=libexecbar',
8248                              '-Ddef_localedir=localebar',
8249                              '-Ddef_localstatedir=localstatebar',
8250                              '-Ddef_mandir=manbar',
8251                              '-Ddef_sbindir=sbinbar',
8252                              '-Ddef_sharedstatedir=sharedstatebar',
8253                              '-Ddef_sysconfdir=sysconfbar'])
8254
8255    def test_cross_file_dirs_overriden(self):
8256        testcase = os.path.join(self.unit_test_dir, '60 native file override')
8257        self.init(testcase, default_args=False,
8258                  extra_args=['--native-file', os.path.join(testcase, 'nativefile'),
8259                              '--cross-file', os.path.join(testcase, 'crossfile'),
8260                              '-Ddef_libdir=liblib', '-Dlibdir=liblib',
8261                              '-Ddef_bindir=binbar',
8262                              '-Ddef_datadir=databar',
8263                              '-Ddef_includedir=includebar',
8264                              '-Ddef_infodir=infobar',
8265                              '-Ddef_libexecdir=libexecbar',
8266                              '-Ddef_localedir=localebar',
8267                              '-Ddef_localstatedir=localstatebar',
8268                              '-Ddef_mandir=manbar',
8269                              '-Ddef_sbindir=sbinbar',
8270                              '-Ddef_sharedstatedir=sharedstatebar',
8271                              '-Ddef_sysconfdir=sysconfbar'])
8272
8273    def test_cross_file_dirs_chain(self):
8274        # crossfile2 overrides crossfile overrides nativefile
8275        testcase = os.path.join(self.unit_test_dir, '60 native file override')
8276        self.init(testcase, default_args=False,
8277                  extra_args=['--native-file', os.path.join(testcase, 'nativefile'),
8278                              '--cross-file', os.path.join(testcase, 'crossfile'),
8279                              '--cross-file', os.path.join(testcase, 'crossfile2'),
8280                              '-Ddef_bindir=binbar2',
8281                              '-Ddef_datadir=databar',
8282                              '-Ddef_includedir=includebar',
8283                              '-Ddef_infodir=infobar',
8284                              '-Ddef_libdir=libbar',
8285                              '-Ddef_libexecdir=libexecbar',
8286                              '-Ddef_localedir=localebar',
8287                              '-Ddef_localstatedir=localstatebar',
8288                              '-Ddef_mandir=manbar',
8289                              '-Ddef_sbindir=sbinbar',
8290                              '-Ddef_sharedstatedir=sharedstatebar',
8291                              '-Ddef_sysconfdir=sysconfbar'])
8292
8293class TAPParserTests(unittest.TestCase):
8294    def assert_test(self, events, **kwargs):
8295        if 'explanation' not in kwargs:
8296            kwargs['explanation'] = None
8297        self.assertEqual(next(events), TAPParser.Test(**kwargs))
8298
8299    def assert_plan(self, events, **kwargs):
8300        if 'skipped' not in kwargs:
8301            kwargs['skipped'] = False
8302        if 'explanation' not in kwargs:
8303            kwargs['explanation'] = None
8304        self.assertEqual(next(events), TAPParser.Plan(**kwargs))
8305
8306    def assert_version(self, events, **kwargs):
8307        self.assertEqual(next(events), TAPParser.Version(**kwargs))
8308
8309    def assert_error(self, events):
8310        self.assertEqual(type(next(events)), TAPParser.Error)
8311
8312    def assert_bailout(self, events, **kwargs):
8313        self.assertEqual(next(events), TAPParser.Bailout(**kwargs))
8314
8315    def assert_last(self, events):
8316        with self.assertRaises(StopIteration):
8317            next(events)
8318
8319    def parse_tap(self, s):
8320        parser = TAPParser(io.StringIO(s))
8321        return iter(parser.parse())
8322
8323    def parse_tap_v13(self, s):
8324        events = self.parse_tap('TAP version 13\n' + s)
8325        self.assert_version(events, version=13)
8326        return events
8327
8328    def test_empty(self):
8329        events = self.parse_tap('')
8330        self.assert_last(events)
8331
8332    def test_empty_plan(self):
8333        events = self.parse_tap('1..0')
8334        self.assert_plan(events, count=0, late=False, skipped=True)
8335        self.assert_last(events)
8336
8337    def test_plan_directive(self):
8338        events = self.parse_tap('1..0 # skipped for some reason')
8339        self.assert_plan(events, count=0, late=False, skipped=True,
8340                         explanation='for some reason')
8341        self.assert_last(events)
8342
8343        events = self.parse_tap('1..1 # skipped for some reason\nok 1')
8344        self.assert_error(events)
8345        self.assert_plan(events, count=1, late=False, skipped=True,
8346                         explanation='for some reason')
8347        self.assert_test(events, number=1, name='', result=TestResult.OK)
8348        self.assert_last(events)
8349
8350        events = self.parse_tap('1..1 # todo not supported here\nok 1')
8351        self.assert_error(events)
8352        self.assert_plan(events, count=1, late=False, skipped=False,
8353                         explanation='not supported here')
8354        self.assert_test(events, number=1, name='', result=TestResult.OK)
8355        self.assert_last(events)
8356
8357    def test_one_test_ok(self):
8358        events = self.parse_tap('ok')
8359        self.assert_test(events, number=1, name='', result=TestResult.OK)
8360        self.assert_last(events)
8361
8362    def test_one_test_with_number(self):
8363        events = self.parse_tap('ok 1')
8364        self.assert_test(events, number=1, name='', result=TestResult.OK)
8365        self.assert_last(events)
8366
8367    def test_one_test_with_name(self):
8368        events = self.parse_tap('ok 1 abc')
8369        self.assert_test(events, number=1, name='abc', result=TestResult.OK)
8370        self.assert_last(events)
8371
8372    def test_one_test_not_ok(self):
8373        events = self.parse_tap('not ok')
8374        self.assert_test(events, number=1, name='', result=TestResult.FAIL)
8375        self.assert_last(events)
8376
8377    def test_one_test_todo(self):
8378        events = self.parse_tap('not ok 1 abc # TODO')
8379        self.assert_test(events, number=1, name='abc', result=TestResult.EXPECTEDFAIL)
8380        self.assert_last(events)
8381
8382        events = self.parse_tap('ok 1 abc # TODO')
8383        self.assert_test(events, number=1, name='abc', result=TestResult.UNEXPECTEDPASS)
8384        self.assert_last(events)
8385
8386    def test_one_test_skip(self):
8387        events = self.parse_tap('ok 1 abc # SKIP')
8388        self.assert_test(events, number=1, name='abc', result=TestResult.SKIP)
8389        self.assert_last(events)
8390
8391    def test_one_test_skip_failure(self):
8392        events = self.parse_tap('not ok 1 abc # SKIP')
8393        self.assert_test(events, number=1, name='abc', result=TestResult.FAIL)
8394        self.assert_last(events)
8395
8396    def test_many_early_plan(self):
8397        events = self.parse_tap('1..4\nok 1\nnot ok 2\nok 3\nnot ok 4')
8398        self.assert_plan(events, count=4, late=False)
8399        self.assert_test(events, number=1, name='', result=TestResult.OK)
8400        self.assert_test(events, number=2, name='', result=TestResult.FAIL)
8401        self.assert_test(events, number=3, name='', result=TestResult.OK)
8402        self.assert_test(events, number=4, name='', result=TestResult.FAIL)
8403        self.assert_last(events)
8404
8405    def test_many_late_plan(self):
8406        events = self.parse_tap('ok 1\nnot ok 2\nok 3\nnot ok 4\n1..4')
8407        self.assert_test(events, number=1, name='', result=TestResult.OK)
8408        self.assert_test(events, number=2, name='', result=TestResult.FAIL)
8409        self.assert_test(events, number=3, name='', result=TestResult.OK)
8410        self.assert_test(events, number=4, name='', result=TestResult.FAIL)
8411        self.assert_plan(events, count=4, late=True)
8412        self.assert_last(events)
8413
8414    def test_directive_case(self):
8415        events = self.parse_tap('ok 1 abc # skip')
8416        self.assert_test(events, number=1, name='abc', result=TestResult.SKIP)
8417        self.assert_last(events)
8418
8419        events = self.parse_tap('ok 1 abc # ToDo')
8420        self.assert_test(events, number=1, name='abc', result=TestResult.UNEXPECTEDPASS)
8421        self.assert_last(events)
8422
8423    def test_directive_explanation(self):
8424        events = self.parse_tap('ok 1 abc # skip why')
8425        self.assert_test(events, number=1, name='abc', result=TestResult.SKIP,
8426                         explanation='why')
8427        self.assert_last(events)
8428
8429        events = self.parse_tap('ok 1 abc # ToDo Because')
8430        self.assert_test(events, number=1, name='abc', result=TestResult.UNEXPECTEDPASS,
8431                         explanation='Because')
8432        self.assert_last(events)
8433
8434    def test_one_test_early_plan(self):
8435        events = self.parse_tap('1..1\nok')
8436        self.assert_plan(events, count=1, late=False)
8437        self.assert_test(events, number=1, name='', result=TestResult.OK)
8438        self.assert_last(events)
8439
8440    def test_one_test_late_plan(self):
8441        events = self.parse_tap('ok\n1..1')
8442        self.assert_test(events, number=1, name='', result=TestResult.OK)
8443        self.assert_plan(events, count=1, late=True)
8444        self.assert_last(events)
8445
8446    def test_out_of_order(self):
8447        events = self.parse_tap('ok 2')
8448        self.assert_error(events)
8449        self.assert_test(events, number=2, name='', result=TestResult.OK)
8450        self.assert_last(events)
8451
8452    def test_middle_plan(self):
8453        events = self.parse_tap('ok 1\n1..2\nok 2')
8454        self.assert_test(events, number=1, name='', result=TestResult.OK)
8455        self.assert_plan(events, count=2, late=True)
8456        self.assert_error(events)
8457        self.assert_test(events, number=2, name='', result=TestResult.OK)
8458        self.assert_last(events)
8459
8460    def test_too_many_plans(self):
8461        events = self.parse_tap('1..1\n1..2\nok 1')
8462        self.assert_plan(events, count=1, late=False)
8463        self.assert_error(events)
8464        self.assert_test(events, number=1, name='', result=TestResult.OK)
8465        self.assert_last(events)
8466
8467    def test_too_many(self):
8468        events = self.parse_tap('ok 1\nnot ok 2\n1..1')
8469        self.assert_test(events, number=1, name='', result=TestResult.OK)
8470        self.assert_test(events, number=2, name='', result=TestResult.FAIL)
8471        self.assert_plan(events, count=1, late=True)
8472        self.assert_error(events)
8473        self.assert_last(events)
8474
8475        events = self.parse_tap('1..1\nok 1\nnot ok 2')
8476        self.assert_plan(events, count=1, late=False)
8477        self.assert_test(events, number=1, name='', result=TestResult.OK)
8478        self.assert_test(events, number=2, name='', result=TestResult.FAIL)
8479        self.assert_error(events)
8480        self.assert_last(events)
8481
8482    def test_too_few(self):
8483        events = self.parse_tap('ok 1\nnot ok 2\n1..3')
8484        self.assert_test(events, number=1, name='', result=TestResult.OK)
8485        self.assert_test(events, number=2, name='', result=TestResult.FAIL)
8486        self.assert_plan(events, count=3, late=True)
8487        self.assert_error(events)
8488        self.assert_last(events)
8489
8490        events = self.parse_tap('1..3\nok 1\nnot ok 2')
8491        self.assert_plan(events, count=3, late=False)
8492        self.assert_test(events, number=1, name='', result=TestResult.OK)
8493        self.assert_test(events, number=2, name='', result=TestResult.FAIL)
8494        self.assert_error(events)
8495        self.assert_last(events)
8496
8497    def test_too_few_bailout(self):
8498        events = self.parse_tap('1..3\nok 1\nnot ok 2\nBail out! no third test')
8499        self.assert_plan(events, count=3, late=False)
8500        self.assert_test(events, number=1, name='', result=TestResult.OK)
8501        self.assert_test(events, number=2, name='', result=TestResult.FAIL)
8502        self.assert_bailout(events, message='no third test')
8503        self.assert_last(events)
8504
8505    def test_diagnostics(self):
8506        events = self.parse_tap('1..1\n# ignored\nok 1')
8507        self.assert_plan(events, count=1, late=False)
8508        self.assert_test(events, number=1, name='', result=TestResult.OK)
8509        self.assert_last(events)
8510
8511        events = self.parse_tap('# ignored\n1..1\nok 1\n# ignored too')
8512        self.assert_plan(events, count=1, late=False)
8513        self.assert_test(events, number=1, name='', result=TestResult.OK)
8514        self.assert_last(events)
8515
8516        events = self.parse_tap('# ignored\nok 1\n1..1\n# ignored too')
8517        self.assert_test(events, number=1, name='', result=TestResult.OK)
8518        self.assert_plan(events, count=1, late=True)
8519        self.assert_last(events)
8520
8521    def test_empty_line(self):
8522        events = self.parse_tap('1..1\n\nok 1')
8523        self.assert_plan(events, count=1, late=False)
8524        self.assert_test(events, number=1, name='', result=TestResult.OK)
8525        self.assert_last(events)
8526
8527    def test_unexpected(self):
8528        events = self.parse_tap('1..1\ninvalid\nok 1')
8529        self.assert_plan(events, count=1, late=False)
8530        self.assert_error(events)
8531        self.assert_test(events, number=1, name='', result=TestResult.OK)
8532        self.assert_last(events)
8533
8534    def test_version(self):
8535        events = self.parse_tap('TAP version 13\n')
8536        self.assert_version(events, version=13)
8537        self.assert_last(events)
8538
8539        events = self.parse_tap('TAP version 12\n')
8540        self.assert_error(events)
8541        self.assert_last(events)
8542
8543        events = self.parse_tap('1..0\nTAP version 13\n')
8544        self.assert_plan(events, count=0, late=False, skipped=True)
8545        self.assert_error(events)
8546        self.assert_last(events)
8547
8548    def test_yaml(self):
8549        events = self.parse_tap_v13('ok\n ---\n foo: abc\n  bar: def\n ...\nok 2')
8550        self.assert_test(events, number=1, name='', result=TestResult.OK)
8551        self.assert_test(events, number=2, name='', result=TestResult.OK)
8552        self.assert_last(events)
8553
8554        events = self.parse_tap_v13('ok\n ---\n foo: abc\n  bar: def')
8555        self.assert_test(events, number=1, name='', result=TestResult.OK)
8556        self.assert_error(events)
8557        self.assert_last(events)
8558
8559        events = self.parse_tap_v13('ok 1\n ---\n foo: abc\n  bar: def\nnot ok 2')
8560        self.assert_test(events, number=1, name='', result=TestResult.OK)
8561        self.assert_error(events)
8562        self.assert_test(events, number=2, name='', result=TestResult.FAIL)
8563        self.assert_last(events)
8564
8565
8566def _clang_at_least(compiler, minver: str, apple_minver: str) -> bool:
8567    """
8568    check that Clang compiler is at least a specified version, whether AppleClang or regular Clang
8569
8570    Parameters
8571    ----------
8572    compiler:
8573        Meson compiler object
8574    minver: str
8575        Clang minimum version
8576    apple_minver: str
8577        AppleCLang minimum version
8578
8579    Returns
8580    -------
8581    at_least: bool
8582        Clang is at least the specified version
8583    """
8584    if isinstance(compiler, (mesonbuild.compilers.AppleClangCCompiler,
8585                             mesonbuild.compilers.AppleClangCPPCompiler)):
8586        return version_compare(compiler.version, apple_minver)
8587    return version_compare(compiler.version, minver)
8588
8589
8590def unset_envs():
8591    # For unit tests we must fully control all command lines
8592    # so that there are no unexpected changes coming from the
8593    # environment, for example when doing a package build.
8594    varnames = ['CPPFLAGS', 'LDFLAGS'] + list(mesonbuild.compilers.compilers.cflags_mapping.values())
8595    for v in varnames:
8596        if v in os.environ:
8597            del os.environ[v]
8598
8599def convert_args(argv):
8600    # If we got passed a list of tests, pass it on
8601    pytest_args = ['-v'] if '-v' in argv else []
8602    test_list = []
8603    for arg in argv:
8604        if arg.startswith('-'):
8605            if arg in ('-f', '--failfast'):
8606                arg = '--exitfirst'
8607            pytest_args.append(arg)
8608            continue
8609        # ClassName.test_name => 'ClassName and test_name'
8610        if '.' in arg:
8611            arg = ' and '.join(arg.split('.'))
8612        test_list.append(arg)
8613    if test_list:
8614        pytest_args += ['-k', ' or '.join(test_list)]
8615    return pytest_args
8616
8617def main():
8618    unset_envs()
8619    try:
8620        import pytest # noqa: F401
8621        # Need pytest-xdist for `-n` arg
8622        import xdist # noqa: F401
8623        pytest_args = ['-n', 'auto', './run_unittests.py']
8624        pytest_args += convert_args(sys.argv[1:])
8625        return subprocess.run(python_command + ['-m', 'pytest'] + pytest_args).returncode
8626    except ImportError:
8627        print('pytest-xdist not found, using unittest instead')
8628    # All attempts at locating pytest failed, fall back to plain unittest.
8629    cases = ['InternalTests', 'DataTests', 'AllPlatformTests', 'FailureTests',
8630             'PythonTests', 'NativeFileTests', 'RewriterTests', 'CrossFileTests',
8631             'TAPParserTests',
8632
8633             'LinuxlikeTests', 'LinuxCrossArmTests', 'LinuxCrossMingwTests',
8634             'WindowsTests', 'DarwinTests']
8635
8636    return unittest.main(defaultTest=cases, buffer=True)
8637
8638if __name__ == '__main__':
8639    print('Meson build system', mesonbuild.coredata.version, 'Unit Tests')
8640    raise SystemExit(main())
8641