# SPDX-License-Identifier: Apache-2.0
# Copyright 2016-2021 The Meson development team

from configparser import ConfigParser
from pathlib import Path
from unittest import mock
import argparse
import contextlib
import io
import json
import operator
import os
import pickle
import stat
import subprocess
import tempfile
import textwrap
import typing as T
import unittest

import mesonbuild.mlog
import mesonbuild.depfile
import mesonbuild.dependencies.base
import mesonbuild.dependencies.factory
import mesonbuild.envconfig
import mesonbuild.environment
import mesonbuild.modules.gnome
import mesonbuild.scripts.env2mfile
from mesonbuild import coredata
from mesonbuild.compilers.c import ClangCCompiler, GnuCCompiler
from mesonbuild.compilers.cpp import VisualStudioCPPCompiler
from mesonbuild.compilers.d import DmdDCompiler
from mesonbuild.compilers.detect import detect_c_compiler
from mesonbuild.compilers.mixins.visualstudio import MSVCCompiler, ClangClCompiler
from mesonbuild.linkers import linkers
from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, ObjectHolder
from mesonbuild.interpreterbase import typed_pos_args, InvalidArguments, typed_kwargs, ContainerTypeInfo, KwargInfo
from mesonbuild.mesonlib import (
    LibType, MachineChoice, PerMachine, Version, is_windows, is_osx,
    is_cygwin, is_openbsd, search_version, MesonException, python_command,
)
from mesonbuild.options import OptionKey
from mesonbuild.interpreter.type_checking import in_set_validator, NoneType
from mesonbuild.dependencies.pkgconfig import PkgConfigDependency, PkgConfigInterface, PkgConfigCLI
from mesonbuild.programs import ExternalProgram
import mesonbuild.modules.pkgconfig
from mesonbuild import utils

from run_tests import get_fake_env, get_fake_options

from .helpers import *

class InternalTests(unittest.TestCase):

    def test_version_number(self):
        self.assertEqual(search_version('foobar 1.2.3'), '1.2.3')
        self.assertEqual(search_version('1.2.3'), '1.2.3')
        self.assertEqual(search_version('foobar 2016.10.28 1.2.3'), '1.2.3')
        self.assertEqual(search_version('2016.10.28 1.2.3'), '1.2.3')
        self.assertEqual(search_version('foobar 2016.10.128'), '2016.10.128')
        self.assertEqual(search_version('2016.10.128'), '2016.10.128')
        self.assertEqual(search_version('2016.10'), '2016.10')
        self.assertEqual(search_version('2016.10 1.2.3'), '1.2.3')
        self.assertEqual(search_version('oops v1.2.3'), '1.2.3')
        self.assertEqual(search_version('2016.oops 1.2.3'), '1.2.3')
        self.assertEqual(search_version('2016.x'), 'unknown version')
        self.assertEqual(search_version(r'something version is \033[32;2m1.2.0\033[0m.'), '1.2.0')

        # Literal output of mvn
        self.assertEqual(search_version(r'''\
            \033[1mApache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d)\033[0m
            Maven home: /nix/store/g84a9wnid2h1d3z2wfydy16dky73wh7i-apache-maven-3.8.1/maven
            Java version: 11.0.10, vendor: Oracle Corporation, runtime: /nix/store/afsnl4ahmm9svvl7s1a0cj41vw4nkmz4-openjdk-11.0.10+9/lib/openjdk
            Default locale: en_US, platform encoding: UTF-8
            OS name: "linux", version: "5.12.17", arch: "amd64", family: "unix"'''),
            '3.8.1')

    def test_mode_symbolic_to_bits(self):
        modefunc = mesonbuild.mesonlib.FileMode.perms_s_to_bits
        self.assertEqual(modefunc('---------'), 0)
        self.assertEqual(modefunc('r--------'), stat.S_IRUSR)
        self.assertEqual(modefunc('---r-----'), stat.S_IRGRP)
        self.assertEqual(modefunc('------r--'), stat.S_IROTH)
        self.assertEqual(modefunc('-w-------'), stat.S_IWUSR)
        self.assertEqual(modefunc('----w----'), stat.S_IWGRP)
        self.assertEqual(modefunc('-------w-'), stat.S_IWOTH)
        self.assertEqual(modefunc('--x------'), stat.S_IXUSR)
        self.assertEqual(modefunc('-----x---'), stat.S_IXGRP)
        self.assertEqual(modefunc('--------x'), stat.S_IXOTH)
        self.assertEqual(modefunc('--S------'), stat.S_ISUID)
        self.assertEqual(modefunc('-----S---'), stat.S_ISGID)
        self.assertEqual(modefunc('--------T'), stat.S_ISVTX)
        self.assertEqual(modefunc('--s------'), stat.S_ISUID | stat.S_IXUSR)
        self.assertEqual(modefunc('-----s---'), stat.S_ISGID | stat.S_IXGRP)
        self.assertEqual(modefunc('--------t'), stat.S_ISVTX | stat.S_IXOTH)
        self.assertEqual(modefunc('rwx------'), stat.S_IRWXU)
        self.assertEqual(modefunc('---rwx---'), stat.S_IRWXG)
        self.assertEqual(modefunc('------rwx'), stat.S_IRWXO)
        # We could keep listing combinations exhaustively but that seems
        # tedious and pointless. Just test a few more.
        self.assertEqual(modefunc('rwxr-xr-x'),
                         stat.S_IRWXU |
                         stat.S_IRGRP | stat.S_IXGRP |
                         stat.S_IROTH | stat.S_IXOTH)
        self.assertEqual(modefunc('rw-r--r--'),
                         stat.S_IRUSR | stat.S_IWUSR |
                         stat.S_IRGRP |
                         stat.S_IROTH)
        self.assertEqual(modefunc('rwsr-x---'),
                         stat.S_IRWXU | stat.S_ISUID |
                         stat.S_IRGRP | stat.S_IXGRP)

    def test_compiler_args_class_none_flush(self):
        cc = ClangCCompiler([], [], 'fake', MachineChoice.HOST, get_fake_env())
        a = cc.compiler_args(['-I.'])
        #first we are checking if the tree construction deduplicates the correct -I argument
        a += ['-I..']
        a += ['-I./tests/']
        a += ['-I./tests2/']
        #think this here as assertion, we cannot apply it, otherwise the CompilerArgs would already flush the changes:
        # assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..', '-I.'])
        a += ['-I.']
        a += ['-I.', '-I./tests/']
        self.assertEqual(a, ['-I.', '-I./tests/', '-I./tests2/', '-I..'])

        #then we are checking that when CompilerArgs already have a build container list, that the deduplication is taking the correct one
        a += ['-I.', '-I./tests2/']
        self.assertEqual(a, ['-I.', '-I./tests2/', '-I./tests/', '-I..'])

    def test_compiler_args_class_d(self):
        d = DmdDCompiler([], 'fake', MachineChoice.HOST, get_fake_env(), 'arch')
        # check include order is kept when deduplicating
        a = d.compiler_args(['-Ifirst', '-Isecond', '-Ithird'])
        a += ['-Ifirst']
        self.assertEqual(a, ['-Ifirst', '-Isecond', '-Ithird'])

    def test_compiler_args_class_clike(self):
        cc = ClangCCompiler([], [], 'fake', MachineChoice.HOST, get_fake_env())
        # Test that empty initialization works
        a = cc.compiler_args()
        self.assertEqual(a, [])
        # Test that list initialization works
        a = cc.compiler_args(['-I.', '-I..'])
        self.assertEqual(a, ['-I.', '-I..'])
        # Test that there is no de-dup on initialization
        self.assertEqual(cc.compiler_args(['-I.', '-I.']), ['-I.', '-I.'])

        ## Test that appending works
        a.append('-I..')
        self.assertEqual(a, ['-I..', '-I.'])
        a.append('-O3')
        self.assertEqual(a, ['-I..', '-I.', '-O3'])

        ## Test that in-place addition works
        a += ['-O2', '-O2']
        self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2', '-O2'])
        # Test that removal works
        a.remove('-O2')
        self.assertEqual(a, ['-I..', '-I.', '-O3', '-O2'])
        # Test that de-dup happens on addition
        a += ['-Ifoo', '-Ifoo']
        self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2'])

        # .extend() is just +=, so we don't test it

        ## Test that addition works
        # Test that adding a list with just one old arg works and yields the same array
        a = a + ['-Ifoo']
        self.assertEqual(a, ['-Ifoo', '-I..', '-I.', '-O3', '-O2'])
        # Test that adding a list with one arg new and one old works
        a = a + ['-Ifoo', '-Ibaz']
        self.assertEqual(a, ['-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2'])
        # Test that adding args that must be prepended and appended works
        a = a + ['-Ibar', '-Wall']
        self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall'])

        ## Test that reflected addition works
        # Test that adding to a list with just one old arg works and yields the same array
        a = ['-Ifoo'] + a
        self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-O3', '-O2', '-Wall'])
        # Test that adding to a list with just one new arg that is not pre-pended works
        a = ['-Werror'] + a
        self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Werror', '-O3', '-O2', '-Wall'])
        # Test that adding to a list with two new args preserves the order
        a = ['-Ldir', '-Lbah'] + a
        self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall'])
        # Test that adding to a list with old args does nothing
        a = ['-Ibar', '-Ibaz', '-Ifoo'] + a
        self.assertEqual(a, ['-Ibar', '-Ifoo', '-Ibaz', '-I..', '-I.', '-Ldir', '-Lbah', '-Werror', '-O3', '-O2', '-Wall'])

        ## Test that adding libraries works
        l = cc.compiler_args(['-Lfoodir', '-lfoo'])
        self.assertEqual(l, ['-Lfoodir', '-lfoo'])
        # Adding a library and a libpath appends both correctly
        l += ['-Lbardir', '-lbar']
        self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar'])
        # Adding the same library again does nothing
        l += ['-lbar']
        self.assertEqual(l, ['-Lbardir', '-Lfoodir', '-lfoo', '-lbar'])

        ## Test that 'direct' append and extend works
        l = cc.compiler_args(['-Lfoodir', '-lfoo'])
        self.assertEqual(l, ['-Lfoodir', '-lfoo'])
        # Direct-adding a library and a libpath appends both correctly
        l.extend_direct(['-Lbardir', '-lbar'])
        self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar'])
        # Direct-adding the same library again still adds it
        l.append_direct('-lbar')
        self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar'])
        # Direct-adding with absolute path deduplicates
        abspath = str(Path('/libbaz.a').resolve())
        l.append_direct(abspath)
        self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', abspath])
        # Adding libbaz again does nothing
        l.append_direct(abspath)
        self.assertEqual(l, ['-Lfoodir', '-lfoo', '-Lbardir', '-lbar', '-lbar', abspath])


    def test_compiler_args_class_visualstudio(self):
        env = get_fake_env()
        linker = linkers.MSVCDynamicLinker(env, MachineChoice.HOST, [])
        # Version just needs to be > 19.0.0
        cc = VisualStudioCPPCompiler([], [], '20.00', MachineChoice.HOST, env, 'x64', linker=linker)

        a = cc.compiler_args(cc.get_always_args())
        self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/utf-8', '/Zc:__cplusplus'])

        # Ensure /source-charset: removes /utf-8
        a.append('/source-charset:utf-8')
        self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/Zc:__cplusplus', '/source-charset:utf-8'])

        # Ensure /execution-charset: removes /utf-8
        a = cc.compiler_args(cc.get_always_args() + ['/execution-charset:utf-8'])
        self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/Zc:__cplusplus', '/execution-charset:utf-8'])

        # Ensure /validate-charset- removes /utf-8
        a = cc.compiler_args(cc.get_always_args() + ['/validate-charset-'])
        self.assertEqual(a.to_native(copy=True), ['/nologo', '/showIncludes', '/Zc:__cplusplus', '/validate-charset-'])


    def test_msvc_unix_args_to_native(self):
        # joined
        self.assertEqual(MSVCCompiler.unix_args_to_native(['-isystemfoo']), ['/Ifoo'])
        self.assertEqual(MSVCCompiler.unix_args_to_native(['-idirafterfoo']), ['/Ifoo'])
        self.assertEqual(MSVCCompiler.unix_args_to_native(['-iquotefoo']), ['/Ifoo'])

        # with = separator
        self.assertEqual(MSVCCompiler.unix_args_to_native(['-isystem=foo']), ['/Ifoo'])
        self.assertEqual(MSVCCompiler.unix_args_to_native(['-idirafter=foo']), ['/Ifoo'])
        self.assertEqual(MSVCCompiler.unix_args_to_native(['-iquote=foo']), ['/Ifoo'])

        # as separate argument
        self.assertEqual(MSVCCompiler.unix_args_to_native(['-isystem', 'foo']), ['/Ifoo'])
        self.assertEqual(MSVCCompiler.unix_args_to_native(['-idirafter', 'foo']), ['/Ifoo'])
        self.assertEqual(MSVCCompiler.unix_args_to_native(['-iquote', 'foo']), ['/Ifoo'])

    def test_clangcl_unix_args_to_native(self):
        # joined
        self.assertEqual(ClangClCompiler.unix_args_to_native(['-isystemfoo']), ['/clang:-isystemfoo'])
        self.assertEqual(ClangClCompiler.unix_args_to_native(['-idirafterfoo']), ['/clang:-idirafterfoo'])
        self.assertEqual(ClangClCompiler.unix_args_to_native(['-iquotefoo']), ['/clang:-iquotefoo'])

        # with = separator
        self.assertEqual(ClangClCompiler.unix_args_to_native(['-isystem=foo']), ['/clang:-isystemfoo'])
        self.assertEqual(ClangClCompiler.unix_args_to_native(['-idirafter=foo']), ['/clang:-idirafterfoo'])
        self.assertEqual(ClangClCompiler.unix_args_to_native(['-iquote=foo']), ['/clang:-iquotefoo'])

        # as separate argument
        self.assertEqual(ClangClCompiler.unix_args_to_native(['-isystem', 'foo']), ['/clang:-isystemfoo'])
        self.assertEqual(ClangClCompiler.unix_args_to_native(['-idirafter', 'foo']), ['/clang:-idirafterfoo'])
        self.assertEqual(ClangClCompiler.unix_args_to_native(['-iquote', 'foo']), ['/clang:-iquotefoo'])

    def test_compiler_args_class_gnuld(self):
        ## Test --start/end-group
        env = get_fake_env()
        linker = linkers.GnuBFDDynamicLinker([], env, MachineChoice.HOST, '-Wl,', [])
        gcc = GnuCCompiler([], [], 'fake', MachineChoice.HOST, env, linker=linker)
        ## Ensure that the fake compiler is never called by overriding the relevant function
        gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include']
        ## Test that 'direct' append and extend works
        l = gcc.compiler_args(['-Lfoodir', '-lfoo'])
        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-lfoo'])
        # Direct-adding a library and a libpath appends both correctly
        l.extend_direct(['-Lbardir', '-lbar'])
        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-Wl,--end-group'])
        # Direct-adding the same library again still adds it
        l.append_direct('-lbar')
        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', '-Wl,--end-group'])
        # Direct-adding with absolute path deduplicates
        abspath = str(Path('/libbaz.a').resolve())
        l.append_direct(abspath)
        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', abspath, '-Wl,--end-group'])
        # Adding libbaz again does nothing
        l.append_direct(abspath)
        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', abspath, '-Wl,--end-group'])
        # Adding a non-library argument doesn't include it in the group
        l += ['-Lfoo', '-Wl,--export-dynamic']
        self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', abspath, '-Wl,--end-group', '-Wl,--export-dynamic'])
        # -Wl,-lfoo is detected as a library and gets added to the group
        l.append('-Wl,-ldl')
        self.assertEqual(l.to_native(copy=True), ['-Lfoo', '-Lfoodir', '-Wl,--start-group', '-lfoo', '-Lbardir', '-lbar', '-lbar', abspath, '-Wl,--export-dynamic', '-Wl,-ldl', '-Wl,--end-group'])

    def test_compiler_args_remove_system(self):
        ## Test --start/end-group
        env = get_fake_env()
        linker = linkers.GnuBFDDynamicLinker([], env, MachineChoice.HOST, '-Wl,', [])
        gcc = GnuCCompiler([], [], 'fake', MachineChoice.HOST, env, linker=linker)
        ## Ensure that the fake compiler is never called by overriding the relevant function
        gcc.get_default_include_dirs = lambda: ['/usr/include', '/usr/share/include', '/usr/local/include']
        ## Test that 'direct' append and extend works
        l = gcc.compiler_args(['-Lfoodir', '-lfoo'])
        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-lfoo'])
        ## Test that to_native removes all system includes
        l += ['-isystem/usr/include', '-isystem=/usr/share/include', '-DSOMETHING_IMPORTANT=1', '-isystem', '/usr/local/include']
        self.assertEqual(l.to_native(copy=True), ['-Lfoodir', '-lfoo', '-DSOMETHING_IMPORTANT=1'])

    def test_string_templates_substitution(self):
        dictfunc = mesonbuild.mesonlib.get_filenames_templates_dict
        substfunc = mesonbuild.mesonlib.substitute_values
        ME = mesonbuild.mesonlib.MesonException

        # Identity
        self.assertEqual(dictfunc([], []), {})

        # One input, no outputs
        inputs = ['bar/foo.c.in']
        outputs = []
        ret = dictfunc(inputs, outputs)
        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
             '@PLAINNAME0@': 'foo.c.in', '@BASENAME0@': 'foo.c',
             '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c'}
        # Check dictionary
        self.assertEqual(ret, d)
        # Check substitutions
        cmd = ['some', 'ordinary', 'strings']
        self.assertEqual(substfunc(cmd, d), cmd)
        cmd = ['@INPUT@.out', 'ordinary', 'strings']
        self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:])
        cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', 'strings']
        self.assertEqual(substfunc(cmd, d),
                         [inputs[0] + '.out'] + [d['@PLAINNAME@'] + '.ok'] + cmd[2:])
        cmd = ['@INPUT@', '@BASENAME@.hah', 'strings']
        self.assertEqual(substfunc(cmd, d),
                         inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])
        cmd = ['@OUTPUT@']
        self.assertRaises(ME, substfunc, cmd, d)

        # One input, one output
        inputs = ['bar/foo.c.in']
        outputs = ['out.c']
        ret = dictfunc(inputs, outputs)
        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
             '@PLAINNAME0@': 'foo.c.in', '@BASENAME0@': 'foo.c',
             '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
             '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': '.'}
        # Check dictionary
        self.assertEqual(ret, d)
        # Check substitutions
        cmd = ['some', 'ordinary', 'strings']
        self.assertEqual(substfunc(cmd, d), cmd)
        cmd = ['@INPUT@ @OUTPUT@']
        self.assertEqual(substfunc(cmd, d),
                         [f'{inputs[0]} {outputs[0]}'])
        cmd = ['@INPUT@.out', '@OUTPUT@', 'strings']
        self.assertEqual(substfunc(cmd, d),
                         [inputs[0] + '.out'] + outputs + cmd[2:])
        cmd = ['@INPUT0@.out', '@PLAINNAME@.ok', '@OUTPUT0@']
        self.assertEqual(substfunc(cmd, d),
                         [inputs[0] + '.out', d['@PLAINNAME@'] + '.ok'] + outputs)
        cmd = ['@INPUT@', '@BASENAME@.hah', 'strings']
        self.assertEqual(substfunc(cmd, d),
                         inputs + [d['@BASENAME@'] + '.hah'] + cmd[2:])

        # One input, one output with a subdir
        outputs = ['dir/out.c']
        ret = dictfunc(inputs, outputs)
        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0],
             '@PLAINNAME0@': 'foo.c.in', '@BASENAME0@': 'foo.c',
             '@PLAINNAME@': 'foo.c.in', '@BASENAME@': 'foo.c',
             '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
        # Check dictionary
        self.assertEqual(ret, d)

        # Two inputs, no outputs
        inputs = ['bar/foo.c.in', 'baz/foo.c.in']
        outputs = []
        ret = dictfunc(inputs, outputs)
        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
             '@PLAINNAME0@': 'foo.c.in', '@PLAINNAME1@': 'foo.c.in',
             '@BASENAME0@': 'foo.c', '@BASENAME1@': 'foo.c'}
        # Check dictionary
        self.assertEqual(ret, d)
        # Check substitutions
        cmd = ['some', 'ordinary', 'strings']
        self.assertEqual(substfunc(cmd, d), cmd)
        cmd = ['@INPUT@', 'ordinary', 'strings']
        self.assertEqual(substfunc(cmd, d), inputs + cmd[1:])
        cmd = ['@INPUT0@.out', 'ordinary', 'strings']
        self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out'] + cmd[1:])
        cmd = ['@INPUT0@.out', '@INPUT1@.ok', 'strings']
        self.assertEqual(substfunc(cmd, d), [inputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:])
        cmd = ['@INPUT0@', '@INPUT1@', 'strings']
        self.assertEqual(substfunc(cmd, d), inputs + cmd[2:])
        # Many inputs, can't use @INPUT@ like this
        cmd = ['@INPUT@.out', 'ordinary', 'strings']
        self.assertRaises(ME, substfunc, cmd, d)
        # Not enough inputs
        cmd = ['@INPUT2@.out', 'ordinary', 'strings']
        self.assertRaises(ME, substfunc, cmd, d)
        # Too many inputs
        cmd = ['@PLAINNAME@']
        self.assertRaises(ME, substfunc, cmd, d)
        cmd = ['@BASENAME@']
        self.assertRaises(ME, substfunc, cmd, d)
        # No outputs
        cmd = ['@OUTPUT@']
        self.assertRaises(ME, substfunc, cmd, d)
        cmd = ['@OUTPUT0@']
        self.assertRaises(ME, substfunc, cmd, d)
        cmd = ['@OUTDIR@']
        self.assertRaises(ME, substfunc, cmd, d)

        # Two inputs, one output
        outputs = ['dir/out.c']
        ret = dictfunc(inputs, outputs)
        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
             '@PLAINNAME0@': 'foo.c.in', '@PLAINNAME1@': 'foo.c.in',
             '@BASENAME0@': 'foo.c', '@BASENAME1@': 'foo.c',
             '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTDIR@': 'dir'}
        # Check dictionary
        self.assertEqual(ret, d)
        # Check substitutions
        cmd = ['some', 'ordinary', 'strings']
        self.assertEqual(substfunc(cmd, d), cmd)
        cmd = ['@OUTPUT@', 'ordinary', 'strings']
        self.assertEqual(substfunc(cmd, d), outputs + cmd[1:])
        cmd = ['@OUTPUT@.out', 'ordinary', 'strings']
        self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out'] + cmd[1:])
        cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', 'strings']
        self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok'] + cmd[2:])
        # Many inputs, can't use @INPUT@ like this
        cmd = ['@INPUT@.out', 'ordinary', 'strings']
        self.assertRaises(ME, substfunc, cmd, d)
        # Not enough inputs
        cmd = ['@INPUT2@.out', 'ordinary', 'strings']
        self.assertRaises(ME, substfunc, cmd, d)
        # Not enough outputs
        cmd = ['@OUTPUT2@.out', 'ordinary', 'strings']
        self.assertRaises(ME, substfunc, cmd, d)

        # Two inputs, two outputs
        outputs = ['dir/out.c', 'dir/out2.c']
        ret = dictfunc(inputs, outputs)
        d = {'@INPUT@': inputs, '@INPUT0@': inputs[0], '@INPUT1@': inputs[1],
             '@PLAINNAME0@': 'foo.c.in', '@PLAINNAME1@': 'foo.c.in',
             '@BASENAME0@': 'foo.c', '@BASENAME1@': 'foo.c',
             '@OUTPUT@': outputs, '@OUTPUT0@': outputs[0], '@OUTPUT1@': outputs[1],
             '@OUTDIR@': 'dir'}
        # Check dictionary
        self.assertEqual(ret, d)
        # Check substitutions
        cmd = ['some', 'ordinary', 'strings']
        self.assertEqual(substfunc(cmd, d), cmd)
        cmd = ['@OUTPUT@', 'ordinary', 'strings']
        self.assertEqual(substfunc(cmd, d), outputs + cmd[1:])
        cmd = ['@OUTPUT0@', '@OUTPUT1@', 'strings']
        self.assertEqual(substfunc(cmd, d), outputs + cmd[2:])
        cmd = ['@OUTPUT0@.out', '@INPUT1@.ok', '@OUTDIR@']
        self.assertEqual(substfunc(cmd, d), [outputs[0] + '.out', inputs[1] + '.ok', 'dir'])
        # Many inputs, can't use @INPUT@ like this
        cmd = ['@INPUT@.out', 'ordinary', 'strings']
        self.assertRaises(ME, substfunc, cmd, d)
        # Not enough inputs
        cmd = ['@INPUT2@.out', 'ordinary', 'strings']
        self.assertRaises(ME, substfunc, cmd, d)
        # Not enough outputs
        cmd = ['@OUTPUT2@.out', 'ordinary', 'strings']
        self.assertRaises(ME, substfunc, cmd, d)
        # Many outputs, can't use @OUTPUT@ like this
        cmd = ['@OUTPUT@.out', 'ordinary', 'strings']
        self.assertRaises(ME, substfunc, cmd, d)

    def test_needs_exe_wrapper_override(self):
        config = ConfigParser()
        config['binaries'] = {
            'c': '\'/usr/bin/gcc\'',
        }
        config['host_machine'] = {
            'system': '\'linux\'',
            'cpu_family': '\'arm\'',
            'cpu': '\'armv7\'',
            'endian': '\'little\'',
        }
        # Can not be used as context manager because we need to
        # open it a second time and this is not possible on
        # Windows.
        configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False, encoding='utf-8')
        configfilename = configfile.name
        config.write(configfile)
        configfile.flush()
        configfile.close()
        opts = get_fake_options()
        opts.cross_file = (configfilename,)
        env = get_fake_env(opts=opts)
        detected_value = env.need_exe_wrapper()
        os.unlink(configfilename)

        desired_value = not detected_value
        config['properties'] = {
            'needs_exe_wrapper': 'true' if desired_value else 'false'
        }

        configfile = tempfile.NamedTemporaryFile(mode='w+', delete=False, encoding='utf-8')
        configfilename = configfile.name
        config.write(configfile)
        configfile.close()
        opts = get_fake_options()
        opts.cross_file = (configfilename,)
        env = get_fake_env(opts=opts)
        forced_value = env.need_exe_wrapper()
        os.unlink(configfilename)

        self.assertEqual(forced_value, desired_value)

    def test_listify(self):
        listify = mesonbuild.mesonlib.listify
        # Test sanity
        self.assertEqual([1], listify(1))
        self.assertEqual([], listify([]))
        self.assertEqual([1], listify([1]))
        # Test flattening
        self.assertEqual([1, 2, 3], listify([1, [2, 3]]))
        self.assertEqual([1, 2, 3], listify([1, [2, [3]]]))
        self.assertEqual([1, [2, [3]]], listify([1, [2, [3]]], flatten=False))
        # Test flattening and unholdering
        class TestHeldObj(mesonbuild.mesonlib.HoldableObject):
            def __init__(self, val: int) -> None:
                self._val = val
        class MockInterpreter:
            def __init__(self) -> None:
                self.subproject = ''
                self.environment = None
        heldObj1 = TestHeldObj(1)
        holder1 = ObjectHolder(heldObj1, MockInterpreter())
        self.assertEqual([holder1], listify(holder1))
        self.assertEqual([holder1], listify([holder1]))
        self.assertEqual([holder1, 2], listify([holder1, 2]))
        self.assertEqual([holder1, 2, 3], listify([holder1, 2, [3]]))

    def test_extract_as_list(self):
        extract = mesonbuild.mesonlib.extract_as_list
        # Test sanity
        kwargs = {'sources': [1, 2, 3]}
        self.assertEqual([1, 2, 3], extract(kwargs, 'sources'))
        self.assertEqual(kwargs, {'sources': [1, 2, 3]})
        self.assertEqual([1, 2, 3], extract(kwargs, 'sources', pop=True))
        self.assertEqual(kwargs, {})

        class TestHeldObj(mesonbuild.mesonlib.HoldableObject):
            pass
        class MockInterpreter:
            def __init__(self) -> None:
                self.subproject = ''
                self.environment = None
        heldObj = TestHeldObj()

        # Test unholding
        holder3 = ObjectHolder(heldObj, MockInterpreter())
        kwargs = {'sources': [1, 2, holder3]}
        self.assertEqual(kwargs, {'sources': [1, 2, holder3]})

        # flatten nested lists
        kwargs = {'sources': [1, [2, [3]]]}
        self.assertEqual([1, 2, 3], extract(kwargs, 'sources'))

    def _test_all_naming(self, cc, patterns, platform):
        shr = patterns[platform]['shared']
        stc = patterns[platform]['static']
        shrstc = shr + tuple(x for x in stc if x not in shr)
        stcshr = stc + tuple(x for x in shr if x not in stc)
        p = cc.get_library_naming(LibType.SHARED)
        self.assertEqual(p, shr)
        p = cc.get_library_naming(LibType.STATIC)
        self.assertEqual(p, stc)
        p = cc.get_library_naming(LibType.PREFER_STATIC)
        self.assertEqual(p, stcshr)
        p = cc.get_library_naming(LibType.PREFER_SHARED)
        self.assertEqual(p, shrstc)
        # Test find library by mocking up openbsd
        if platform != 'openbsd':
            return
        with tempfile.TemporaryDirectory() as tmpdir:
            for i in ['libfoo.so.6.0', 'libfoo.so.5.0', 'libfoo.so.54.0', 'libfoo.so.66a.0b', 'libfoo.so.70.0.so.1',
                      'libbar.so.7.10', 'libbar.so.7.9', 'libbar.so.7.9.3']:
                libpath = Path(tmpdir) / i
                src = libpath.with_suffix('.c')
                with src.open('w', encoding='utf-8') as f:
                    f.write('int meson_foobar (void) { return 0; }')
                subprocess.check_call(['gcc', str(src), '-o', str(libpath), '-shared'])

            found = cc._find_library_real('foo', [tmpdir], 'int main(void) { return 0; }', LibType.PREFER_SHARED, lib_prefix_warning=True, ignore_system_dirs=False)
            self.assertEqual(os.path.basename(found[0]), 'libfoo.so.54.0')
            found = cc._find_library_real('bar', [tmpdir], 'int main(void) { return 0; }', LibType.PREFER_SHARED, lib_prefix_warning=True, ignore_system_dirs=False)
            self.assertEqual(os.path.basename(found[0]), 'libbar.so.7.10')

    def test_find_library_patterns(self):
        '''
        Unit test for the library search patterns used by find_library()
        '''
        unix_static = ('lib{}.a', '{}.a')
        msvc_static = ('lib{}.a', 'lib{}.lib', '{}.a', '{}.lib')
        # This is the priority list of pattern matching for library searching
        patterns = {'openbsd': {'shared': ('lib{}.so', '{}.so', 'lib{}.so.[0-9]*.[0-9]*', '{}.so.[0-9]*.[0-9]*'),
                                'static': unix_static},
                    'linux': {'shared': ('lib{}.so', '{}.so'),
                              'static': unix_static},
                    'darwin': {'shared': ('lib{}.dylib', 'lib{}.so', '{}.dylib', '{}.so'),
                               'static': unix_static},
                    'cygwin': {'shared': ('cyg{}.dll', 'cyg{}.dll.a', 'lib{}.dll',
                                          'lib{}.dll.a', '{}.dll', '{}.dll.a'),
                               'static': ('cyg{}.a',) + unix_static},
                    'windows-msvc': {'shared': ('lib{}.lib', '{}.lib'),
                                     'static': msvc_static},
                    'windows-mingw': {'shared': ('lib{}.dll.a', 'lib{}.lib', 'lib{}.dll',
                                                 '{}.dll.a', '{}.lib', '{}.dll'),
                                      'static': msvc_static}}
        env = get_fake_env()
        cc = detect_c_compiler(env, MachineChoice.HOST)
        if is_osx():
            self._test_all_naming(cc, patterns, 'darwin')
        elif is_cygwin():
            self._test_all_naming(cc, patterns, 'cygwin')
        elif is_windows():
            if cc.get_argument_syntax() == 'msvc':
                self._test_all_naming(cc, patterns, 'windows-msvc')
            else:
                self._test_all_naming(cc, patterns, 'windows-mingw')
        elif is_openbsd():
            self._test_all_naming(cc, patterns, 'openbsd')
        else:
            self._test_all_naming(cc, patterns, 'linux')
            env.machines.host.system = 'openbsd'
            self._test_all_naming(cc, patterns, 'openbsd')
            env.machines.host.system = 'darwin'
            self._test_all_naming(cc, patterns, 'darwin')
            env.machines.host.system = 'cygwin'
            self._test_all_naming(cc, patterns, 'cygwin')
            env.machines.host.system = 'windows'
            self._test_all_naming(cc, patterns, 'windows-mingw')

    @skipIfNoPkgconfig
    def test_pkgconfig_parse_libs(self):
        '''
        Unit test for parsing of pkg-config output to search for libraries

        https://github.com/mesonbuild/meson/issues/3951
        '''
        def create_static_lib(name):
            src = name.with_suffix('.c')
            out = name.with_suffix('.o')
            with src.open('w', encoding='utf-8') as f:
                f.write('int meson_foobar (void) { return 0; }')
            # use of x86_64 is hardcoded in run_tests.py:get_fake_env()
            if is_osx():
                subprocess.check_call(['clang', '-c', str(src), '-o', str(out), '-arch', 'x86_64'])
            else:
                subprocess.check_call(['gcc', '-c', str(src), '-o', str(out)])
            subprocess.check_call(['ar', 'csr', str(name), str(out)])

        # The test relies on some open-coded toolchain invocations for
        # library creation in create_static_lib.
        if is_windows() or is_cygwin():
            return

        with tempfile.TemporaryDirectory() as tmpdir:
            pkgbin = ExternalProgram('pkg-config', command=['pkg-config'], silent=True)
            env = get_fake_env()
            compiler = detect_c_compiler(env, MachineChoice.HOST)
            env.coredata.compilers.host = {'c': compiler}
            p1 = Path(tmpdir) / '1'
            p2 = Path(tmpdir) / '2'
            p1.mkdir()
            p2.mkdir()
            # libfoo.a is in one prefix
            create_static_lib(p1 / 'libfoo.a')
            # libbar.a is in both prefixes
            create_static_lib(p1 / 'libbar.a')
            create_static_lib(p2 / 'libbar.a')
            # Ensure that we never statically link to these
            create_static_lib(p1 / 'libpthread.a')
            create_static_lib(p1 / 'libm.a')
            create_static_lib(p1 / 'libc.a')
            create_static_lib(p1 / 'libdl.a')
            create_static_lib(p1 / 'librt.a')

            class FakeInstance(PkgConfigCLI):
                def _call_pkgbin(self, args, env=None):
                    if '--libs' not in args:
                        return 0, '', ''
                    if args[-1] == 'foo':
                        return 0, f'-L{p2.as_posix()} -lfoo -L{p1.as_posix()} -lbar', ''
                    if args[-1] == 'bar':
                        return 0, f'-L{p2.as_posix()} -lbar', ''
                    if args[-1] == 'internal':
                        return 0, f'-L{p1.as_posix()} -lpthread -lm -lc -lrt -ldl', ''

            with mock.patch.object(PkgConfigInterface, 'instance') as instance_method:
                instance_method.return_value = FakeInstance(env, MachineChoice.HOST, silent=True)
                kwargs = {'required': True, 'silent': True, 'native': MachineChoice.HOST}
                foo_dep = PkgConfigDependency('foo', env, kwargs)
                self.assertEqual(foo_dep.get_link_args(),
                                 [(p1 / 'libfoo.a').as_posix(), (p2 / 'libbar.a').as_posix()])
                bar_dep = PkgConfigDependency('bar', env, kwargs)
                self.assertEqual(bar_dep.get_link_args(), [(p2 / 'libbar.a').as_posix()])
                internal_dep = PkgConfigDependency('internal', env, kwargs)
                if compiler.get_argument_syntax() == 'msvc':
                    self.assertEqual(internal_dep.get_link_args(), [])
                else:
                    link_args = internal_dep.get_link_args()
                    for link_arg in link_args:
                        for lib in ('pthread', 'm', 'c', 'dl', 'rt'):
                            self.assertNotIn(f'lib{lib}.a', link_arg, msg=link_args)

    def test_program_version(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            script_path = Path(tmpdir) / 'script.py'
            script_path.write_text('import sys\nprint(sys.argv[1])\n', encoding='utf-8')
            script_path.chmod(0o755)

            for output, expected in {
                    '': None,
                    '1': None,
                    '1.2.4': '1.2.4',
                    '1 1.2.4': '1.2.4',
                    'foo version 1.2.4': '1.2.4',
                    'foo 1.2.4.': '1.2.4',
                    'foo 1.2.4': '1.2.4',
                    'foo 1.2.4 bar': '1.2.4',
                    'foo 10.0.0': '10.0.0',
                    '50 5.4.0': '5.4.0',
                    'This is perl 5, version 40, subversion 0 (v5.40.0)': '5.40.0',
                    'git version 2.48.0.rc1': '2.48.0',
            }.items():
                prog = ExternalProgram('script', command=[python_command, str(script_path), output], silent=True)

                if expected is None:
                    with self.assertRaisesRegex(MesonException, 'Could not find a version number'):
                        prog.get_version()
                else:
                    self.assertEqual(prog.get_version(), expected)

    def test_version_compare(self):
        comparefunc = mesonbuild.mesonlib.version_compare_many
        for (a, b, result) in [
                ('0.99.beta19', '>= 0.99.beta14', True),
        ]:
            self.assertEqual(comparefunc(a, b)[0], result)

        for (a, b, op) in [
                # examples from https://fedoraproject.org/wiki/Archive:Tools/RPM/VersionComparison
                ("1.0010", "1.9", operator.gt),
                ("1.05", "1.5", operator.eq),
                ("1.0", "1", operator.gt),
                ("2.50", "2.5", operator.gt),
                ("fc4", "fc.4", operator.eq),
                ("FC5", "fc4", operator.lt),
                ("2a", "2.0", operator.lt),
                ("1.0", "1.fc4", operator.gt),
                ("3.0.0_fc", "3.0.0.fc", operator.eq),
                # from RPM tests
                ("1.0", "1.0", operator.eq),
                ("1.0", "2.0", operator.lt),
                ("2.0", "1.0", operator.gt),
                ("2.0.1", "2.0.1", operator.eq),
                ("2.0", "2.0.1", operator.lt),
                ("2.0.1", "2.0", operator.gt),
                ("2.0.1a", "2.0.1a", operator.eq),
                ("2.0.1a", "2.0.1", operator.gt),
                ("2.0.1", "2.0.1a", operator.lt),
                ("5.5p1", "5.5p1", operator.eq),
                ("5.5p1", "5.5p2", operator.lt),
                ("5.5p2", "5.5p1", operator.gt),
                ("5.5p10", "5.5p10", operator.eq),
                ("5.5p1", "5.5p10", operator.lt),
                ("5.5p10", "5.5p1", operator.gt),
                ("10xyz", "10.1xyz", operator.lt),
                ("10.1xyz", "10xyz", operator.gt),
                ("xyz10", "xyz10", operator.eq),
                ("xyz10", "xyz10.1", operator.lt),
                ("xyz10.1", "xyz10", operator.gt),
                ("xyz.4", "xyz.4", operator.eq),
                ("xyz.4", "8", operator.lt),
                ("8", "xyz.4", operator.gt),
                ("xyz.4", "2", operator.lt),
                ("2", "xyz.4", operator.gt),
                ("5.5p2", "5.6p1", operator.lt),
                ("5.6p1", "5.5p2", operator.gt),
                ("5.6p1", "6.5p1", operator.lt),
                ("6.5p1", "5.6p1", operator.gt),
                ("6.0.rc1", "6.0", operator.gt),
                ("6.0", "6.0.rc1", operator.lt),
                ("10b2", "10a1", operator.gt),
                ("10a2", "10b2", operator.lt),
                ("1.0aa", "1.0aa", operator.eq),
                ("1.0a", "1.0aa", operator.lt),
                ("1.0aa", "1.0a", operator.gt),
                ("10.0001", "10.0001", operator.eq),
                ("10.0001", "10.1", operator.eq),
                ("10.1", "10.0001", operator.eq),
                ("10.0001", "10.0039", operator.lt),
                ("10.0039", "10.0001", operator.gt),
                ("4.999.9", "5.0", operator.lt),
                ("5.0", "4.999.9", operator.gt),
                ("20101121", "20101121", operator.eq),
                ("20101121", "20101122", operator.lt),
                ("20101122", "20101121", operator.gt),
                ("2_0", "2_0", operator.eq),
                ("2.0", "2_0", operator.eq),
                ("2_0", "2.0", operator.eq),
                ("a", "a", operator.eq),
                ("a+", "a+", operator.eq),
                ("a+", "a_", operator.eq),
                ("a_", "a+", operator.eq),
                ("+a", "+a", operator.eq),
                ("+a", "_a", operator.eq),
                ("_a", "+a", operator.eq),
                ("+_", "+_", operator.eq),
                ("_+", "+_", operator.eq),
                ("_+", "_+", operator.eq),
                ("+", "_", operator.eq),
                ("_", "+", operator.eq),
                # other tests
                ('0.99.beta19', '0.99.beta14', operator.gt),
                ("1.0.0", "2.0.0", operator.lt),
                (".0.0", "2.0.0", operator.lt),
                ("alpha", "beta", operator.lt),
                ("1.0", "1.0.0", operator.lt),
                ("2.456", "2.1000", operator.lt),
                ("2.1000", "3.111", operator.lt),
                ("2.001", "2.1", operator.eq),
                ("2.34", "2.34", operator.eq),
                ("6.1.2", "6.3.8", operator.lt),
                ("1.7.3.0", "2.0.0", operator.lt),
                ("2.24.51", "2.25", operator.lt),
                ("2.1.5+20120813+gitdcbe778", "2.1.5", operator.gt),
                ("3.4.1", "3.4b1", operator.gt),
                ("041206", "200090325", operator.lt),
                ("0.6.2+git20130413", "0.6.2", operator.gt),
                ("2.6.0+bzr6602", "2.6.0", operator.gt),
                ("2.6.0", "2.6b2", operator.gt),
                ("2.6.0+bzr6602", "2.6b2x", operator.gt),
                ("0.6.7+20150214+git3a710f9", "0.6.7", operator.gt),
                ("15.8b", "15.8.0.1", operator.lt),
                ("1.2rc1", "1.2.0", operator.lt),
        ]:
            ver_a = Version(a)
            ver_b = Version(b)
            if op is operator.eq:
                for o, name in [(op, 'eq'), (operator.ge, 'ge'), (operator.le, 'le')]:
                    self.assertTrue(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}')
            if op is operator.lt:
                for o, name in [(op, 'lt'), (operator.le, 'le'), (operator.ne, 'ne')]:
                    self.assertTrue(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}')
                for o, name in [(operator.gt, 'gt'), (operator.ge, 'ge'), (operator.eq, 'eq')]:
                    self.assertFalse(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}')
            if op is operator.gt:
                for o, name in [(op, 'gt'), (operator.ge, 'ge'), (operator.ne, 'ne')]:
                    self.assertTrue(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}')
                for o, name in [(operator.lt, 'lt'), (operator.le, 'le'), (operator.eq, 'eq')]:
                    self.assertFalse(o(ver_a, ver_b), f'{ver_a} {name} {ver_b}')

    def test_split_args(self):
        split_args = mesonbuild.mesonlib.split_args
        join_args = mesonbuild.mesonlib.join_args
        if is_windows():
            test_data = [
                # examples from https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments
                (r'"a b c" d e', ['a b c', 'd', 'e'], True),
                (r'"ab\"c" "\\" d', ['ab"c', '\\', 'd'], False),
                (r'a\\\b d"e f"g h', [r'a\\\b', 'de fg', 'h'], False),
                (r'a\\\"b c d', [r'a\"b', 'c', 'd'], False),
                (r'a\\\\"b c" d e', [r'a\\b c', 'd', 'e'], False),
                # other basics
                (r'""', [''], True),
                (r'a b c d "" e', ['a', 'b', 'c', 'd', '', 'e'], True),
                (r"'a b c' d e", ["'a", 'b', "c'", 'd', 'e'], True),
                (r"'a&b&c' d e", ["'a&b&c'", 'd', 'e'], True),
                (r"a & b & c d e", ['a', '&', 'b', '&', 'c', 'd', 'e'], True),
                (r"'a & b & c d e'", ["'a", '&', 'b', '&', 'c', 'd', "e'"], True),
                ('a  b\nc\rd \n\re', ['a', 'b', 'c', 'd', 'e'], False),
                # more illustrative tests
                (r'cl test.cpp /O1 /Fe:test.exe', ['cl', 'test.cpp', '/O1', '/Fe:test.exe'], True),
                (r'cl "test.cpp /O1 /Fe:test.exe"', ['cl', 'test.cpp /O1 /Fe:test.exe'], True),
                (r'cl /DNAME=\"Bob\" test.cpp', ['cl', '/DNAME="Bob"', 'test.cpp'], False),
                (r'cl "/DNAME=\"Bob\"" test.cpp', ['cl', '/DNAME="Bob"', 'test.cpp'], True),
                (r'cl /DNAME=\"Bob, Alice\" test.cpp', ['cl', '/DNAME="Bob,', 'Alice"', 'test.cpp'], False),
                (r'cl "/DNAME=\"Bob, Alice\"" test.cpp', ['cl', '/DNAME="Bob, Alice"', 'test.cpp'], True),
                (r'cl C:\path\with\backslashes.cpp', ['cl', r'C:\path\with\backslashes.cpp'], True),
                (r'cl C:\\path\\with\\double\\backslashes.cpp', ['cl', r'C:\\path\\with\\double\\backslashes.cpp'], True),
                (r'cl "C:\\path\\with\\double\\backslashes.cpp"', ['cl', r'C:\\path\\with\\double\\backslashes.cpp'], False),
                (r'cl C:\path with spaces\test.cpp', ['cl', r'C:\path', 'with', r'spaces\test.cpp'], False),
                (r'cl "C:\path with spaces\test.cpp"', ['cl', r'C:\path with spaces\test.cpp'], True),
                (r'cl /DPATH="C:\path\with\backslashes test.cpp', ['cl', r'/DPATH=C:\path\with\backslashes test.cpp'], False),
                (r'cl /DPATH=\"C:\\ends\\with\\backslashes\\\" test.cpp', ['cl', r'/DPATH="C:\\ends\\with\\backslashes\"', 'test.cpp'], False),
                (r'cl /DPATH="C:\\ends\\with\\backslashes\\" test.cpp', ['cl', '/DPATH=C:\\\\ends\\\\with\\\\backslashes\\', 'test.cpp'], False),
                (r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\"', 'test.cpp'], True),
                (r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\\ test.cpp'], False),
                (r'cl "/DNAME=\"C:\\ends\\with\\backslashes\\\\\"" test.cpp', ['cl', r'/DNAME="C:\\ends\\with\\backslashes\\"', 'test.cpp'], True),
            ]
        else:
            test_data = [
                (r"'a b c' d e", ['a b c', 'd', 'e'], True),
                (r"a/b/c d e", ['a/b/c', 'd', 'e'], True),
                (r"a\b\c d e", [r'abc', 'd', 'e'], False),
                (r"a\\b\\c d e", [r'a\b\c', 'd', 'e'], False),
                (r'"a b c" d e', ['a b c', 'd', 'e'], False),
                (r'"a\\b\\c\\" d e', ['a\\b\\c\\', 'd', 'e'], False),
                (r"'a\b\c\' d e", ['a\\b\\c\\', 'd', 'e'], True),
                (r"'a&b&c' d e", ['a&b&c', 'd', 'e'], True),
                (r"a & b & c d e", ['a', '&', 'b', '&', 'c', 'd', 'e'], False),
                (r"'a & b & c d e'", ['a & b & c d e'], True),
                (r"abd'e f'g h", [r'abde fg', 'h'], False),
                ('a  b\nc\rd \n\re', ['a', 'b', 'c', 'd', 'e'], False),

                ('g++ -DNAME="Bob" test.cpp', ['g++', '-DNAME=Bob', 'test.cpp'], False),
                ("g++ '-DNAME=\"Bob\"' test.cpp", ['g++', '-DNAME="Bob"', 'test.cpp'], True),
                ('g++ -DNAME="Bob, Alice" test.cpp', ['g++', '-DNAME=Bob, Alice', 'test.cpp'], False),
                ("g++ '-DNAME=\"Bob, Alice\"' test.cpp", ['g++', '-DNAME="Bob, Alice"', 'test.cpp'], True),
            ]

        for (cmd, expected, roundtrip) in test_data:
            self.assertEqual(split_args(cmd), expected)
            if roundtrip:
                self.assertEqual(join_args(expected), cmd)

    def test_quote_arg(self):
        split_args = mesonbuild.mesonlib.split_args
        quote_arg = mesonbuild.mesonlib.quote_arg
        if is_windows():
            test_data = [
                ('', '""'),
                ('arg1', 'arg1'),
                ('/option1', '/option1'),
                ('/Ovalue', '/Ovalue'),
                ('/OBob&Alice', '/OBob&Alice'),
                ('/Ovalue with spaces', r'"/Ovalue with spaces"'),
                (r'/O"value with spaces"', r'"/O\"value with spaces\""'),
                (r'/OC:\path with spaces\test.exe', r'"/OC:\path with spaces\test.exe"'),
                ('/LIBPATH:C:\\path with spaces\\ends\\with\\backslashes\\', r'"/LIBPATH:C:\path with spaces\ends\with\backslashes\\"'),
                ('/LIBPATH:"C:\\path with spaces\\ends\\with\\backslashes\\\\"', r'"/LIBPATH:\"C:\path with spaces\ends\with\backslashes\\\\\""'),
                (r'/DMSG="Alice said: \"Let\'s go\""', r'"/DMSG=\"Alice said: \\\"Let\'s go\\\"\""'),
            ]
        else:
            test_data = [
                ('arg1', 'arg1'),
                ('--option1', '--option1'),
                ('-O=value', '-O=value'),
                ('-O=Bob&Alice', "'-O=Bob&Alice'"),
                ('-O=value with spaces', "'-O=value with spaces'"),
                ('-O="value with spaces"', '\'-O=\"value with spaces\"\''),
                ('-O=/path with spaces/test', '\'-O=/path with spaces/test\''),
                ('-DMSG="Alice said: \\"Let\'s go\\""', "'-DMSG=\"Alice said: \\\"Let'\"'\"'s go\\\"\"'"),
            ]

        for (arg, expected) in test_data:
            self.assertEqual(quote_arg(arg), expected)
            self.assertEqual(split_args(expected)[0], arg)

    def test_depfile(self):
        for (f, target, expdeps) in [
                # empty, unknown target
                ([''], 'unknown', set()),
                # simple target & deps
                (['meson/foo.o  : foo.c   foo.h'], 'meson/foo.o', set({'foo.c', 'foo.h'})),
                (['meson/foo.o: foo.c foo.h'], 'foo.c', set()),
                # get all deps
                (['meson/foo.o: foo.c foo.h',
                  'foo.c: gen.py'], 'meson/foo.o', set({'foo.c', 'foo.h', 'gen.py'})),
                (['meson/foo.o: foo.c foo.h',
                  'foo.c: gen.py'], 'foo.c', set({'gen.py'})),
                # linue continuation, multiple targets
                (['foo.o \\', 'foo.h: bar'], 'foo.h', set({'bar'})),
                (['foo.o \\', 'foo.h: bar'], 'foo.o', set({'bar'})),
                # \\ handling
                (['foo: Program\\ F\\iles\\\\X'], 'foo', set({'Program Files\\X'})),
                # $ handling
                (['f$o.o: c/b'], 'f$o.o', set({'c/b'})),
                (['f$$o.o: c/b'], 'f$o.o', set({'c/b'})),
                # cycles
                (['a: b', 'b: a'], 'a', set({'a', 'b'})),
                (['a: b', 'b: a'], 'b', set({'a', 'b'})),
        ]:
            d = mesonbuild.depfile.DepFile(f)
            deps = d.get_all_dependencies(target)
            self.assertEqual(sorted(deps), sorted(expdeps))

    def test_log_once(self):
        f = io.StringIO()
        with mock.patch('mesonbuild.mlog._logger.log_file', f), \
                mock.patch('mesonbuild.mlog._logger.logged_once', set()):
            mesonbuild.mlog.log('foo', once=True)
            mesonbuild.mlog.log('foo', once=True)
            actual = f.getvalue().strip()
            self.assertEqual(actual, 'foo', actual)

    def test_log_once_ansi(self):
        f = io.StringIO()
        with mock.patch('mesonbuild.mlog._logger.log_file', f), \
                mock.patch('mesonbuild.mlog._logger.logged_once', set()):
            mesonbuild.mlog.log(mesonbuild.mlog.bold('foo'), once=True)
            mesonbuild.mlog.log(mesonbuild.mlog.bold('foo'), once=True)
            actual = f.getvalue().strip()
            self.assertEqual(actual.count('foo'), 1, actual)

            mesonbuild.mlog.log('foo', once=True)
            actual = f.getvalue().strip()
            self.assertEqual(actual.count('foo'), 1, actual)

            f.truncate()

            mesonbuild.mlog.warning('bar', once=True)
            mesonbuild.mlog.warning('bar', once=True)
            actual = f.getvalue().strip()
            self.assertEqual(actual.count('bar'), 1, actual)

    def test_sort_libpaths(self):
        sort_libpaths = mesonbuild.dependencies.base.sort_libpaths
        self.assertEqual(sort_libpaths(
            ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'],
            ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']),
            ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])
        self.assertEqual(sort_libpaths(
            ['/usr/local/lib', '/home/mesonuser/.local/lib', '/usr/lib'],
            ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']),
            ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])
        self.assertEqual(sort_libpaths(
            ['/usr/lib', '/usr/local/lib', '/home/mesonuser/.local/lib'],
            ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/lib/pkgconfig']),
            ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])
        self.assertEqual(sort_libpaths(
            ['/usr/lib', '/usr/local/lib', '/home/mesonuser/.local/lib'],
            ['/home/mesonuser/.local/lib/pkgconfig', '/usr/local/libdata/pkgconfig']),
            ['/home/mesonuser/.local/lib', '/usr/local/lib', '/usr/lib'])

    def test_dependency_factory_order(self):
        b = mesonbuild.dependencies.base
        F = mesonbuild.dependencies.factory
        with tempfile.TemporaryDirectory() as tmpdir:
            with chdir(tmpdir):
                env = get_fake_env()
                env.scratch_dir = tmpdir

                f = F.DependencyFactory(
                    'test_dep',
                    methods=[b.DependencyMethods.PKGCONFIG, b.DependencyMethods.CMAKE]
                )
                actual = [m() for m in f(env, {'required': False, 'native': MachineChoice.HOST})]
                self.assertListEqual([m.type_name for m in actual], ['pkgconfig', 'cmake'])

                f = F.DependencyFactory(
                    'test_dep',
                    methods=[b.DependencyMethods.CMAKE, b.DependencyMethods.PKGCONFIG]
                )
                actual = [m() for m in f(env, {'required': False, 'native': MachineChoice.HOST})]
                self.assertListEqual([m.type_name for m in actual], ['cmake', 'pkgconfig'])

    def test_validate_json(self) -> None:
        """Validate the json schema for the test cases."""
        try:
            from fastjsonschema import compile, JsonSchemaValueException as JsonSchemaFailure
            fast = True
        except ImportError:
            try:
                from jsonschema import validate, ValidationError as JsonSchemaFailure
                fast = False
            except:
                if IS_CI:
                    raise
                raise unittest.SkipTest('neither Python fastjsonschema nor jsonschema module not found.')

        with open('data/test.schema.json', 'r', encoding='utf-8') as f:
            data = json.loads(f.read())

        if fast:
            schema_validator = compile(data)
        else:
            schema_validator = lambda x: validate(x, schema=data)

        errors: T.List[T.Tuple[Path, Exception]] = []
        for p in Path('test cases').glob('**/test.json'):
            try:
                schema_validator(json.loads(p.read_text(encoding='utf-8')))
            except JsonSchemaFailure as e:
                errors.append((p.resolve(), e))

        for f, e in errors:
            print(f'Failed to validate: "{f}"')
            print(str(e))

        self.assertFalse(errors)

    def test_typed_pos_args_types(self) -> None:
        @typed_pos_args('foo', str, int, bool)
        def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None:
            self.assertIsInstance(args, tuple)
            self.assertIsInstance(args[0], str)
            self.assertIsInstance(args[1], int)
            self.assertIsInstance(args[2], bool)

        _(None, mock.Mock(), ['string', 1, False], None)

    def test_typed_pos_args_types_invalid(self) -> None:
        @typed_pos_args('foo', str, int, bool)
        def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None:
            self.assertTrue(False)  # should not be reachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), ['string', 1.0, False], None)
        self.assertEqual(str(cm.exception), 'foo argument 2 was of type "float" but should have been "int"')

    def test_typed_pos_args_types_wrong_number(self) -> None:
        @typed_pos_args('foo', str, int, bool)
        def _(obj, node, args: T.Tuple[str, int, bool], kwargs) -> None:
            self.assertTrue(False)  # should not be reachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), ['string', 1], None)
        self.assertEqual(str(cm.exception), 'foo takes exactly 3 arguments, but got 2.')

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), ['string', 1, True, True], None)
        self.assertEqual(str(cm.exception), 'foo takes exactly 3 arguments, but got 4.')

    def test_typed_pos_args_varargs(self) -> None:
        @typed_pos_args('foo', str, varargs=str)
        def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
            self.assertIsInstance(args, tuple)
            self.assertIsInstance(args[0], str)
            self.assertIsInstance(args[1], list)
            self.assertIsInstance(args[1][0], str)
            self.assertIsInstance(args[1][1], str)

        _(None, mock.Mock(), ['string', 'var', 'args'], None)

    def test_typed_pos_args_varargs_not_given(self) -> None:
        @typed_pos_args('foo', str, varargs=str)
        def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
            self.assertIsInstance(args, tuple)
            self.assertIsInstance(args[0], str)
            self.assertIsInstance(args[1], list)
            self.assertEqual(args[1], [])

        _(None, mock.Mock(), ['string'], None)

    def test_typed_pos_args_varargs_invalid(self) -> None:
        @typed_pos_args('foo', str, varargs=str)
        def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
            self.assertTrue(False)  # should not be reachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), ['string', 'var', 'args', 0], None)
        self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been "str"')

    def test_typed_pos_args_varargs_invalid_multiple_types(self) -> None:
        @typed_pos_args('foo', str, varargs=(str, list))
        def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
            self.assertTrue(False)  # should not be reachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), ['string', 'var', 'args', 0], None)
        self.assertEqual(str(cm.exception), 'foo argument 4 was of type "int" but should have been one of: "str", "list"')

    def test_typed_pos_args_max_varargs(self) -> None:
        @typed_pos_args('foo', str, varargs=str, max_varargs=5)
        def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
            self.assertIsInstance(args, tuple)
            self.assertIsInstance(args[0], str)
            self.assertIsInstance(args[1], list)
            self.assertIsInstance(args[1][0], str)
            self.assertIsInstance(args[1][1], str)

        _(None, mock.Mock(), ['string', 'var', 'args'], None)

    def test_typed_pos_args_max_varargs_exceeded(self) -> None:
        @typed_pos_args('foo', str, varargs=str, max_varargs=1)
        def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None:
            self.assertTrue(False)  # should not be reachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), ['string', 'var', 'args'], None)
        self.assertEqual(str(cm.exception), 'foo takes between 1 and 2 arguments, but got 3.')

    def test_typed_pos_args_min_varargs(self) -> None:
        @typed_pos_args('foo', varargs=str, max_varargs=2, min_varargs=1)
        def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
            self.assertIsInstance(args, tuple)
            self.assertIsInstance(args[0], list)
            self.assertIsInstance(args[0][0], str)
            self.assertIsInstance(args[0][1], str)

        _(None, mock.Mock(), ['string', 'var'], None)

    def test_typed_pos_args_min_varargs_not_met(self) -> None:
        @typed_pos_args('foo', str, varargs=str, min_varargs=1)
        def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
            self.assertTrue(False)  # should not be reachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), ['string'], None)
        self.assertEqual(str(cm.exception), 'foo takes at least 2 arguments, but got 1.')

    def test_typed_pos_args_min_and_max_varargs_exceeded(self) -> None:
        @typed_pos_args('foo', str, varargs=str, min_varargs=1, max_varargs=2)
        def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None:
            self.assertTrue(False)  # should not be reachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), ['string', 'var', 'args', 'bar'], None)
        self.assertEqual(str(cm.exception), 'foo takes between 2 and 3 arguments, but got 4.')

    def test_typed_pos_args_min_and_max_varargs_not_met(self) -> None:
        @typed_pos_args('foo', str, varargs=str, min_varargs=1, max_varargs=2)
        def _(obj, node, args: T.Tuple[str, T.Tuple[str, ...]], kwargs) -> None:
            self.assertTrue(False)  # should not be reachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), ['string'], None)
        self.assertEqual(str(cm.exception), 'foo takes between 2 and 3 arguments, but got 1.')

    def test_typed_pos_args_variadic_and_optional(self) -> None:
        @typed_pos_args('foo', str, optargs=[str], varargs=str, min_varargs=0)
        def _(obj, node, args: T.Tuple[str, T.List[str]], kwargs) -> None:
            self.assertTrue(False)  # should not be reachable

        with self.assertRaises(AssertionError) as cm:
            _(None, mock.Mock(), ['string'], None)
        self.assertEqual(
            str(cm.exception),
            'varargs and optargs not supported together as this would be ambiguous')

    def test_typed_pos_args_min_optargs_not_met(self) -> None:
        @typed_pos_args('foo', str, str, optargs=[str])
        def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
            self.assertTrue(False)  # should not be reachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), ['string'], None)
        self.assertEqual(str(cm.exception), 'foo takes at least 2 arguments, but got 1.')

    def test_typed_pos_args_min_optargs_max_exceeded(self) -> None:
        @typed_pos_args('foo', str, optargs=[str])
        def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
            self.assertTrue(False)  # should not be reachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), ['string', '1', '2'], None)
        self.assertEqual(str(cm.exception), 'foo takes at most 2 arguments, but got 3.')

    def test_typed_pos_args_optargs_not_given(self) -> None:
        @typed_pos_args('foo', str, optargs=[str])
        def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
            self.assertEqual(len(args), 2)
            self.assertIsInstance(args[0], str)
            self.assertEqual(args[0], 'string')
            self.assertIsNone(args[1])

        _(None, mock.Mock(), ['string'], None)

    def test_typed_pos_args_optargs_some_given(self) -> None:
        @typed_pos_args('foo', str, optargs=[str, int])
        def _(obj, node, args: T.Tuple[str, T.Optional[str], T.Optional[int]], kwargs) -> None:
            self.assertEqual(len(args), 3)
            self.assertIsInstance(args[0], str)
            self.assertEqual(args[0], 'string')
            self.assertIsInstance(args[1], str)
            self.assertEqual(args[1], '1')
            self.assertIsNone(args[2])

        _(None, mock.Mock(), ['string', '1'], None)

    def test_typed_pos_args_optargs_all_given(self) -> None:
        @typed_pos_args('foo', str, optargs=[str])
        def _(obj, node, args: T.Tuple[str, T.Optional[str]], kwargs) -> None:
            self.assertEqual(len(args), 2)
            self.assertIsInstance(args[0], str)
            self.assertEqual(args[0], 'string')
            self.assertIsInstance(args[1], str)

        _(None, mock.Mock(), ['string', '1'], None)

    def test_typed_kwarg_basic(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', str, default='')
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
            self.assertIsInstance(kwargs['input'], str)
            self.assertEqual(kwargs['input'], 'foo')

        _(None, mock.Mock(), [], {'input': 'foo'})

    def test_typed_kwarg_missing_required(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', str, required=True),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
            self.assertTrue(False)  # should be unreachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), [], {})
        self.assertEqual(str(cm.exception), 'testfunc is missing required keyword argument "input"')

    def test_typed_kwarg_missing_optional(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', (str, type(None))),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.Optional[str]]) -> None:
            self.assertIsNone(kwargs['input'])

        _(None, mock.Mock(), [], {})

    def test_typed_kwarg_default(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', str, default='default'),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
            self.assertEqual(kwargs['input'], 'default')

        _(None, mock.Mock(), [], {})

    def test_typed_kwarg_container_valid(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', ContainerTypeInfo(list, str), default=[], required=True),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None:
            self.assertEqual(kwargs['input'], ['str'])

        _(None, mock.Mock(), [], {'input': ['str']})

    def test_typed_kwarg_container_invalid(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', ContainerTypeInfo(list, str), required=True),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None:
            self.assertTrue(False)  # should be unreachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), [], {'input': {}})
        self.assertEqual(str(cm.exception), "testfunc keyword argument 'input' was of type dict[] but should have been array[str]")

    def test_typed_kwarg_contained_invalid(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', ContainerTypeInfo(dict, str), required=True),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.Dict[str, str]]) -> None:
            self.assertTrue(False)  # should be unreachable

        with self.assertRaises(InvalidArguments) as cm:
            _(None, mock.Mock(), [], {'input': {'key': 1, 'bar': 2}})
        self.assertEqual(str(cm.exception), "testfunc keyword argument 'input' was of type dict[int] but should have been dict[str]")

    def test_typed_kwarg_container_listify(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', ContainerTypeInfo(list, str), default=[], listify=True),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None:
            self.assertEqual(kwargs['input'], ['str'])

        _(None, mock.Mock(), [], {'input': 'str'})

    def test_typed_kwarg_container_default_copy(self) -> None:
        default: T.List[str] = []
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', ContainerTypeInfo(list, str), listify=True, default=default),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None:
            self.assertIsNot(kwargs['input'], default)

        _(None, mock.Mock(), [], {})

    def test_typed_kwarg_container_pairs(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', ContainerTypeInfo(list, str, pairs=True), listify=True),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, T.List[str]]) -> None:
            self.assertEqual(kwargs['input'], ['a', 'b'])

        _(None, mock.Mock(), [], {'input': ['a', 'b']})

        with self.assertRaises(MesonException) as cm:
            _(None, mock.Mock(), [], {'input': ['a']})
        self.assertEqual(str(cm.exception), "testfunc keyword argument 'input' was of type array[str] but should have been array[str] that has even size")

    def test_typed_kwarg_since(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', str, since='1.0', since_message='It\'s awesome, use it',
                      deprecated='2.0', deprecated_message='It\'s terrible, don\'t use it')
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
            self.assertIsInstance(kwargs['input'], str)
            self.assertEqual(kwargs['input'], 'foo')

        with self.subTest('use before available'), \
                mock.patch('sys.stdout', io.StringIO()) as out, \
                mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': '0.1'}):
            # With Meson 0.1 it should trigger the "introduced" warning but not the "deprecated" warning
            _(None, mock.Mock(subproject=''), [], {'input': 'foo'})
            self.assertRegex(out.getvalue(), r'WARNING:.*introduced.*input arg in testfunc. It\'s awesome, use it')
            self.assertNotRegex(out.getvalue(), r'WARNING:.*deprecated.*input arg in testfunc. It\'s terrible, don\'t use it')

        with self.subTest('no warnings should be triggered'), \
                mock.patch('sys.stdout', io.StringIO()) as out, \
                mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': '1.5'}):
            # With Meson 1.5 it shouldn't trigger any warning
            _(None, mock.Mock(subproject=''), [], {'input': 'foo'})
            self.assertNotRegex(out.getvalue(), r'WARNING:.*')

        with self.subTest('use after deprecated'), \
                mock.patch('sys.stdout', io.StringIO()) as out, \
                mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': '2.0'}):
            # With Meson 2.0 it should trigger the "deprecated" warning but not the "introduced" warning
            _(None, mock.Mock(subproject=''), [], {'input': 'foo'})
            self.assertRegex(out.getvalue(), r'WARNING:.*deprecated.*input arg in testfunc. It\'s terrible, don\'t use it')
            self.assertNotRegex(out.getvalue(), r'WARNING:.*introduced.*input arg in testfunc. It\'s awesome, use it')

    def test_typed_kwarg_validator(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', str, default='', validator=lambda x: 'invalid!' if x != 'foo' else None)
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
            pass

        # Should be valid
        _(None, mock.Mock(), tuple(), dict(input='foo'))

        with self.assertRaises(MesonException) as cm:
            _(None, mock.Mock(), tuple(), dict(input='bar'))
        self.assertEqual(str(cm.exception), "testfunc keyword argument \"input\" invalid!")

    def test_typed_kwarg_convertor(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('native', bool, default=False, convertor=lambda n: MachineChoice.BUILD if n else MachineChoice.HOST)
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, MachineChoice]) -> None:
            assert isinstance(kwargs['native'], MachineChoice)

        _(None, mock.Mock(), tuple(), dict(native=True))

    @mock.patch('mesonbuild.mesonlib.project_meson_versions', {'': '1.0'})
    def test_typed_kwarg_since_values(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', ContainerTypeInfo(list, str), listify=True, default=[], deprecated_values={'foo': '0.9'}, since_values={'bar': '1.1'}),
            KwargInfo('output', ContainerTypeInfo(dict, str), default={}, deprecated_values={'foo': '0.9', 'foo2': ('0.9', 'don\'t use it')}, since_values={'bar': '1.1', 'bar2': ('1.1', 'use this')}),
            KwargInfo('install_dir', (bool, str, NoneType), deprecated_values={False: '0.9'}),
            KwargInfo(
                'mode',
                (str, type(None)),
                validator=in_set_validator({'clean', 'build', 'rebuild', 'deprecated', 'since'}),
                deprecated_values={'deprecated': '1.0'},
                since_values={'since': '1.1'}),
            KwargInfo('dict', (ContainerTypeInfo(list, str), ContainerTypeInfo(dict, str)), default={},
                      since_values={list: '1.9'}),
            KwargInfo('new_dict', (ContainerTypeInfo(list, str), ContainerTypeInfo(dict, str)), default={},
                      since_values={dict: '1.1'}),
            KwargInfo('foo', (str, int, ContainerTypeInfo(list, str), ContainerTypeInfo(dict, str), ContainerTypeInfo(list, int)), default={},
                      since_values={str: '1.1', ContainerTypeInfo(list, str): '1.2', ContainerTypeInfo(dict, str): '1.3'},
                      deprecated_values={int: '0.8', ContainerTypeInfo(list, int): '0.9'}),
            KwargInfo('tuple', (ContainerTypeInfo(list, (str, int))), default=[], listify=True,
                      since_values={ContainerTypeInfo(list, str): '1.1', ContainerTypeInfo(list, int): '1.2'}),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
            pass

        with self.subTest('deprecated array string value'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'input': ['foo']})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "input" value "foo".*""")

        with self.subTest('new array string value'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'input': ['bar']})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "input" value "bar".*""")

        with self.subTest('deprecated dict string value'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'output': {'foo': 'a'}})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "output" value "foo".*""")

        with self.subTest('deprecated dict string value with msg'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'output': {'foo2': 'a'}})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "output" value "foo2" in dict keys. don't use it.*""")

        with self.subTest('new dict string value'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'output': {'bar': 'b'}})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "output" value "bar".*""")

        with self.subTest('new dict string value with msg'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'output': {'bar2': 'a'}})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "output" value "bar2" in dict keys. use this.*""")

        with self.subTest('new string type'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'foo': 'foo'})
            self.assertRegex(out.getvalue(), r"""WARNING: Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "foo" of type str.*""")

        with self.subTest('new array of string type'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'foo': ['foo']})
            self.assertRegex(out.getvalue(), r"""WARNING: Project targets '1.0'.*introduced in '1.2': "testfunc" keyword argument "foo" of type array\[str\].*""")

        with self.subTest('new dict of string type'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'foo': {'plop': 'foo'}})
            self.assertRegex(out.getvalue(), r"""WARNING: Project targets '1.0'.*introduced in '1.3': "testfunc" keyword argument "foo" of type dict\[str\].*""")

        with self.subTest('deprecated int value'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'foo': 1})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.8': "testfunc" keyword argument "foo" of type int.*""")

        with self.subTest('deprecated array int value'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'foo': [1]})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "foo" of type array\[int\].*""")

        with self.subTest('new list[str] value'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'tuple': ['foo', 42]})
            self.assertRegex(out.getvalue(), r"""WARNING: Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "tuple" of type array\[str\].*""")
            self.assertRegex(out.getvalue(), r"""WARNING: Project targets '1.0'.*introduced in '1.2': "testfunc" keyword argument "tuple" of type array\[int\].*""")

        with self.subTest('deprecated array string value'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'input': 'foo'})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "input" value "foo".*""")

        with self.subTest('new array string value'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'input': 'bar'})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "input" value "bar".*""")

        with self.subTest('non string union'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'install_dir': False})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '0.9': "testfunc" keyword argument "install_dir" value "False".*""")

        with self.subTest('deprecated string union'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'mode': 'deprecated'})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*deprecated since '1.0': "testfunc" keyword argument "mode" value "deprecated".*""")

        with self.subTest('new string union'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'mode': 'since'})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "mode" value "since".*""")

        with self.subTest('new container'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'dict': ['a=b']})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.9': "testfunc" keyword argument "dict" of type list.*""")

        with self.subTest('new container set to default'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {'new_dict': {}})
            self.assertRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "new_dict" of type dict.*""")

        with self.subTest('new container default'), mock.patch('sys.stdout', io.StringIO()) as out:
            _(None, mock.Mock(subproject=''), [], {})
            self.assertNotRegex(out.getvalue(), r"""WARNING:.Project targets '1.0'.*introduced in '1.1': "testfunc" keyword argument "new_dict" of type dict.*""")

    def test_typed_kwarg_evolve(self) -> None:
        k = KwargInfo('foo', str, required=True, default='foo')
        v = k.evolve(default='bar')
        self.assertEqual(k.name, 'foo')
        self.assertEqual(k.name, v.name)
        self.assertEqual(k.types, str)
        self.assertEqual(k.types, v.types)
        self.assertEqual(k.required, True)
        self.assertEqual(k.required, v.required)
        self.assertEqual(k.default, 'foo')
        self.assertEqual(v.default, 'bar')

    def test_typed_kwarg_default_type(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('no_default', (str, ContainerTypeInfo(list, str), NoneType)),
            KwargInfo('str_default', (str, ContainerTypeInfo(list, str)), default=''),
            KwargInfo('list_default', (str, ContainerTypeInfo(list, str)), default=['']),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
            self.assertEqual(kwargs['no_default'], None)
            self.assertEqual(kwargs['str_default'], '')
            self.assertEqual(kwargs['list_default'], [''])
        _(None, mock.Mock(), [], {})

    def test_typed_kwarg_invalid_default_type(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('invalid_default', (str, ContainerTypeInfo(list, str), NoneType), default=42),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
            pass
        self.assertRaises(AssertionError, _, None, mock.Mock(), [], {})

    def test_typed_kwarg_container_in_tuple(self) -> None:
        @typed_kwargs(
            'testfunc',
            KwargInfo('input', (str, ContainerTypeInfo(list, str))),
        )
        def _(obj, node, args: T.Tuple, kwargs: T.Dict[str, str]) -> None:
            self.assertEqual(kwargs['input'], args[0])
        _(None, mock.Mock(), [''], {'input': ''})
        _(None, mock.Mock(), [['']], {'input': ['']})
        self.assertRaises(InvalidArguments, _, None, mock.Mock(), [], {'input': 42})

    def test_detect_cpu_family(self) -> None:
        """Test the various cpu families that we detect and normalize.

        This is particularly useful as both documentation, and to keep testing
        platforms that are less common.
        """

        @contextlib.contextmanager
        def mock_trial(value: str) -> T.Iterable[None]:
            """Mock all of the ways we could get the trial at once."""
            mocked = mock.Mock(return_value=value)

            with mock.patch('mesonbuild.envconfig.detect_windows_arch', mocked), \
                    mock.patch('mesonbuild.envconfig.platform.processor', mocked), \
                    mock.patch('mesonbuild.envconfig.platform.machine', mocked):
                yield

        cases = [
            ('x86', 'x86'),
            ('i386', 'x86'),
            ('bepc', 'x86'),  # Haiku
            ('earm', 'arm'),  # NetBSD
            ('arm', 'arm'),
            ('ppc64', 'ppc64'),
            ('powerpc64', 'ppc64'),
            ('powerpc', 'ppc'),
            ('ppc', 'ppc'),
            ('macppc', 'ppc'),
            ('power macintosh', 'ppc'),
            ('mips64el', 'mips'),
            ('mips64', 'mips'),
            ('mips', 'mips'),
            ('mipsel', 'mips'),
            ('ip30', 'mips'),
            ('ip35', 'mips'),
            ('parisc64', 'parisc'),
            ('sun4u', 'sparc64'),
            ('sun4v', 'sparc64'),
            ('amd64', 'x86_64'),
            ('x64', 'x86_64'),
            ('i86pc', 'x86_64'),  # Solaris
            ('aarch64', 'aarch64'),
            ('aarch64_be', 'aarch64'),
        ]

        cc = ClangCCompiler([], [], 'fake', MachineChoice.HOST, get_fake_env())

        with mock.patch('mesonbuild.envconfig.any_compiler_has_define', mock.Mock(return_value=False)):
            for test, expected in cases:
                with self.subTest(test, has_define=False), mock_trial(test):
                    actual = mesonbuild.envconfig.detect_cpu_family({'c': cc})
                    self.assertEqual(actual, expected)

        with mock.patch('mesonbuild.envconfig.any_compiler_has_define', mock.Mock(return_value=True)):
            for test, expected in [('x86_64', 'x86'), ('aarch64', 'arm'), ('ppc', 'ppc64'), ('mips64', 'mips64')]:
                with self.subTest(test, has_define=True), mock_trial(test):
                    actual = mesonbuild.envconfig.detect_cpu_family({'c': cc})
                    self.assertEqual(actual, expected)

        # machine_info_can_run calls detect_cpu_family with no compilers at all
        with mock.patch(
            'mesonbuild.envconfig.any_compiler_has_define',
            mock.Mock(side_effect=AssertionError('Should not be called')),
        ):
            for test, expected in [('mips64', 'mips64')]:
                with self.subTest(test, has_compiler=False), mock_trial(test):
                    actual = mesonbuild.envconfig.detect_cpu_family({})
                    self.assertEqual(actual, expected)

    def test_detect_cpu(self) -> None:

        @contextlib.contextmanager
        def mock_trial(value: str) -> T.Iterable[None]:
            """Mock all of the ways we could get the trial at once."""
            mocked = mock.Mock(return_value=value)

            with mock.patch('mesonbuild.envconfig.detect_windows_arch', mocked), \
                    mock.patch('mesonbuild.envconfig.platform.processor', mocked), \
                    mock.patch('mesonbuild.envconfig.platform.machine', mocked):
                yield

        cases = [
            ('amd64', 'x86_64'),
            ('x64', 'x86_64'),
            ('i86pc', 'x86_64'),
            ('earm', 'arm'),
            ('mips64el', 'mips'),
            ('mips64', 'mips'),
            ('mips', 'mips'),
            ('mipsel', 'mips'),
            ('aarch64', 'aarch64'),
            ('aarch64_be', 'aarch64'),
        ]

        cc = ClangCCompiler([], [], 'fake', MachineChoice.HOST, get_fake_env())

        with mock.patch('mesonbuild.envconfig.any_compiler_has_define', mock.Mock(return_value=False)):
            for test, expected in cases:
                with self.subTest(test, has_define=False), mock_trial(test):
                    actual = mesonbuild.envconfig.detect_cpu({'c': cc})
                    self.assertEqual(actual, expected)

        with mock.patch('mesonbuild.envconfig.any_compiler_has_define', mock.Mock(return_value=True)):
            for test, expected in [('x86_64', 'i686'), ('aarch64', 'arm'), ('ppc', 'ppc64'), ('mips64', 'mips64')]:
                with self.subTest(test, has_define=True), mock_trial(test):
                    actual = mesonbuild.envconfig.detect_cpu({'c': cc})
                    self.assertEqual(actual, expected)

        with mock.patch(
            'mesonbuild.envconfig.any_compiler_has_define',
            mock.Mock(side_effect=AssertionError('Should not be called')),
        ):
            for test, expected in [('mips64', 'mips64')]:
                with self.subTest(test, has_compiler=False), mock_trial(test):
                    actual = mesonbuild.envconfig.detect_cpu({})
                    self.assertEqual(actual, expected)

    @mock.patch('mesonbuild.interpreter.Interpreter.load_root_meson_file', mock.Mock(return_value=None))
    @mock.patch('mesonbuild.interpreter.Interpreter.sanity_check_ast', mock.Mock(return_value=None))
    @mock.patch('mesonbuild.interpreter.Interpreter.parse_project', mock.Mock(return_value=None))
    def test_interpreter_unpicklable(self) -> None:
        build = mock.Mock()
        build.environment = mock.Mock()
        build.environment.get_source_dir = mock.Mock(return_value='')
        with mock.patch('mesonbuild.interpreter.Interpreter._redetect_machines', mock.Mock()), \
                self.assertRaises(mesonbuild.mesonlib.MesonBugException):
            i = mesonbuild.interpreter.Interpreter(build)
            pickle.dumps(i)

    def test_major_versions_differ(self) -> None:
        # Return True when going to next major release, when going to dev cycle,
        # when going to rc cycle or when going out of rc cycle.
        self.assertTrue(coredata.major_versions_differ('0.59.0', '0.60.0'))
        self.assertTrue(coredata.major_versions_differ('0.59.0', '0.59.99'))
        self.assertTrue(coredata.major_versions_differ('0.59.0', '0.60.0.rc1'))
        self.assertTrue(coredata.major_versions_differ('0.59.99', '0.60.0.rc1'))
        self.assertTrue(coredata.major_versions_differ('0.60.0.rc1', '0.60.0'))
        # Return False when going to next point release or when staying in dev/rc cycle.
        self.assertFalse(coredata.major_versions_differ('0.60.0', '0.60.0'))
        self.assertFalse(coredata.major_versions_differ('0.60.0', '0.60.1'))
        self.assertFalse(coredata.major_versions_differ('0.59.99', '0.59.99'))
        self.assertFalse(coredata.major_versions_differ('0.60.0.rc1', '0.60.0.rc2'))

    def test_option_key_from_string(self) -> None:
        cases = [
            ('c_args', OptionKey('c_args')),
            ('build.cpp_args', OptionKey('cpp_args', machine=MachineChoice.BUILD)),
            ('prefix', OptionKey('prefix')),
            ('made_up', OptionKey('made_up')),

            # TODO: the from_String method should be splitting the prefix off of
            # these, as we have the type already, but it doesn't. For now have a
            # test so that we don't change the behavior un-intentionally
            ('b_lto', OptionKey('b_lto')),
            ('backend_startup_project', OptionKey('backend_startup_project')),
        ]

        for raw, expected in cases:
            with self.subTest(raw):
                self.assertEqual(OptionKey.from_string(raw), expected)

    def test_env2mfile_deb(self) -> None:
        MachineInfo = mesonbuild.scripts.env2mfile.MachineInfo
        to_machine_info = mesonbuild.scripts.env2mfile.dpkg_architecture_to_machine_info

        # For testing purposes, behave as though all cross-programs
        # exist in /usr/bin
        def locate_path(program: str) -> T.List[str]:
            if os.path.isabs(program):
                return [program]
            return ['/usr/bin/' + program]

        def expected_compilers(
            gnu_tuple: str,
            gcc_suffix: str = '',
        ) -> T.Dict[str, T.List[str]]:
            return {
                'c': [f'/usr/bin/{gnu_tuple}-gcc{gcc_suffix}'],
                'cpp': [f'/usr/bin/{gnu_tuple}-g++{gcc_suffix}'],
                'objc': [f'/usr/bin/{gnu_tuple}-gobjc{gcc_suffix}'],
                'objcpp': [f'/usr/bin/{gnu_tuple}-gobjc++{gcc_suffix}'],
                'vala': [f'/usr/bin/{gnu_tuple}-valac'],
            }

        def expected_binaries(gnu_tuple: str) -> T.Dict[str, T.List[str]]:
            return {
                'ar': [f'/usr/bin/{gnu_tuple}-ar'],
                'strip': [f'/usr/bin/{gnu_tuple}-strip'],
                'objcopy': [f'/usr/bin/{gnu_tuple}-objcopy'],
                'ld': [f'/usr/bin/{gnu_tuple}-ld'],
                'cmake': ['/usr/bin/cmake'],
                'pkg-config': [f'/usr/bin/{gnu_tuple}-pkg-config'],
                'cups-config': ['/usr/bin/cups-config'],
                'exe_wrapper': [f'/usr/bin/{gnu_tuple}-cross-exe-wrapper'],
                'g-ir-annotation-tool': [f'/usr/bin/{gnu_tuple}-g-ir-annotation-tool'],
                'g-ir-compiler': [f'/usr/bin/{gnu_tuple}-g-ir-compiler'],
                'g-ir-doc-tool': [f'/usr/bin/{gnu_tuple}-g-ir-doc-tool'],
                'g-ir-generate': [f'/usr/bin/{gnu_tuple}-g-ir-generate'],
                'g-ir-inspect': [f'/usr/bin/{gnu_tuple}-g-ir-inspect'],
                'g-ir-scanner': [f'/usr/bin/{gnu_tuple}-g-ir-scanner'],
                'vapigen': [f'/usr/bin/{gnu_tuple}-vapigen'],
            }

        for title, dpkg_arch, gccsuffix, env, expected in [
            (
                # s390x is an example of the common case where the
                # Meson CPU name, the GNU CPU name, the dpkg architecture
                # name and uname -m all agree.
                # (alpha, m68k, ppc64, riscv64, sh4, sparc64 are similar)
                's390x-linux-gnu',
                # Output of `dpkg-architecture -a...`, filtered to
                # only the DEB_HOST_ parts because that's all we use
                textwrap.dedent(
                    '''
                    DEB_HOST_ARCH=s390x
                    DEB_HOST_ARCH_ABI=base
                    DEB_HOST_ARCH_BITS=64
                    DEB_HOST_ARCH_CPU=s390x
                    DEB_HOST_ARCH_ENDIAN=big
                    DEB_HOST_ARCH_LIBC=gnu
                    DEB_HOST_ARCH_OS=linux
                    DEB_HOST_GNU_CPU=s390x
                    DEB_HOST_GNU_SYSTEM=linux-gnu
                    DEB_HOST_GNU_TYPE=s390x-linux-gnu
                    DEB_HOST_MULTIARCH=s390x-linux-gnu
                    '''
                ),
                '',
                {'PATH': '/usr/bin'},
                MachineInfo(
                    compilers=expected_compilers('s390x-linux-gnu'),
                    binaries=expected_binaries('s390x-linux-gnu'),
                    properties={},
                    compile_args={},
                    link_args={},
                    cmake={
                        'CMAKE_C_COMPILER': ['/usr/bin/s390x-linux-gnu-gcc'],
                        'CMAKE_CXX_COMPILER': ['/usr/bin/s390x-linux-gnu-g++'],
                        'CMAKE_SYSTEM_NAME': 'Linux',
                        'CMAKE_SYSTEM_PROCESSOR': 's390x',
                    },
                    system='linux',
                    subsystem='linux',
                    kernel='linux',
                    cpu='s390x',
                    cpu_family='s390x',
                    endian='big',
                ),
            ),
            # Debian amd64 vs. GNU, Meson, etc. x86_64.
            # arm64/aarch64, hppa/parisc, i386/i686/x86, loong64/loongarch64,
            # powerpc/ppc are similar.
            (
                'x86_64-linux-gnu',
                textwrap.dedent(
                    '''
                    DEB_HOST_ARCH=amd64
                    DEB_HOST_ARCH_ABI=base
                    DEB_HOST_ARCH_BITS=64
                    DEB_HOST_ARCH_CPU=amd64
                    DEB_HOST_ARCH_ENDIAN=little
                    DEB_HOST_ARCH_LIBC=gnu
                    DEB_HOST_ARCH_OS=linux
                    DEB_HOST_GNU_CPU=x86_64
                    DEB_HOST_GNU_SYSTEM=linux-gnu
                    DEB_HOST_GNU_TYPE=x86_64-linux-gnu
                    DEB_HOST_MULTIARCH=x86_64-linux-gnu
                    '''
                ),
                '',
                {'PATH': '/usr/bin'},
                MachineInfo(
                    compilers=expected_compilers('x86_64-linux-gnu'),
                    binaries=expected_binaries('x86_64-linux-gnu'),
                    properties={},
                    compile_args={},
                    link_args={},
                    cmake={
                        'CMAKE_C_COMPILER': ['/usr/bin/x86_64-linux-gnu-gcc'],
                        'CMAKE_CXX_COMPILER': ['/usr/bin/x86_64-linux-gnu-g++'],
                        'CMAKE_SYSTEM_NAME': 'Linux',
                        'CMAKE_SYSTEM_PROCESSOR': 'x86_64',
                    },
                    system='linux',
                    subsystem='linux',
                    kernel='linux',
                    cpu='x86_64',
                    cpu_family='x86_64',
                    endian='little',
                ),
            ),
            (
                'arm-linux-gnueabihf with non-default gcc and environment',
                textwrap.dedent(
                    '''
                    DEB_HOST_ARCH=armhf
                    DEB_HOST_ARCH_ABI=eabihf
                    DEB_HOST_ARCH_BITS=32
                    DEB_HOST_ARCH_CPU=arm
                    DEB_HOST_ARCH_ENDIAN=little
                    DEB_HOST_ARCH_LIBC=gnu
                    DEB_HOST_ARCH_OS=linux
                    DEB_HOST_GNU_CPU=arm
                    DEB_HOST_GNU_SYSTEM=linux-gnueabihf
                    DEB_HOST_GNU_TYPE=arm-linux-gnueabihf
                    DEB_HOST_MULTIARCH=arm-linux-gnueabihf
                    '''
                ),
                '-12',
                {
                    'PATH': '/usr/bin',
                    'CPPFLAGS': '-DNDEBUG',
                    'CFLAGS': '-std=c99',
                    'CXXFLAGS': '-std=c++11',
                    'OBJCFLAGS': '-fobjc-exceptions',
                    'OBJCXXFLAGS': '-fobjc-nilcheck',
                    'LDFLAGS': '-Wl,-O1',
                },
                MachineInfo(
                    compilers=expected_compilers('arm-linux-gnueabihf', '-12'),
                    binaries=expected_binaries('arm-linux-gnueabihf'),
                    properties={},
                    compile_args={
                        'c': ['-DNDEBUG', '-std=c99'],
                        'cpp': ['-DNDEBUG', '-std=c++11'],
                        'objc': ['-DNDEBUG', '-fobjc-exceptions'],
                        'objcpp': ['-DNDEBUG', '-fobjc-nilcheck'],
                    },
                    link_args={
                        'c': ['-std=c99', '-Wl,-O1'],
                        'cpp': ['-std=c++11', '-Wl,-O1'],
                        'objc': ['-fobjc-exceptions', '-Wl,-O1'],
                        'objcpp': ['-fobjc-nilcheck', '-Wl,-O1'],
                    },
                    cmake={
                        'CMAKE_C_COMPILER': ['/usr/bin/arm-linux-gnueabihf-gcc-12'],
                        'CMAKE_CXX_COMPILER': ['/usr/bin/arm-linux-gnueabihf-g++-12'],
                        'CMAKE_SYSTEM_NAME': 'Linux',
                        'CMAKE_SYSTEM_PROCESSOR': 'armv7l',
                    },
                    system='linux',
                    subsystem='linux',
                    kernel='linux',
                    # In a native build this would often be armv8l
                    # (the version of the running CPU) but the architecture
                    # baseline in Debian is officially ARMv7
                    cpu='arm7hlf',
                    cpu_family='arm',
                    endian='little',
                ),
            ),
            (
                'special cases for i386 (i686, x86) and Hurd',
                textwrap.dedent(
                    '''
                    DEB_HOST_ARCH=hurd-i386
                    DEB_HOST_ARCH_ABI=base
                    DEB_HOST_ARCH_BITS=32
                    DEB_HOST_ARCH_CPU=i386
                    DEB_HOST_ARCH_ENDIAN=little
                    DEB_HOST_ARCH_LIBC=gnu
                    DEB_HOST_ARCH_OS=hurd
                    DEB_HOST_GNU_CPU=i686
                    DEB_HOST_GNU_SYSTEM=gnu
                    DEB_HOST_GNU_TYPE=i686-gnu
                    DEB_HOST_MULTIARCH=i386-gnu
                    '''
                ),
                '',
                {'PATH': '/usr/bin'},
                MachineInfo(
                    compilers=expected_compilers('i686-gnu'),
                    binaries=expected_binaries('i686-gnu'),
                    properties={},
                    compile_args={},
                    link_args={},
                    cmake={
                        'CMAKE_C_COMPILER': ['/usr/bin/i686-gnu-gcc'],
                        'CMAKE_CXX_COMPILER': ['/usr/bin/i686-gnu-g++'],
                        'CMAKE_SYSTEM_NAME': 'GNU',
                        'CMAKE_SYSTEM_PROCESSOR': 'i686',
                    },
                    system='gnu',
                    subsystem='gnu',
                    kernel='gnu',
                    cpu='i686',
                    cpu_family='x86',
                    endian='little',
                ),
            ),
            (
                'special cases for amd64 (x86_64) and kFreeBSD',
                textwrap.dedent(
                    '''
                    DEB_HOST_ARCH=kfreebsd-amd64
                    DEB_HOST_ARCH_ABI=base
                    DEB_HOST_ARCH_BITS=64
                    DEB_HOST_ARCH_CPU=x86_amd64
                    DEB_HOST_ARCH_ENDIAN=little
                    DEB_HOST_ARCH_LIBC=gnu
                    DEB_HOST_ARCH_OS=kfreebsd
                    DEB_HOST_GNU_CPU=x86_64
                    DEB_HOST_GNU_SYSTEM=kfreebsd-gnu
                    DEB_HOST_GNU_TYPE=x86_64-kfreebsd-gnu
                    DEB_HOST_MULTIARCH=x86_64-kfreebsd-gnu
                    '''
                ),
                '',
                {'PATH': '/usr/bin'},
                MachineInfo(
                    compilers=expected_compilers('x86_64-kfreebsd-gnu'),
                    binaries=expected_binaries('x86_64-kfreebsd-gnu'),
                    properties={},
                    compile_args={},
                    link_args={},
                    cmake={
                        'CMAKE_C_COMPILER': ['/usr/bin/x86_64-kfreebsd-gnu-gcc'],
                        'CMAKE_CXX_COMPILER': ['/usr/bin/x86_64-kfreebsd-gnu-g++'],
                        'CMAKE_SYSTEM_NAME': 'kFreeBSD',
                        'CMAKE_SYSTEM_PROCESSOR': 'x86_64',
                    },
                    system='kfreebsd',
                    subsystem='kfreebsd',
                    kernel='freebsd',
                    cpu='x86_64',
                    cpu_family='x86_64',
                    endian='little',
                ),
            ),
            (
                'special case for mips64el',
                textwrap.dedent(
                    '''
                    DEB_HOST_ARCH=mips64el
                    DEB_HOST_ARCH_ABI=abi64
                    DEB_HOST_ARCH_BITS=64
                    DEB_HOST_ARCH_CPU=mips64el
                    DEB_HOST_ARCH_ENDIAN=little
                    DEB_HOST_ARCH_LIBC=gnu
                    DEB_HOST_ARCH_OS=linux
                    DEB_HOST_GNU_CPU=mips64el
                    DEB_HOST_GNU_SYSTEM=linux-gnuabi64
                    DEB_HOST_GNU_TYPE=mips64el-linux-gnuabi64
                    DEB_HOST_MULTIARCH=mips64el-linux-gnuabi64
                    '''
                ),
                '',
                {'PATH': '/usr/bin'},
                MachineInfo(
                    compilers=expected_compilers('mips64el-linux-gnuabi64'),
                    binaries=expected_binaries('mips64el-linux-gnuabi64'),
                    properties={},
                    compile_args={},
                    link_args={},
                    cmake={
                        'CMAKE_C_COMPILER': ['/usr/bin/mips64el-linux-gnuabi64-gcc'],
                        'CMAKE_CXX_COMPILER': ['/usr/bin/mips64el-linux-gnuabi64-g++'],
                        'CMAKE_SYSTEM_NAME': 'Linux',
                        'CMAKE_SYSTEM_PROCESSOR': 'mips64',
                    },
                    system='linux',
                    subsystem='linux',
                    kernel='linux',
                    cpu='mips64',
                    cpu_family='mips64',
                    endian='little',
                ),
            ),
            (
                'special case for ppc64el',
                textwrap.dedent(
                    '''
                    DEB_HOST_ARCH=ppc64el
                    DEB_HOST_ARCH_ABI=base
                    DEB_HOST_ARCH_BITS=64
                    DEB_HOST_ARCH_CPU=ppc64el
                    DEB_HOST_ARCH_ENDIAN=little
                    DEB_HOST_ARCH_LIBC=gnu
                    DEB_HOST_ARCH_OS=linux
                    DEB_HOST_GNU_CPU=powerpc64le
                    DEB_HOST_GNU_SYSTEM=linux-gnu
                    DEB_HOST_GNU_TYPE=powerpc64le-linux-gnu
                    DEB_HOST_MULTIARCH=powerpc64le-linux-gnu
                    '''
                ),
                '',
                {'PATH': '/usr/bin'},
                MachineInfo(
                    compilers=expected_compilers('powerpc64le-linux-gnu'),
                    binaries=expected_binaries('powerpc64le-linux-gnu'),
                    properties={},
                    compile_args={},
                    link_args={},
                    cmake={
                        'CMAKE_C_COMPILER': ['/usr/bin/powerpc64le-linux-gnu-gcc'],
                        'CMAKE_CXX_COMPILER': ['/usr/bin/powerpc64le-linux-gnu-g++'],
                        'CMAKE_SYSTEM_NAME': 'Linux',
                        'CMAKE_SYSTEM_PROCESSOR': 'ppc64le',
                    },
                    system='linux',
                    subsystem='linux',
                    kernel='linux',
                    # TODO: Currently ppc64, but native builds have ppc64le
                    # https://github.com/mesonbuild/meson/issues/13741
                    cpu='TODO',
                    cpu_family='ppc64',
                    endian='little',
                ),
            ),
        ]:
            with self.subTest(title), \
                    unittest.mock.patch.dict('os.environ', env, clear=True), \
                    unittest.mock.patch('mesonbuild.scripts.env2mfile.locate_path') as mock_locate_path:
                mock_locate_path.side_effect = locate_path
                options = argparse.Namespace()
                options.gccsuffix = gccsuffix
                actual = to_machine_info(dpkg_arch, options)

                if expected.system == 'TODO':
                    print(f'TODO: {title}: system() -> {actual.system}')
                else:
                    self.assertEqual(actual.system, expected.system)

                if expected.subsystem == 'TODO':
                    print(f'TODO: {title}: subsystem() -> {actual.subsystem}')
                else:
                    self.assertEqual(actual.subsystem, expected.subsystem)

                if expected.kernel == 'TODO':
                    print(f'TODO: {title}: kernel() -> {actual.kernel}')
                else:
                    self.assertEqual(actual.kernel, expected.kernel)

                if expected.cpu == 'TODO':
                    print(f'TODO: {title}: cpu() -> {actual.cpu}')
                else:
                    self.assertEqual(actual.cpu, expected.cpu)

                self.assertEqual(actual.cpu_family, expected.cpu_family)
                self.assertEqual(actual.endian, expected.endian)

                self.assertEqual(actual.compilers, expected.compilers)
                self.assertEqual(actual.binaries, expected.binaries)
                self.assertEqual(actual.properties, expected.properties)
                self.assertEqual(actual.compile_args, expected.compile_args)
                self.assertEqual(actual.link_args, expected.link_args)
                self.assertEqual(actual.cmake, expected.cmake)
