 SCons script itself doesn't need to, so this option gets
        "eaten" by the bootstrap.py script.
"""

import os
import os.path
import sys
import glob
import subprocess
import filecmp
import shutil

def parseManifestLines(basedir, manifest):
    """
    Scans a MANIFEST file, and returns the list of source files.

    Has basic support for recursive globs '**',
    filename wildcards of the form '*.xml' and
    comment lines, starting with a '#'.

    :param basedir: base path to find files in. Note this does not
                    run in an SCons context so path must not need
                    further processing (e.g. no '#' signs)
    :param manifest: path to manifest file
    :returns: list of files
    """
    sources = []
    basewd = os.path.abspath(basedir)
    with open(manifest) as m:
        lines = m.readlines()
    for l in lines:
        if l.startswith('#'):
            # Skip comments
            continue
        l = l.rstrip('\n')
        if l.endswith('**'):
            # Glob all files recursively
            globwd = os.path.dirname(os.path.join(basewd, l))
            for path, dirs, files in os.walk(globwd):
                for f in files:
                    fpath = os.path.join(globwd, path, f)
                    sources.append(os.path.relpath(fpath, basewd))
        elif '*' in l:
            # Glob file pattern
            files = glob.glob(os.path.join(basewd, l))
            for f in files:
                sources.append(os.path.relpath(f, basewd))
        else:
            sources.append(l)

    return sources

def main():
    script_dir = os.path.abspath(os.path.dirname(__file__))

    bootstrap_dir = os.path.join(script_dir, 'bootstrap')

    pass_through_args = []
    update_only = None

    requires_an_argument = 'bootstrap.py:  %s requires an argument\n'

    search = [script_dir]

    def find(filename, search=search):
        for dir_name in search:
            filepath = os.path.join(dir_name, filename)
            if os.path.exists(filepath):
                return os.path.normpath(filepath)
        sys.stderr.write("could not find `%s' in search path:\n" % filename)
        sys.stderr.write("\t" + "\n\t".join(search) + "\n")
        sys.exit(2)

    def must_copy(dst, src):
        if not os.path.exists(dst):
            return 1
        return not filecmp.cmp(dst,src)

    # Note:  We don't use the getopt module to process the command-line
    # arguments because we'd have to teach it about all of the SCons options.

    command_line_args = sys.argv[1:]

    while command_line_args:
        arg = command_line_args.pop(0)

        if arg == '--bootstrap_dir':
            try:
                bootstrap_dir = command_line_args.pop(0)
            except IndexError:
                sys.stderr.write(requires_an_argument % arg)
                sys.exit(1)
        elif arg[:16] == '--bootstrap_dir=':
            bootstrap_dir = arg[16:]
        elif arg == '--bootstrap_force':
            def must_copy(dst, src):
                return 1
        elif arg == '--bootstrap_src':
            try:
                search.insert(0, command_line_args.pop(0))
            except IndexError:
                sys.stderr.write(requires_an_argument % arg)
                sys.exit(1)
        elif arg[:16] == '--bootstrap_src=':
            search.insert(0, arg[16:])
        elif arg == '--bootstrap_update':
            update_only = 1
        elif arg in ('-C', '--directory'):
            try:
                dir = command_line_args.pop(0)
            except IndexError:
                sys.stderr.write(requires_an_argument % arg)
                sys.exit(1)
            else:
                os.chdir(dir)
        elif arg[:2] == '-C':
            os.chdir(arg[2:])
        elif arg[:12] == '--directory=':
            os.chdir(arg[12:])
        else:
            pass_through_args.append(arg)

    scons_py = os.path.join('scripts', 'scons.py')
    src_engine = os.path.join('src', 'engine')
    MANIFEST_in = find(os.path.join(src_engine, 'MANIFEST.in'))
    manifest_files = [os.path.join(src_engine, x)
                      for x in parseManifestLines(os.path.join(script_dir, src_engine),
                                                  MANIFEST_in)]

    files = [scons_py] + manifest_files

    for filename in files:
        src = find(filename)
        dst = os.path.join(bootstrap_dir, filename)
        if must_copy(dst, src):
            dir = os.path.split(dst)[0]
            if not os.path.isdir(dir):
                os.makedirs(dir)
            try:
                os.unlink(dst)
            except Exception as e:
                pass

            shutil.copyfile(src, dst)

    if update_only:
        sys.exit(0)

    args = [sys.executable, os.path.join(bootstrap_dir, scons_py)] + pass_through_args

    sys.stdout.write(" ".join(args) + '\n')
    sys.stdout.flush()

    os.environ['SCONS_LIB_DIR'] = os.path.join(bootstrap_dir, src_engine)

    sys.exit(subprocess.Popen(args, env=os.environ).wait())


if __name__ == "__main__":
    print("Please use")
    print("python scripts/scons.py")
    print("Instead of python bootstrap.py. Bootstrap.py is obsolete")
    sys.exit(-1)
    main()

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# runtest.py

#!/usr/bin/env python
#
# Copyright The SCons Foundation
#
"""runtest - wrapper script for running SCons tests

The SCons test suite consists of:

 * unit tests        - *Tests.py files from the SCons/ dir
 * end-to-end tests  - *.py files in the test/ directory that
                       require the custom SCons framework from testing/

This script adds SCons/ and testing/ directories to PYTHONPATH,
performs test discovery and processes tests according to options.
"""

# TODO: normalize requested and testlist/exclude paths for easier comparison.
# e.g.: "runtest foo/bar" on windows will produce paths like foo/bar\test.py
# this is hard to match with excludelists, and makes those both os.sep-specific
# and command-line-typing specific.

import argparse
import glob
import os
import stat
import subprocess
import sys
import tempfile
import threading
import time
from abc import ABC, abstractmethod
from pathlib import Path
from queue import Queue

cwd = os.getcwd()

debug = None
scons = None
catch_output = False
suppress_output = False

script = os.path.basename(sys.argv[0])
usagestr = """\
%(script)s [OPTIONS] [TEST ...]
""" % locals()

epilogstr = """\
Environment Variables:
  PRESERVE, PRESERVE_{PASS,FAIL,NO_RESULT}: preserve test subdirs
  TESTCMD_VERBOSE: turn on verbosity in TestCommand\
"""

parser = argparse.ArgumentParser(
    usage=usagestr, epilog=epilogstr, allow_abbrev=False,
    formatter_class=argparse.RawDescriptionHelpFormatter
)

# test selection options:
testsel = parser.add_argument_group(description='Test selection options:')
testsel.add_argument(metavar='TEST', nargs='*', dest='testlist',
                     help="Select TEST(s) (tests and/or directories) to run")
testlisting = testsel.add_mutually_exclusive_group()
testlisting.add_argument('-f', '--file', metavar='FILE', dest='testlistfile',
                     help="Select only tests in FILE")
testlisting.add_argument('-a', '--all', action='store_true',
                     help="Select all tests")
testlisting.add_argument('--retry', action='store_true',
                     help="Rerun the last failed tests in 'failed_tests.log'")
testsel.add_argument('--exclude-list', metavar="FILE", dest='excludelistfile',
                     help="""Exclude tests in FILE from current selection""")
testtype = testsel.add_mutually_exclusive_group()
testtype.add_argument('--e2e-only', action='store_true',
                      help="Exclude unit tests from selection")
testtype.add_argument('--unit-only', action='store_true',
                      help="Exclude end-to-end tests from selection")

# miscellaneous options
parser.add_argument('-b', '--baseline', metavar='BASE',
                    help="Run test scripts against baseline BASE.")
parser.add_argument('-d', '--debug', action='store_true',
                    help="Run test scripts under the Python debugger.")
parser.add_argument('-D', '--devmode', action='store_true',
                    help="Run tests in Python's development mode (Py3.7+ only).")
parser.add_argument('-e', '--external', action='store_true',
                    help="Run the script in external mode (for external Tools)")
parser.add_argument('-j', '--jobs', metavar='JOBS', default=1, type=int,
                    help="Run tests in JOBS parallel jobs (0 for cpu_count).")
parser.add_argument('-l', '--list', action='store_true', dest='list_only',
                    help="List available tests and exit.")
parser.add_argument('-n', '--no-exec', action='store_false',
                    dest='execute_tests',
                    help="No execute, just print command lines.")
parser.add_argument('--nopipefiles', action='store_false',
                    dest='allow_pipe_files',
                    help="""Do not use the "file pipe" workaround for subprocess
                           for starting tests. See source code for warnings.""")
parser.add_argument('-P', '--python', metavar='PYTHON',
                    help="Use the specified Python interpreter.")
parser.add_argument('--quit-on-failure', action='store_true',
                    help="Quit on any test failure.")
parser.add_argument('--runner', metavar='CLASS',
                    help="Test runner class for unit tests.")
parser.add_argument('-X', dest='scons_exec', action='store_true',
                    help="Test script is executable, don't feed to Python.")
parser.add_argument('-x', '--exec', metavar="SCRIPT",
                    help="Test using SCRIPT as path to SCons.")
parser.add_argument('--faillog', dest='error_log', metavar="FILE",
                    default='failed_tests.log',
                    help="Log failed tests to FILE (enabled by default, "
                         "default file 'failed_tests.log')")
parser.add_argument('--no-faillog', dest='error_log',
                    action='store_const', const=None,
                    default='failed_tests.log',
                    help="Do not log failed tests to a file")

parser.add_argument('--no-ignore-skips', dest='dont_ignore_skips',
                    action='store_true',
                    default=False,
                    help="If any tests are skipped, exit status 2")

outctl = parser.add_argument_group(description='Output control options:')
outctl.add_argument('-k', '--no-progress', action='store_false',
                    dest='print_progress',
                    help="Suppress count and progress percentage messages.")
outctl.add_argument('--passed', action='store_true',
                    dest='print_passed_summary',
                    help="Summarize which tests passed.")
outctl.add_argument('-q', '--quiet', action='store_false',
                    dest='printcommand',
                    help="Don't print the test being executed.")
outctl.add_argument('-s', '--short-progress', action='store_true',
                    help="""Short progress, prints only the command line
                             and a progress percentage.""")
outctl.add_argument('-t', '--time', action='store_true', dest='print_times',
                    help="Print test execution time.")
outctl.add_argument('--verbose', metavar='LEVEL', type=int, choices=range(1, 4),
                    help="""Set verbose level
                             (1=print executed commands,
                             2=print commands and non-zero output,
                             3=print commands and all output).""")
# maybe add?
# outctl.add_argument('--version', action='version', version='%s 1.0' % script)

logctl = parser.add_argument_group(description='Log control options:')
logctl.add_argument('-o', '--output', metavar='LOG', help="Save console output to LOG.")
logctl.add_argument('--xml', metavar='XML', help="Save results to XML in SCons XML format.")

# process args and handle a few specific cases:
args = parser.parse_args()

# we can't do this check with an argparse exclusive group, since those
# only work with optional args, and the cmdline tests (args.testlist)
# are not optional args,
if args.testlist and (args.testlistfile or args.all or args.retry):
    sys.stderr.write(
        parser.format_usage()
        + "error: command line tests cannot be combined with -f/--file, -a/--all or --retry\n"
    )
    sys.exit(1)

if args.retry:
    args.testlistfile = 'failed_tests.log'

if args.testlistfile:
    # args.testlistfile changes from a string to a pathlib Path object
    try:
        p = Path(args.testlistfile)
        # TODO simplify when Py3.5 dropped
        if sys.version_info.major == 3 and sys.version_info.minor < 6:
            args.testlistfile = p.resolve()
        else:
            args.testlistfile = p.resolve(strict=True)
    except FileNotFoundError:
        sys.stderr.write(
            parser.format_usage()
            + 'error: -f/--file testlist file "%s" not found\n' % p
        )
        sys.exit(1)

if args.excludelistfile:
    # args.excludelistfile changes from a string to a pathlib Path object
    try:
        p = Path(args.excludelistfile)
        # TODO simplify when Py3.5 dropped
        if sys.version_info.major == 3 and sys.version_info.minor < 6:
            args.excludelistfile = p.resolve()
        else:
            args.excludelistfile = p.resolve(strict=True)
    except FileNotFoundError:
        sys.stderr.write(
            parser.format_usage()
            + 'error: --exclude-list file "%s" not found\n' % p
        )
        sys.exit(1)

if args.jobs == 0:
    try:
        # on Linux, check available rather then physical CPUs
        args.jobs = len(os.sched_getaffinity(0))
    except AttributeError:
        # Windows
        args.jobs = os.cpu_count()

# sanity check
if args.jobs == 0:
    sys.stderr.write(
        parser.format_usage()
        + "Unable to detect CPU count, give -j a non-zero value\n"
    )
    sys.exit(1)

if args.jobs > 1 or args.output:
    # 1. don't let tests write stdout/stderr directly if multi-job,
    # else outputs will interleave and be hard to read.
    # 2. If we're going to write a logfile, we also need to catch the output.
    catch_output = True

if not args.printcommand:
    suppress_output = catch_output = True

if args.verbose:
    os.environ['TESTCMD_VERBOSE'] = str(args.verbose)

if args.short_progress:
    args.print_progress = True
    suppress_output = catch_output = True

if args.debug:
    # TODO: add a way to pass a specific debugger
    debug = "pdb"

if args.exec:
    scons = args.exec

# --- setup stdout/stderr ---
class Unbuffered:
    def __init__(self, file):
        self.file = file

    def write(self, arg):
        self.file.write(arg)
        self.file.flush()

    def __getattr__(self, attr):
        return getattr(self.file, attr)

sys.stdout = Unbuffered(sys.stdout)
sys.stderr = Unbuffered(sys.stderr)

# possible alternative: switch to using print, and:
# print = functools.partial(print, flush)

if args.output:
    class Tee:
        def __init__(self, openfile, stream):
            self.file = openfile
            self.stream = stream

        def write(self, data):
            self.file.write(data)
            self.stream.write(data)

        def flush(self, data):
            self.file.flush(data)
            self.stream.flush(data)

    logfile = open(args.output, 'w')
    # this is not ideal: we monkeypatch stdout/stderr a second time
    # (already did for Unbuffered), so here we can't easily detect what
    # state we're in on closedown. Just hope it's okay...
    sys.stdout = Tee(logfile, sys.stdout)
    sys.stderr = Tee(logfile, sys.stderr)

# --- define helpers ----
if sys.platform == 'win32':
    # thanks to Eryk Sun for this recipe
    import ctypes

    shlwapi = ctypes.OleDLL('shlwapi')
    shlwapi.AssocQueryStringW.argtypes = (
        ctypes.c_ulong,  # flags
        ctypes.c_ulong,  # str
        ctypes.c_wchar_p,  # pszAssoc
        ctypes.c_wchar_p,  # pszExtra
        ctypes.c_wchar_p,  # pszOut
        ctypes.POINTER(ctypes.c_ulong),  # pcchOut
    )

    ASSOCF_NOTRUNCATE = 0x00000020
    ASSOCF_INIT_IGNOREUNKNOWN = 0x00000400
    ASSOCSTR_COMMAND = 1
    ASSOCSTR_EXECUTABLE = 2
    E_POINTER = ctypes.c_long(0x80004003).value

    def get_template_command(filetype, verb=None):
        flags = ASSOCF_INIT_IGNOREUNKNOWN | ASSOCF_NOTRUNCATE
        assoc_str = ASSOCSTR_COMMAND
        cch = ctypes.c_ulong(260)
        while True:
            buf = (ctypes.c_wchar * cch.value)()
            try:
                shlwapi.AssocQueryStringW(
                    flags, assoc_str, filetype, verb, buf, ctypes.byref(cch)
                )
            except OSError as e:
                if e.winerror != E_POINTER:
                    raise
                continue
            break
        return buf.value


if not catch_output:
    # Without any output suppressed, we let the subprocess
    # write its stuff freely to stdout/stderr.

    def spawn_it(command_args, env):
        cp = subprocess.run(command_args, shell=False, env=env)
        return cp.stdout, cp.stderr, cp.returncode

else:
    # Else, we catch the output of both pipes...
    if args.allow_pipe_files:
        # The subprocess.Popen() suffers from a well-known
        # problem. Data for stdout/stderr is read into a
        # memory buffer of fixed size, 65K which is not very much.
        # When it fills up, it simply stops letting the child process
        # write to it. The child will then sit and patiently wait to
        # be able to write the rest of its output. Hang!
        # In order to work around this, we follow a suggestion
        # by Anders Pearson in
        #   https://thraxil.org/users/anders/posts/2008/03/13/Subprocess-Hanging-PIPE-is-your-enemy/
        # and pass temp file objects to Popen() instead of the ubiquitous
        # subprocess.PIPE.

        def spawn_it(command_args, env):
            # Create temporary files
            tmp_stdout = tempfile.TemporaryFile(mode='w+t')
            tmp_stderr = tempfile.TemporaryFile(mode='w+t')
            # Start subprocess...
            cp = subprocess.run(
                command_args,
                stdout=tmp_stdout,
                stderr=tmp_stderr,
                shell=False,
                env=env,
            )

            try:
                # Rewind to start of files
                tmp_stdout.seek(0)
                tmp_stderr.seek(0)
                # Read output
                spawned_stdout = tmp_stdout.read()
                spawned_stderr = tmp_stderr.read()
            finally:
                # Remove temp files by closing them
                tmp_stdout.close()
                tmp_stderr.close()

            # Return values
            return spawned_stderr, spawned_stdout, cp.returncode

    else:
        # We get here only if the user gave the '--nopipefiles'
        # option, meaning the "temp file" approach for
        # subprocess.communicate() above shouldn't be used.
        # He hopefully knows what he's doing, but again we have a
        # potential deadlock situation in the following code:
        #   If the subprocess writes a lot of data to its stderr,
        #   the pipe will fill up (nobody's reading it yet) and the
        #   subprocess will wait for someone to read it.
        #   But the parent process is trying to read from stdin
        #   (but the subprocess isn't writing anything there).
        #   Hence a deadlock.
        # Be dragons here! Better don't use this!

        def spawn_it(command_args, env):
            cp = subprocess.run(
                command_args,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                shell=False,
                env=env,
            )
            return cp.stdout, cp.stderr, cp.returncode


class RuntestBase(ABC):
    """ Base class for tests """
    def __init__(self, path, num, spe=None):
        self.path = path
        self.num = num
        self.stdout = self.stderr = self.status = None
        self.abspath = os.path.abspath(path)
        self.command_args = []
        self.command_str = ""
        self.test_time = self.total_time = 0
        if spe:
            for d in spe:
                f = os.path.join(d, path)
                if os.path.isfile(f):
                    self.abspath = f
                    break

    @abstractmethod
    def execute(self):
        pass


class SystemExecutor(RuntestBase):
    """ Test class for tests executed with spawn_it() """
    def execute(self, env):
        self.stderr, self.stdout, s = spawn_it(self.command_args, env)
        self.status = s
        if s < 0 or s > 2:
            sys.stdout.write("Unexpected exit status %d\n" % s)


class PopenExecutor(RuntestBase):
    """ Test class for tests executed with Popen

    A bit of a misnomer as the Popen call is now wrapped
    by calling subprocess.run (behind the covers uses Popen.
    Very similar to SystemExecutor, but doesn't allow for not catching
    the output).
    """
    # For an explanation of the following 'if ... else'
    # and the 'allow_pipe_files' option, please check out the
    # definition of spawn_it() above.
    if args.allow_pipe_files:

        def execute(self, env):
            # Create temporary files
            tmp_stdout = tempfile.TemporaryFile(mode='w+t')
            tmp_stderr = tempfile.TemporaryFile(mode='w+t')
            # Start subprocess...
            cp = subprocess.run(
                self.command_args,
                stdout=tmp_stdout,
                stderr=tmp_stderr,
                shell=False,
                env=env,
            )
            self.status = cp.returncode

            try:
                # Rewind to start of files
                tmp_stdout.seek(0)
                tmp_stderr.seek(0)
                # Read output
                self.stdout = tmp_stdout.read()
                self.stderr = tmp_stderr.read()
            finally:
                # Remove temp files by closing them
                tmp_stdout.close()
                tmp_stderr.close()
    else:

        def execute(self, env):
            cp = subprocess.run(
                self.command_args,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                shell=False,
                env=env,
            )
            self.status, self.stdout, self.stderr = cp.returncode, cp.stdout, cp.stderr

class XML(PopenExecutor):
    """ Test class for tests that will output in scons xml """
    @staticmethod
    def header(f):
        f.write('  <results>\n')

    def write(self, f):
        f.write('    <test>\n')
        f.write('      <file_name>%s</file_name>\n' % self.path)
        f.write('      <command_line>%s</command_line>\n' % self.command_str)
        f.write('      <exit_status>%s</exit_status>\n' % self.status)
        f.write('      <stdout>%s</stdout>\n' % self.stdout)
        f.write('      <stderr>%s</stderr>\n' % self.stderr)
        f.write('      <time>%.1f</time>\n' % self.test_time)
        f.write('    </test>\n')

    def footer(self, f):
        f.write('  <time>%.1f</time>\n' % self.total_time)
        f.write('  </results>\n')

if args.xml:
    Test = XML
else:
    Test = SystemExecutor

# --- start processing ---

if not args.baseline or args.baseline == '.':
    baseline = cwd
elif args.baseline == '-':
    print("This logic used to checkout from svn. It's been removed. If you used this, please let us know on devel mailing list, IRC, or discord server")
    sys.exit(-1)
else:
    baseline = args.baseline
scons_runtest_dir = baseline

if not args.external:
    scons_script_dir = os.path.join(baseline, 'scripts')
    scons_tools_dir = os.path.join(baseline, 'bin')
    scons_lib_dir = baseline
else:
    scons_script_dir = ''
    scons_tools_dir = ''
    scons_lib_dir = ''

testenv = {
    'SCONS_RUNTEST_DIR': scons_runtest_dir,
    'SCONS_TOOLS_DIR': scons_tools_dir,
    'SCONS_SCRIPT_DIR': scons_script_dir,
    'SCONS_CWD': cwd,
}

if scons:
    # Let the version of SCons that the -x option pointed to find
    # its own modules.
    testenv['SCONS'] = scons
elif scons_lib_dir:
    # Because SCons is really aggressive about finding its modules,
    # it sometimes finds SCons modules elsewhere on the system.
    # This forces SCons to use the modules that are being tested.
    testenv['SCONS_LIB_DIR'] = scons_lib_dir

if args.scons_exec:
    testenv['SCONS_EXEC'] = '1'

if args.external:
    testenv['SCONS_EXTERNAL_TEST'] = '1'

# Insert scons path and path for testing framework to PYTHONPATH
scriptpath = os.path.dirname(os.path.realpath(__file__))
frameworkpath = os.path.join(scriptpath, 'testing', 'framework')
testenv['PYTHONPATH'] = os.pathsep.join((scons_lib_dir, frameworkpath))
pythonpath = os.environ.get('PYTHONPATH')
if pythonpath:
    testenv['PYTHONPATH'] = testenv['PYTHONPATH'] + os.pathsep + pythonpath

if sys.platform == 'win32':
    # Windows doesn't support "shebang" lines directly (the Python launcher
    # and Windows Store version do, but you have to get them launched first)
    # so to directly launch a script we depend on an assoc for .py to work.
    # Some systems may have none, and in some cases IDE programs take over 
    # the assoc.  Detect this so the small number of tests affected can skip.
    try:
        python_assoc = get_template_command('.py')
    except OSError:
        python_assoc = None
    if not python_assoc or "py" not in python_assoc:
        testenv['SCONS_NO_DIRECT_SCRIPT'] = '1'

os.environ.update(testenv)

# Clear _JAVA_OPTIONS which java tools output to stderr when run breaking tests
if '_JAVA_OPTIONS' in os.environ:
    del os.environ['_JAVA_OPTIONS']


# ---[ test discovery ]------------------------------------
# This section figures which tests to run.
#
# The initial testlist is made by reading from the testlistfile,
# if supplied, or by looking at the test arguments, if supplied,
# or by looking for all test files if the "all" argument is supplied.
# One of the three is required.
#
# Each test path, whichever of the three sources it comes from,
# specifies either a test file or a directory to search for
# SCons tests. SCons code layout assumes that any file under the 'SCons'
# subdirectory that ends with 'Tests.py' is a unit test, and any Python
# script (*.py) under the 'test' subdirectory is an end-to-end test.
# We need to track these because they are invoked differently.
# find_unit_tests and find_e2e_tests are used for this searching.
#
# Note that there are some tests under 'SCons' that *begin* with
# 'test_', but they're packaging and installation tests, not
# functional tests, so we don't execute them by default.  (They can
# still be executed by hand, though).
#
# Test exclusions, if specified, are then applied.


def scanlist(testlist):
    """ Process a testlist file """
    tests = [t.strip() for t in testlist if not t.startswith('#')]
    return [t for t in tests if t]


def find_unit_tests(directory):
    """ Look for unit tests """
    result = []
    for dirpath, dirnames, filenames in os.walk(directory):
        # Skip folders containing a sconstest.skip file
        if 'sconstest.skip' in filenames:
            continue
        for fname in filenames:
            if fname.endswith("Tests.py"):
                result.append(os.path.join(dirpath, fname))
    return sorted(result)


def find_e2e_tests(directory):
    """ Look for end-to-end tests """
    result = []
    for dirpath, dirnames, filenames in os.walk(directory):
        # Skip folders containing a sconstest.skip file
        if 'sconstest.skip' in filenames:
            continue

        # Slurp in any tests in exclude lists
        excludes = []
        if ".exclude_tests" in filenames:
            p = Path(dirpath).joinpath(".exclude_tests")
            # TODO simplify when Py3.5 dropped
            if sys.version_info.major == 3 and sys.version_info.minor < 6:
                excludefile = p.resolve()
            else:
                excludefile = p.resolve(strict=True)
            with excludefile.open() as f:
                excludes = scanlist(f)

        for fname in filenames:
            if fname.endswith(".py") and fname not in excludes:
                result.append(os.path.join(dirpath, fname))

    return sorted(result)


# initial selection:
unittests = []
endtests = []
if args.testlistfile:
    with args.testlistfile.open() as f:
        tests = scanlist(f)
else:
    testpaths = []
    if args.all:
        testpaths = ['SCons', 'test']
    elif args.testlist:
        testpaths = args.testlist

    for tp in testpaths:
        # Clean up path so it can match startswith's below
        # remove leading ./ or .\
        if tp.startswith('.') and tp[1] in (os.sep, os.altsep):
            tp = tp[2:]

        for path in glob.glob(tp):
            if os.path.isdir(path):
                if path.startswith(('SCons', 'testing')):
                    unittests.extend(find_unit_tests(path))
                elif path.startswith('test'):
                    endtests.extend(find_e2e_tests(path))
            else:
                if path.endswith("Tests.py"):
                    unittests.append(path)
                elif path.endswith(".py"):
                    endtests.append(path)
    tests = sorted(unittests + endtests)


# Remove exclusions:
if args.e2e_only:
    tests = [t for t in tests if not t.endswith("Tests.py")]
if args.unit_only:
    tests = [t for t in tests if t.endswith("Tests.py")]
if args.excludelistfile:
    with args.excludelistfile.open() as f:
        excludetests = scanlist(f)
    tests = [t for t in tests if t not in excludetests]

if not tests:
    sys.stderr.write(parser.format_usage() + """
error: no tests were found.
       Tests can be specified on the command line, read from a file with
       the -f/--file option, or discovered with -a/--all to run all tests.
""")
    sys.exit(1)

# ---[ test processing ]-----------------------------------
tests = [Test(t, n + 1) for n, t in enumerate(tests)]

if args.list_only:
    for t in tests:
        sys.stdout.write(t.path + "\n")
    sys.exit(0)

if not args.python:
    if os.name == 'java':
        args.python = os.path.join(sys.prefix, 'jython')
    else:
        args.python = sys.executable
os.environ["python_executable"] = args.python

if args.print_times:

    def print_time(fmt, tm):
        sys.stdout.write(fmt % tm)

else:

    def print_time(fmt, tm):
        pass

time_func = time.perf_counter
total_start_time = time_func()
total_num_tests = len(tests)


def log_result(t, io_lock=None):
    """ log the result of a test.

    "log" in this case means writing to stdout. Since we might be
    called from from any of several different threads (multi-job run),
    we need to lock access to the log to avoid interleaving. The same
    would apply if output was a file.

    Args:
        t (Test): (completed) testcase instance
        io_lock (threading.lock): (optional) lock to use
    """

    # there is no lock in single-job run, which includes
    # running test/runtest tests from multi-job run, so check.
    if io_lock:
        io_lock.acquire()
    try:
        if suppress_output or catch_output:
            sys.stdout.write(t.headline)
        if not suppress_output:
            if t.stdout:
                print(t.stdout)
            if t.stderr:
                print(t.stderr)
        print_time("Test execution time: %.1f seconds\n", t.test_time)
    finally:
        if io_lock:
            io_lock.release()

    if args.quit_on_failure and t.status == 1:
        print("Exiting due to error")
        print(t.status)
        sys.exit(1)


def run_test(t, io_lock=None, run_async=True):
    """ Run a testcase.

    Builds the command line to give to execute().
    Also the best place to record some information that will be
    used in output, which in some conditions is printed here.

    Args:
        t (Test): testcase instance
        io_lock (threading.Lock): (optional) lock to use
        run_async (bool): whether to run asynchronously
    """

    t.headline = ""
    command_args = []
    if debug:
        command_args.extend(['-m', debug])
    if args.devmode and sys.version_info >= (3, 7, 0):
        command_args.append('-X dev')
    command_args.append(t.path)
    if args.runner and t.path in unittests:
        # For example --runner TestUnit.TAPTestRunner
        command_args.append('--runner ' + args.runner)
    t.command_args = [args.python] + command_args
    t.command_str = " ".join(t.command_args)
    if args.printcommand:
        if args.print_progress:
            t.headline += "%d/%d (%.2f%s) %s\n" % (
                t.num, total_num_tests,
                float(t.num) * 100.0 / float(total_num_tests),
                "%",
                t.command_str,
            )
        else:
            t.headline += t.command_str + "\n"
    if not suppress_output and not catch_output:
        # defer printing the headline until test is done
        sys.stdout.write(t.headline)
    head, _ = os.path.split(t.abspath)
    fixture_dirs = []
    if head:
        fixture_dirs.append(head)
    fixture_dirs.append(os.path.join(scriptpath, 'test', 'fixture'))

    # Set the list of fixture dirs directly in the environment. Just putting
    # it in os.environ and spawning the process is racy. Make it reliable by
    # overriding the environment passed to execute().
    env = dict(os.environ)
    env['FIXTURE_DIRS'] = os.pathsep.join(fixture_dirs)

    test_start_time = time_func()
    if args.execute_tests:
        t.execute(env)

    t.test_time = time_func() - test_start_time
    log_result(t, io_lock=io_lock)


class RunTest(threading.Thread):
    """ Test Runner class.

    One instance will be created for each job thread in multi-job mode
    """
    def __init__(self, queue=None, io_lock=None, group=None, target=None, name=None):
        super().__init__(group=group, target=target, name=name)
        self.queue = queue
        self.io_lock = io_lock

    def run(self):
        for t in iter(self.queue.get, None):
            run_test(t, io_lock=self.io_lock, run_async=True)
            self.queue.task_done()

if args.jobs > 1:
    print("Running tests using %d jobs" % args.jobs)
    testq = Queue()
    for t in tests:
        testq.put(t)
    testlock = threading.Lock()
    # Start worker threads to consume the queue
    threads = [RunTest(queue=testq, io_lock=testlock) for _ in range(args.jobs)]
    for t in threads:
        t.daemon = True
        t.start()
    # wait on the queue rather than the individual threads
    testq.join()
else:
    for t in tests:
        run_test(t, io_lock=None, run_async=False)

# --- all tests are complete by the time we get here ---
if tests:
    tests[0].total_time = time_func() - total_start_time
    print_time("Total execution time for all tests: %.1f seconds\n", tests[0].total_time)

passed = [t for t in tests if t.status == 0]
fail = [t for t in tests if t.status == 1]
no_result = [t for t in tests if t.status == 2]

# print summaries, but only if multiple tests were run
if len(tests) != 1 and args.execute_tests:
    if passed and args.print_passed_summary:
        if len(passed) == 1:
            sys.stdout.write("\nPassed the following test:\n")
        else:
            sys.stdout.write("\nPassed the following %d tests:\n" % len(passed))
        paths = [x.path for x in passed]
        sys.stdout.write("\t" + "\n\t".join(paths) + "\n")
    if fail:
        if len(fail) == 1:
            sys.stdout.write("\nFailed the following test:\n")
        else:
            sys.stdout.write("\nFailed the following %d tests:\n" % len(fail))
        paths = [x.path for x in fail]
        sys.stdout.write("\t" + "\n\t".join(paths) + "\n")
    if no_result:
        if len(no_result) == 1:
            sys.stdout.write("\nNO RESULT from the following test:\n")
        else:
            sys.stdout.write("\nNO RESULT from the following %d tests:\n" % len(no_result))
        paths = [x.path for x in no_result]
        sys.stdout.write("\t" + "\n\t".join(paths) + "\n")

# save the fails to a file
if args.error_log:
    with open(args.error_log, "w") as f:
        if fail:
            paths = [x.path for x in fail]
            for test in paths:
                print(test, file=f)
        # if there are no fails, file will be cleared

if args.xml:
    if args.output == '-':
        f = sys.stdout
    else:
        f = open(args.xml, 'w')
    tests[0].header(f)
    #f.write("test_result = [\n")
    for t in tests:
        t.write(f)
    tests[0].footer(f)
    #f.write("];\n")
    if args.output != '-':
        f.close()

if args.output:
    if isinstance(sys.stdout, Tee):
        sys.stdout.file.close()
    if isinstance(sys.stderr, Tee):
        sys.stderr.file.close()

if fail:
    sys.exit(1)
elif no_result and args.dont_ignore_skips:
    # if no fails, but skips were found
    sys.exit(2)
else:
    sys.exit(0)

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# setup.py

#!python3

import fnmatch
from setuptools import setup
from setuptools.command.build_py import build_py as build_py_orig

import codecs
import os.path


def read(rel_path):
    here = os.path.abspath(os.path.dirname(__file__))
    with codecs.open(os.path.join(here, rel_path), 'r') as fp:
        return fp.read()


def get_version(rel_path):
    for line in read(rel_path).splitlines():
        if line.startswith('__version__'):
            delim = '"' if '"' in line else "'"
            return line.split(delim)[1]
    else:
        raise RuntimeError("Unable to find version string.")


exclude = ['*Tests']


class build_py(build_py_orig):

    def find_package_modules(self, package, package_dir):
        """
        Custom module to find package modules.
        It will strip out any modules which match the glob patters in exclude above
        """
        modules = super().find_package_modules(package, package_dir)
        return [(pkg, mod, file, ) for (pkg, mod, file, ) in modules
                if not any(fnmatch.fnmatchcase(mod, pat=pattern)
                for pattern in exclude)]

setup(
    cmdclass={
        'build_py': build_py,
    },
    version=get_version('SCons/__init__.py'),
)

# SCons/Action.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""SCons Actions.

Information about executing any sort of action that
can build one or more target Nodes (typically files) from one or more
source Nodes (also typically files) given a specific Environment.

The base class here is ActionBase.  The base class supplies just a few
utility methods and some generic methods for displaying information
about an Action in response to the various commands that control printing.

A second-level base class is _ActionAction.  This extends ActionBase
by providing the methods that can be used to show and perform an
action.  True Action objects will subclass _ActionAction; Action
factory class objects will subclass ActionBase.

The heavy lifting is handled by subclasses for the different types of
actions we might execute:

    CommandAction
    CommandGeneratorAction
    FunctionAction
    ListAction

The subclasses supply the following public interface methods used by
other modules:

    __call__()
        THE public interface, "calling" an Action object executes the
        command or Python function.  This also takes care of printing
        a pre-substitution command for debugging purposes.

    get_contents()
        Fetches the "contents" of an Action for signature calculation
        plus the varlist.  This is what gets checksummed to decide
        if a target needs to be rebuilt because its action changed.

    genstring()
        Returns a string representation of the Action *without*
        command substitution, but allows a CommandGeneratorAction to
        generate the right action based on the specified target,
        source and env.  This is used by the Signature subsystem
        (through the Executor) to obtain an (imprecise) representation
        of the Action operation for informative purposes.


Subclasses also supply the following methods for internal use within
this module:

    __str__()
        Returns a string approximation of the Action; no variable
        substitution is performed.

    execute()
        The internal method that really, truly, actually handles the
        execution of a command or Python function.  This is used so
        that the __call__() methods can take care of displaying any
        pre-substitution representations, and *then* execute an action
        without worrying about the specific Actions involved.

    get_presig()
        Fetches the "contents" of a subclass for signature calculation.
        The varlist is added to this to produce the Action's contents.
        TODO(?): Change this to always return bytes and not str?

    strfunction()
        Returns a substituted string representation of the Action.
        This is used by the _ActionAction.show() command to display the
        command/function that will be executed to generate the target(s).

There is a related independent ActionCaller class that looks like a
regular Action, and which serves as a wrapper for arbitrary functions
that we want to let the user specify the arguments to now, but actually
execute later (when an out-of-date check determines that it's needed to
be executed, for example).  Objects of this class are returned by an
ActionFactory class that provides a __call__() method as a convenient
way for wrapping up the functions.

"""

import os
import pickle
import re
import sys
import subprocess
from subprocess import DEVNULL
import inspect
from collections import OrderedDict

import SCons.Debug
import SCons.Util
from SCons.Debug import logInstanceCreation
import SCons.Errors
import SCons.Util
import SCons.Subst

# we use these a lot, so try to optimize them
from SCons.Util import is_String, is_List

class _null:
    pass

print_actions = True
execute_actions = True
print_actions_presub = False

# Use pickle protocol 1 when pickling functions for signature
# otherwise python3 and python2 will yield different pickles
# for the same object.
# This is due to default being 1 for python 2.7, and 3 for 3.x
# TODO: We can roll this forward to 2 (if it has value), but not
# before a deprecation cycle as the sconsigns will change
ACTION_SIGNATURE_PICKLE_PROTOCOL = 1


def rfile(n):
    try:
        return n.rfile()
    except AttributeError:
        return n


def default_exitstatfunc(s):
    return s

strip_quotes = re.compile(r'^[\'"](.*)[\'"]$')


def _callable_contents(obj):
    """Return the signature contents of a callable Python object.
    """
    try:
        # Test if obj is a method.
        return _function_contents(obj.__func__)

    except AttributeError:
        try:
            # Test if obj is a callable object.
            return _function_contents(obj.__call__.__func__)

        except AttributeError:
            try:
                # Test if obj is a code object.
                return _code_contents(obj)

            except AttributeError:
                # Test if obj is a function object.
                return _function_contents(obj)


def _object_contents(obj):
    """Return the signature contents of any Python object.

    We have to handle the case where object contains a code object
    since it can be pickled directly.
    """
    try:
        # Test if obj is a method.
        return _function_contents(obj.__func__)

    except AttributeError:
        try:
            # Test if obj is a callable object.
            return _function_contents(obj.__call__.__func__)

        except AttributeError:
            try:
                # Test if obj is a code object.
                return _code_contents(obj)

            except AttributeError:
                try:
                    # Test if obj is a function object.
                    return _function_contents(obj)

                except AttributeError as ae:
                    # Should be a pickle-able Python object.
                    try:
                        return _object_instance_content(obj)
                        # pickling an Action instance or object doesn't yield a stable
                        # content as instance property may be dumped in different orders
                        # return pickle.dumps(obj, ACTION_SIGNATURE_PICKLE_PROTOCOL)
                    except (pickle.PicklingError, TypeError, AttributeError) as ex:
                        # This is weird, but it seems that nested classes
                        # are unpickable. The Python docs say it should
                        # always be a PicklingError, but some Python
                        # versions seem to return TypeError.  Just do
                        # the best we can.
                        return bytearray(repr(obj), 'utf-8')


def _code_contents(code, docstring=None):
    r"""Return the signature contents of a code object.

    By providing direct access to the code object of the
    function, Python makes this extremely easy.  Hooray!

    Unfortunately, older versions of Python include line
    number indications in the compiled byte code.  Boo!
    So we remove the line number byte codes to prevent
    recompilations from moving a Python function.

    See:
      - https://docs.python.org/2/library/inspect.html
      - http://python-reference.readthedocs.io/en/latest/docs/code/index.html

    For info on what each co\_ variable provides

    The signature is as follows (should be byte/chars):
    co_argcount, len(co_varnames), len(co_cellvars), len(co_freevars),
    ( comma separated signature for each object in co_consts ),
    ( comma separated signature for each object in co_names ),
    ( The bytecode with line number bytecodes removed from  co_code )

    co_argcount - Returns the number of positional arguments (including arguments with default values).
    co_varnames - Returns a tuple containing the names of the local variables (starting with the argument names).
    co_cellvars - Returns a tuple containing the names of local variables that are referenced by nested functions.
    co_freevars - Returns a tuple containing the names of free variables. (?)
    co_consts   - Returns a tuple containing the literals used by the bytecode.
    co_names    - Returns a tuple containing the names used by the bytecode.
    co_code     - Returns a string representing the sequence of bytecode instructions.

    """

    # contents = []

    # The code contents depends on the number of local variables
    # but not their actual names.
    contents = bytearray("{}, {}".format(code.co_argcount, len(code.co_varnames)), 'utf-8')

    contents.extend(b", ")
    contents.extend(bytearray(str(len(code.co_cellvars)), 'utf-8'))
    contents.extend(b", ")
    contents.extend(bytearray(str(len(code.co_freevars)), 'utf-8'))

    # The code contents depends on any constants accessed by the
    # function. Note that we have to call _object_contents on each
    # constants because the code object of nested functions can
    # show-up among the constants.
    z = [_object_contents(cc) for cc in code.co_consts if cc != docstring]
    contents.extend(b',(')
    contents.extend(bytearray(',', 'utf-8').join(z))
    contents.extend(b')')

    # The code contents depends on the variable names used to
    # accessed global variable, as changing the variable name changes
    # the variable actually accessed and therefore changes the
    # function result.
    z= [bytearray(_object_contents(cc)) for cc in code.co_names]
    contents.extend(b',(')
    contents.extend(bytearray(',','utf-8').join(z))
    contents.extend(b')')

    # The code contents depends on its actual code!!!
    contents.extend(b',(')
    contents.extend(code.co_code)
    contents.extend(b')')

    return contents


def _function_contents(func):
    """
    The signature is as follows (should be byte/chars):
    < _code_contents (see above) from func.__code__ >
    ,( comma separated _object_contents for function argument defaults)
    ,( comma separated _object_contents for any closure contents )


    See also: https://docs.python.org/3/reference/datamodel.html
      - func.__code__     - The code object representing the compiled function body.
      - func.__defaults__ - A tuple containing default argument values for those arguments that have defaults, or None if no arguments have a default value
      - func.__closure__  - None or a tuple of cells that contain bindings for the function's free variables.

    :Returns:
      Signature contents of a function. (in bytes)
    """

    contents = [_code_contents(func.__code__, func.__doc__)]

    # The function contents depends on the value of defaults arguments
    if func.__defaults__:

        function_defaults_contents = [_object_contents(cc) for cc in func.__defaults__]

        defaults = bytearray(b',(')
        defaults.extend(bytearray(b',').join(function_defaults_contents))
        defaults.extend(b')')

        contents.append(defaults)
    else:
        contents.append(b',()')

    # The function contents depends on the closure captured cell values.
    closure = func.__closure__ or []

    try:
        closure_contents = [_object_contents(x.cell_contents) for x in closure]
    except AttributeError:
        closure_contents = []

    contents.append(b',(')
    contents.append(bytearray(b',').join(closure_contents))
    contents.append(b')')

    retval = bytearray(b'').join(contents)
    return retval


def _object_instance_content(obj):
    """
    Returns consistant content for a action class or an instance thereof

    :Parameters:
      - `obj` Should be either and action class or an instance thereof

    :Returns:
      bytearray or bytes representing the obj suitable for generating a signature from.
    """
    retval = bytearray()

    if obj is None:
        return b'N.'

    if isinstance(obj, SCons.Util.BaseStringTypes):
        return SCons.Util.to_bytes(obj)

    inst_class = obj.__class__
    inst_class_name = bytearray(obj.__class__.__name__,'utf-8')
    inst_class_module = bytearray(obj.__class__.__module__,'utf-8')
    inst_class_hierarchy = bytearray(repr(inspect.getclasstree([obj.__class__,])),'utf-8')
    # print("ICH:%s : %s"%(inst_class_hierarchy, repr(obj)))

    properties = [(p, getattr(obj, p, "None")) for p in dir(obj) if not (p[:2] == '__' or inspect.ismethod(getattr(obj, p)) or inspect.isbuiltin(getattr(obj,p))) ]
    properties.sort()
    properties_str = ','.join(["%s=%s"%(p[0],p[1]) for p in properties])
    properties_bytes = bytearray(properties_str,'utf-8')

    methods = [p for p in dir(obj) if inspect.ismethod(getattr(obj, p))]
    methods.sort()

    method_contents = []
    for m in methods:
        # print("Method:%s"%m)
        v = _function_contents(getattr(obj, m))
        # print("[%s->]V:%s [%s]"%(m,v,type(v)))
        method_contents.append(v)

    retval = bytearray(b'{')
    retval.extend(inst_class_name)
    retval.extend(b":")
    retval.extend(inst_class_module)
    retval.extend(b'}[[')
    retval.extend(inst_class_hierarchy)
    retval.extend(b']]{{')
    retval.extend(bytearray(b",").join(method_contents))
    retval.extend(b"}}{{{")
    retval.extend(properties_bytes)
    retval.extend(b'}}}')
    return retval

    # print("class          :%s"%inst_class)
    # print("class_name     :%s"%inst_class_name)
    # print("class_module   :%s"%inst_class_module)
    # print("Class hier     :\n%s"%pp.pformat(inst_class_hierarchy))
    # print("Inst Properties:\n%s"%pp.pformat(properties))
    # print("Inst Methods   :\n%s"%pp.pformat(methods))

def _actionAppend(act1, act2):
    # This function knows how to slap two actions together.
    # Mainly, it handles ListActions by concatenating into
    # a single ListAction.
    a1 = Action(act1)
    a2 = Action(act2)
    if a1 is None:
        return a2
    if a2 is None:
        return a1
    if isinstance(a1, ListAction):
        if isinstance(a2, ListAction):
            return ListAction(a1.list + a2.list)
        else:
            return ListAction(a1.list + [ a2 ])
    else:
        if isinstance(a2, ListAction):
            return ListAction([ a1 ] + a2.list)
        else:
            return ListAction([ a1, a2 ])


def _do_create_keywords(args, kw):
    """This converts any arguments after the action argument into
    their equivalent keywords and adds them to the kw argument.
    """
    v = kw.get('varlist', ())
    # prevent varlist="FOO" from being interpreted as ['F', 'O', 'O']
    if is_String(v): v = (v,)
    kw['varlist'] = tuple(v)
    if args:
        # turn positional args into equivalent keywords
        cmdstrfunc = args[0]
        if cmdstrfunc is None or is_String(cmdstrfunc):
            kw['cmdstr'] = cmdstrfunc
        elif callable(cmdstrfunc):
            kw['strfunction'] = cmdstrfunc
        else:
            raise SCons.Errors.UserError(
                'Invalid command display variable type. '
                'You must either pass a string or a callback which '
                'accepts (target, source, env) as parameters.')
        if len(args) > 1:
            kw['varlist'] = tuple(SCons.Util.flatten(args[1:])) + kw['varlist']
    if kw.get('strfunction', _null) is not _null \
                      and kw.get('cmdstr', _null) is not _null:
        raise SCons.Errors.UserError(
            'Cannot have both strfunction and cmdstr args to Action()')


def _do_create_action(act, kw):
    """This is the actual "implementation" for the
    Action factory method, below.  This handles the
    fact that passing lists to Action() itself has
    different semantics than passing lists as elements
    of lists.

    The former will create a ListAction, the latter
    will create a CommandAction by converting the inner
    list elements to strings."""

    if isinstance(act, ActionBase):
        return act

    if is_String(act):
        var=SCons.Util.get_environment_var(act)
        if var:
            # This looks like a string that is purely an Environment
            # variable reference, like "$FOO" or "${FOO}".  We do
            # something special here...we lazily evaluate the contents
            # of that Environment variable, so a user could put something
            # like a function or a CommandGenerator in that variable
            # instead of a string.
            return LazyAction(var, kw)
        commands = str(act).split('\n')
        if len(commands) == 1:
            return CommandAction(commands[0], **kw)
        # The list of string commands may include a LazyAction, so we
        # reprocess them via _do_create_list_action.
        return _do_create_list_action(commands, kw)

    if is_List(act):
        return CommandAction(act, **kw)

    if callable(act):
        try:
            gen = kw['generator']
            del kw['generator']
        except KeyError:
            gen = 0
        if gen:
            action_type = CommandGeneratorAction
        else:
            action_type = FunctionAction
        return action_type(act, kw)

    # Catch a common error case with a nice message:
    if isinstance(act, int) or isinstance(act, float):
        raise TypeError("Don't know how to create an Action from a number (%s)"%act)
    # Else fail silently (???)
    return None


def _do_create_list_action(act, kw):
    """A factory for list actions.  Convert the input list into Actions
    and then wrap them in a ListAction."""
    acts = []
    for a in act:
        aa = _do_create_action(a, kw)
        if aa is not None: acts.append(aa)
    if not acts:
        return ListAction([])
    elif len(acts) == 1:
        return acts[0]
    else:
        return ListAction(acts)


def Action(act, *args, **kw):
    """A factory for action objects."""
    # Really simple: the _do_create_* routines do the heavy lifting.
    _do_create_keywords(args, kw)
    if is_List(act):
        return _do_create_list_action(act, kw)
    return _do_create_action(act, kw)


class ActionBase:
    """Base class for all types of action objects that can be held by
    other objects (Builders, Executors, etc.)  This provides the
    common methods for manipulating and combining those actions."""

    def __eq__(self, other):
        return self.__dict__ == other

    def no_batch_key(self, env, target, source):
        return None

    batch_key = no_batch_key

    def genstring(self, target, source, env):
        return str(self)

    def get_contents(self, target, source, env):
        result = self.get_presig(target, source, env)

        if not isinstance(result,(bytes, bytearray)):
            result = bytearray(result, 'utf-8')
        else:
            # Make a copy and put in bytearray, without this the contents returned by get_presig
            # can be changed by the logic below, appending with each call and causing very
            # hard to track down issues...
            result = bytearray(result)

        # At this point everything should be a bytearray

        # This should never happen, as the Action() factory should wrap
        # the varlist, but just in case an action is created directly,
        # we duplicate this check here.
        vl = self.get_varlist(target, source, env)
        if is_String(vl): vl = (vl,)
        for v in vl:
            # do the subst this way to ignore $(...$) parts:
            if isinstance(result, bytearray):
                result.extend(SCons.Util.to_bytes(env.subst_target_source('${'+v+'}', SCons.Subst.SUBST_SIG, target, source)))
            else:
                raise Exception("WE SHOULD NEVER GET HERE result should be bytearray not:%s"%type(result))
                # result.append(SCons.Util.to_bytes(env.subst_target_source('${'+v+'}', SCons.Subst.SUBST_SIG, target, source)))


        if isinstance(result, (bytes,bytearray)):
            return result
        else:
            raise Exception("WE SHOULD NEVER GET HERE - #2 result should be bytearray not:%s" % type(result))
            # return b''.join(result)

    def __add__(self, other):
        return _actionAppend(self, other)

    def __radd__(self, other):
        return _actionAppend(other, self)

    def presub_lines(self, env):
        # CommandGeneratorAction needs a real environment
        # in order to return the proper string here, since
        # it may call LazyAction, which looks up a key
        # in that env.  So we temporarily remember the env here,
        # and CommandGeneratorAction will use this env
        # when it calls its _generate method.
        self.presub_env = env
        lines = str(self).split('\n')
        self.presub_env = None      # don't need this any more
        return lines

    def get_varlist(self, target, source, env, executor=None):
        return self.varlist

    def get_targets(self, env, executor):
        """
        Returns the type of targets ($TARGETS, $CHANGED_TARGETS) used
        by this action.
        """
        return self.targets


class _ActionAction(ActionBase):
    """Base class for actions that create output objects."""
    def __init__(self, cmdstr=_null, strfunction=_null, varlist=(),
                       presub=_null, chdir=None, exitstatfunc=None,
                       batch_key=None, targets='$TARGETS',
                 **kw):
        self.cmdstr = cmdstr
        if strfunction is not _null:
            if strfunction is None:
                self.cmdstr = None
            else:
                self.strfunction = strfunction
        self.varlist = varlist
        self.presub = presub
        self.chdir = chdir
        if not exitstatfunc:
            exitstatfunc = default_exitstatfunc
        self.exitstatfunc = exitstatfunc

        self.targets = targets

        if batch_key:
            if not callable(batch_key):
                # They have set batch_key, but not to their own
                # callable.  The default behavior here will batch
                # *all* targets+sources using this action, separated
                # for each construction environment.
                def default_batch_key(self, env, target, source):
                    return (id(self), id(env))
                batch_key = default_batch_key
            SCons.Util.AddMethod(self, batch_key, 'batch_key')

    def print_cmd_line(self, s, target, source, env):
        """
        In python 3, and in some of our tests, sys.stdout is
        a String io object, and it takes unicode strings only
        This code assumes s is a regular string.
        """
        sys.stdout.write(s + "\n")

    def __call__(self, target, source, env,
                               exitstatfunc=_null,
                               presub=_null,
                               show=_null,
                               execute=_null,
                               chdir=_null,
                               executor=None):
        if not is_List(target):
            target = [target]
        if not is_List(source):
            source = [source]

        if presub is _null:
            presub = self.presub
            if presub is _null:
                presub = print_actions_presub
        if exitstatfunc is _null:
            exitstatfunc = self.exitstatfunc
        if show is _null:
            show = print_actions
        if execute is _null:
            execute = execute_actions
        if chdir is _null:
            chdir = self.chdir
        save_cwd = None
        if chdir:
            save_cwd = os.getcwd()
            try:
                chdir = str(chdir.get_abspath())
            except AttributeError:
                if not is_String(chdir):
                    if executor:
                        chdir = str(executor.batches[0].targets[0].dir)
                    else:
                        chdir = str(target[0].dir)
        if presub:
            if executor:
                target = executor.get_all_targets()
                source = executor.get_all_sources()
            t = ' and '.join(map(str, target))
            l = '\n  '.join(self.presub_lines(env))
            out = "Building %s with action:\n  %s\n" % (t, l)
            sys.stdout.write(out)
        cmd = None
        if show and self.strfunction:
            if executor:
                target = executor.get_all_targets()
                source = executor.get_all_sources()
            try:
                cmd = self.strfunction(target, source, env, executor)
            except TypeError:
                cmd = self.strfunction(target, source, env)
            if cmd:
                if chdir:
                    cmd = ('os.chdir(%s)\n' % repr(chdir)) + cmd
                try:
                    get = env.get
                except AttributeError:
                    print_func = self.print_cmd_line
                else:
                    print_func = get('PRINT_CMD_LINE_FUNC')
                    if not print_func:
                        print_func = self.print_cmd_line
                print_func(cmd, target, source, env)
        stat = 0
        if execute:
            if chdir:
                os.chdir(chdir)
            try:
                stat = self.execute(target, source, env, executor=executor)
                if isinstance(stat, SCons.Errors.BuildError):
                    s = exitstatfunc(stat.status)
                    if s:
                        stat.status = s
                    else:
                        stat = s
                else:
                    stat = exitstatfunc(stat)
            finally:
                if save_cwd:
                    os.chdir(save_cwd)
        if cmd and save_cwd:
            print_func('os.chdir(%s)' % repr(save_cwd), target, source, env)

        return stat


def _string_from_cmd_list(cmd_list):
    """Takes a list of command line arguments and returns a pretty
    representation for printing."""
    cl = []
    for arg in map(str, cmd_list):
        if ' ' in arg or '\t' in arg:
            arg = '"' + arg + '"'
        cl.append(arg)
    return ' '.join(cl)

default_ENV = None


def get_default_ENV(env):
    """Returns an execution environment.

    If there is one in *env*, just use it, else return the Default
    Environment, insantiated if necessary.

    A fiddlin' little function that has an ``import SCons.Environment``
    which cannot be moved to the top level without creating an import
    loop.  Since this import creates a local variable named ``SCons``,
    it blocks access to the global variable, so we move it here to
    prevent complaints about local variables being used uninitialized.
    """
    global default_ENV

    try:
        return env['ENV']
    except KeyError:
        if not default_ENV:
            import SCons.Environment
            # This is a hideously expensive way to get a default execution
            # environment.  What it really should do is run the platform
            # setup to get the default ENV.  Fortunately, it's incredibly
            # rare for an Environment not to have an execution environment,
            # so we're not going to worry about it overmuch.
            default_ENV = SCons.Environment.Environment()['ENV']
        return default_ENV


def _resolve_shell_env(env, target, source):
    """Returns a resolved execution environment.

    First get the execution environment.  Then if ``SHELL_ENV_GENERATORS``
    is set and is iterable, call each function to allow it to alter the
    created execution environment, passing each the returned execution
    environment from the previous call.

    .. versionadded:: 4.4
    """
    ENV = get_default_ENV(env)
    shell_gen = env.get('SHELL_ENV_GENERATORS')
    if shell_gen:
        try:
            shell_gens = iter(shell_gen)
        except TypeError:
            raise SCons.Errors.UserError("SHELL_ENV_GENERATORS must be iteratable.")
        else:
            ENV = ENV.copy()
            for generator in shell_gens:
                ENV = generator(env, target, source, ENV)
                if not isinstance(ENV, dict):
                    raise SCons.Errors.UserError(f"SHELL_ENV_GENERATORS function: {generator} must return a dict.")
    return ENV


def _subproc(scons_env, cmd, error='ignore', **kw):
    """Wrapper for subprocess which pulls from construction env.

    Use for calls to subprocess which need to interpolate values from
    an SCons construction environment into the environment passed to
    subprocess.  Adds an an error-handling argument.  Adds ability
    to specify std{in,out,err} with "'devnull'" tag.
    """
    # TODO: just uses subprocess.DEVNULL now, we can drop the "devnull"
    # string now - it is a holdover from Py2, which didn't have DEVNULL.
    for stream in 'stdin', 'stdout', 'stderr':
        io = kw.get(stream)
        if is_String(io) and io == 'devnull':
            kw[stream] = DEVNULL

    # Figure out what execution environment to use
    ENV = kw.get('env', None)
    if ENV is None: ENV = get_default_ENV(scons_env)

    kw['env'] = SCons.Util.sanitize_shell_env(ENV)

    try:
        pobj = subprocess.Popen(cmd, **kw)
    except EnvironmentError as e:
        if error == 'raise': raise
        # return a dummy Popen instance that only returns error
        class dummyPopen:
            def __init__(self, e):
                self.exception = e
            # Add the following two to enable using the return value as a context manager
            # for example
            #    with Action._subproc(...) as po:
            #       logic here which uses po

            def __enter__(self):
                return self

            def __exit__(self, *args):
                pass

            def communicate(self, input=None):
                return ('', '')

            def wait(self):
                return -self.exception.errno

            stdin = None
            class f:
                def read(self): return ''
                def readline(self): return ''
                def __iter__(self): return iter(())
            stdout = stderr = f()
        pobj = dummyPopen(e)
    finally:
        # clean up open file handles stored in parent's kw
        for k, v in kw.items():
            if inspect.ismethod(getattr(v, 'close', None)):
                v.close()

    return pobj


class CommandAction(_ActionAction):
    """Class for command-execution actions."""
    def __init__(self, cmd, **kw):
        # Cmd can actually be a list or a single item; if it's a
        # single item it should be the command string to execute; if a
        # list then it should be the words of the command string to
        # execute.  Only a single command should be executed by this
        # object; lists of commands should be handled by embedding
        # these objects in a ListAction object (which the Action()
        # factory above does).  cmd will be passed to
        # Environment.subst_list() for substituting environment
        # variables.
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Action.CommandAction')

        super().__init__(**kw)
        if is_List(cmd):
            if [c for c in cmd if is_List(c)]:
                raise TypeError("CommandAction should be given only "
                                "a single command")
        self.cmd_list = cmd

    def __str__(self):
        if is_List(self.cmd_list):
            return ' '.join(map(str, self.cmd_list))
        return str(self.cmd_list)

    def process(self, target, source, env, executor=None):
        if executor:
            result = env.subst_list(self.cmd_list, 0, executor=executor)
        else:
            result = env.subst_list(self.cmd_list, 0, target, source)
        silent = None
        ignore = None
        while True:
            try: c = result[0][0][0]
            except IndexError: c = None
            if c == '@': silent = 1
            elif c == '-': ignore = 1
            else: break
            result[0][0] = result[0][0][1:]
        try:
            if not result[0][0]:
                result[0] = result[0][1:]
        except IndexError:
            pass
        return result, ignore, silent

    def strfunction(self, target, source, env, executor=None):
        if self.cmdstr is None:
            return None
        if self.cmdstr is not _null:
            from SCons.Subst import SUBST_RAW
            if executor:
                c = env.subst(self.cmdstr, SUBST_RAW, executor=executor)
            else:
                c = env.subst(self.cmdstr, SUBST_RAW, target, source)
            if c:
                return c
        cmd_list, ignore, silent = self.process(target, source, env, executor)
        if silent:
            return ''
        return _string_from_cmd_list(cmd_list[0])

    def execute(self, target, source, env, executor=None):
        """Execute a command action.

        This will handle lists of commands as well as individual commands,
        because construction variable substitution may turn a single
        "command" into a list.  This means that this class can actually
        handle lists of commands, even though that's not how we use it
        externally.
        """
        escape_list = SCons.Subst.escape_list
        flatten_sequence = SCons.Util.flatten_sequence

        try:
            shell = env['SHELL']
        except KeyError:
            raise SCons.Errors.UserError('Missing SHELL construction variable.')

        try:
            spawn = env['SPAWN']
        except KeyError:
            raise SCons.Errors.UserError('Missing SPAWN construction variable.')
        else:
            if is_String(spawn):
                spawn = env.subst(spawn, raw=1, conv=lambda x: x)

        escape = env.get('ESCAPE', lambda x: x)

        ENV = _resolve_shell_env(env, target, source)

        # Ensure that the ENV values are all strings:
        for key, value in ENV.items():
            if not is_String(value):
                if is_List(value):
                    # If the value is a list, then we assume it is a
                    # path list, because that's a pretty common list-like
                    # value to stick in an environment variable:
                    value = flatten_sequence(value)
                    ENV[key] = os.pathsep.join(map(str, value))
                else:
                    # If it isn't a string or a list, then we just coerce
                    # it to a string, which is the proper way to handle
                    # Dir and File instances and will produce something
                    # reasonable for just about everything else:
                    ENV[key] = str(value)

        if executor:
            target = executor.get_all_targets()
            source = executor.get_all_sources()
        cmd_list, ignore, silent = self.process(target, list(map(rfile, source)), env, executor)

        # Use len() to filter out any "command" that's zero-length.
        for cmd_line in filter(len, cmd_list):
            # Escape the command line for the interpreter we are using.
            cmd_line = escape_list(cmd_line, escape)
            result = spawn(shell, escape, cmd_line[0], cmd_line, ENV)
            if not ignore and result:
                msg = "Error %s" % result
                return SCons.Errors.BuildError(errstr=msg,
                                               status=result,
                                               action=self,
                                               command=cmd_line)
        return 0

    def get_presig(self, target, source, env, executor=None):
        """Return the signature contents of this action's command line.

        This strips $(-$) and everything in between the string,
        since those parts don't affect signatures.
        """
        from SCons.Subst import SUBST_SIG
        cmd = self.cmd_list
        if is_List(cmd):
            cmd = ' '.join(map(str, cmd))
        else:
            cmd = str(cmd)
        if executor:
            return env.subst_target_source(cmd, SUBST_SIG, executor=executor)
        else:
            return env.subst_target_source(cmd, SUBST_SIG, target, source)

    def get_implicit_deps(self, target, source, env, executor=None):
        """Return the implicit dependencies of this action's command line."""
        icd = env.get('IMPLICIT_COMMAND_DEPENDENCIES', True)
        if is_String(icd) and icd[:1] == '$':
            icd = env.subst(icd)

        if not icd or str(icd).lower() in ('0', 'none', 'false', 'no', 'off'):
            return []

        try:
            icd_int = int(icd)
        except ValueError:
            icd_int = None

        if (icd_int and icd_int > 1) or str(icd).lower() == 'all':
            # An integer value greater than 1 specifies the number of entries
            # to scan. "all" means to scan all.
            return self._get_implicit_deps_heavyweight(target, source, env, executor, icd_int)
        else:
            # Everything else (usually 1 or True) means that we want
            # lightweight dependency scanning.
            return self._get_implicit_deps_lightweight(target, source, env, executor)

    def _get_implicit_deps_lightweight(self, target, source, env, executor):
        """
        Lightweight dependency scanning involves only scanning the first entry
        in an action string, even if it contains &&.
        """
        from SCons.Subst import SUBST_SIG
        if executor:
            cmd_list = env.subst_list(self.cmd_list, SUBST_SIG, executor=executor)
        else:
            cmd_list = env.subst_list(self.cmd_list, SUBST_SIG, target, source)
        res = []
        for cmd_line in cmd_list:
            if cmd_line:
                d = str(cmd_line[0])
                m = strip_quotes.match(d)
                if m:
                    d = m.group(1)
                d = env.WhereIs(d)
                if d:
                    res.append(env.fs.File(d))
        return res

    def _get_implicit_deps_heavyweight(self, target, source, env, executor,
                                       icd_int):
        """
        Heavyweight dependency scanning involves scanning more than just the
        first entry in an action string. The exact behavior depends on the
        value of icd_int. Only files are taken as implicit dependencies;
        directories are ignored.

        If icd_int is an integer value, it specifies the number of entries to
        scan for implicit dependencies. Action strings are also scanned after
        a &&. So for example, if icd_int=2 and the action string is
        "cd <some_dir> && $PYTHON $SCRIPT_PATH <another_path>", the implicit
        dependencies would be the path to the python binary and the path to the
        script.

        If icd_int is None, all entries are scanned for implicit dependencies.
        """

        # Avoid circular and duplicate dependencies by not providing source,
        # target, or executor to subst_list. This causes references to
        # $SOURCES, $TARGETS, and all related variables to disappear.
        from SCons.Subst import SUBST_SIG
        cmd_list = env.subst_list(self.cmd_list, SUBST_SIG, conv=lambda x: x)
        res = []

        for cmd_line in cmd_list:
            if cmd_line:
                entry_count = 0
                for entry in cmd_line:
                    d = str(entry)
                    if ((icd_int is None or entry_count < icd_int) and
                            not d.startswith(('&', '-', '/') if os.name == 'nt'
                                             else ('&', '-'))):
                        m = strip_quotes.match(d)
                        if m:
                            d = m.group(1)

                        if d:
                            # Resolve the first entry in the command string using
                            # PATH, which env.WhereIs() looks in.
                            # For now, only match files, not directories.
                            p = os.path.abspath(d) if os.path.isfile(d) else None
                            if not p and entry_count == 0:
                                p = env.WhereIs(d)

                            if p:
                                res.append(env.fs.File(p))

                        entry_count = entry_count + 1
                    else:
                        entry_count = 0 if d == '&&' else entry_count + 1

        # Despite not providing source and target to env.subst() above, we
        # can still end up with sources in this list. For example, files in
        # LIBS will still resolve in env.subst(). This won't result in
        # circular dependencies, but it causes problems with cache signatures
        # changing between full and incremental builds.
        return [r for r in res if r not in target and r not in source]


class CommandGeneratorAction(ActionBase):
    """Class for command-generator actions."""
    def __init__(self, generator, kw):
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Action.CommandGeneratorAction')
        self.generator = generator
        self.gen_kw = kw
        self.varlist = kw.get('varlist', ())
        self.targets = kw.get('targets', '$TARGETS')

    def _generate(self, target, source, env, for_signature, executor=None):
        # ensure that target is a list, to make it easier to write
        # generator functions:
        if not is_List(target):
            target = [target]

        if executor:
            target = executor.get_all_targets()
            source = executor.get_all_sources()
        ret = self.generator(target=target,
                             source=source,
                             env=env,
                             for_signature=for_signature)
        gen_cmd = Action(ret, **self.gen_kw)
        if not gen_cmd:
            raise SCons.Errors.UserError("Object returned from command generator: %s cannot be used to create an Action." % repr(ret))
        return gen_cmd

    def __str__(self):
        try:
            env = self.presub_env
        except AttributeError:
            env = None
        if env is None:
            env = SCons.Defaults.DefaultEnvironment()
        act = self._generate([], [], env, 1)
        return str(act)

    def batch_key(self, env, target, source):
        return self._generate(target, source, env, 1).batch_key(env, target, source)

    def genstring(self, target, source, env, executor=None):
        return self._generate(target, source, env, 1, executor).genstring(target, source, env)

    def __call__(self, target, source, env, exitstatfunc=_null, presub=_null,
                 show=_null, execute=_null, chdir=_null, executor=None):
        act = self._generate(target, source, env, 0, executor)
        if act is None:
            raise SCons.Errors.UserError(
                "While building `%s': "
                "Cannot deduce file extension from source files: %s"
                % (repr(list(map(str, target))), repr(list(map(str, source))))
            )
        return act(
            target, source, env, exitstatfunc, presub, show, execute, chdir, executor
        )

    def get_presig(self, target, source, env, executor=None):
        """Return the signature contents of this action's command line.

        This strips $(-$) and everything in between the string,
        since those parts don't affect signatures.
        """
        return self._generate(target, source, env, 1, executor).get_presig(target, source, env)

    def get_implicit_deps(self, target, source, env, executor=None):
        return self._generate(target, source, env, 1, executor).get_implicit_deps(target, source, env)

    def get_varlist(self, target, source, env, executor=None):
        return self._generate(target, source, env, 1, executor).get_varlist(target, source, env, executor)

    def get_targets(self, env, executor):
        return self._generate(None, None, env, 1, executor).get_targets(env, executor)


class LazyAction(CommandGeneratorAction, CommandAction):
    """
    A LazyAction is a kind of hybrid generator and command action for
    strings of the form "$VAR".  These strings normally expand to other
    strings (think "$CCCOM" to "$CC -c -o $TARGET $SOURCE"), but we also
    want to be able to replace them with functions in the construction
    environment.  Consequently, we want lazy evaluation and creation of
    an Action in the case of the function, but that's overkill in the more
    normal case of expansion to other strings.

    So we do this with a subclass that's both a generator *and*
    a command action.  The overridden methods all do a quick check
    of the construction variable, and if it's a string we just call
    the corresponding CommandAction method to do the heavy lifting.
    If not, then we call the same-named CommandGeneratorAction method.
    The CommandGeneratorAction methods work by using the overridden
    _generate() method, that is, our own way of handling "generation" of
    an action based on what's in the construction variable.
    """

    def __init__(self, var, kw):
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Action.LazyAction')
        CommandAction.__init__(self, '${'+var+'}', **kw)
        self.var = SCons.Util.to_String(var)
        self.gen_kw = kw

    def get_parent_class(self, env):
        c = env.get(self.var)
        if is_String(c) and '\n' not in c:
            return CommandAction
        return CommandGeneratorAction

    def _generate_cache(self, env):
        if env:
            c = env.get(self.var, '')
        else:
            c = ''
        gen_cmd = Action(c, **self.gen_kw)
        if not gen_cmd:
            raise SCons.Errors.UserError("$%s value %s cannot be used to create an Action." % (self.var, repr(c)))
        return gen_cmd

    def _generate(self, target, source, env, for_signature, executor=None):
        return self._generate_cache(env)

    def __call__(self, target, source, env, *args, **kw):
        c = self.get_parent_class(env)
        return c.__call__(self, target, source, env, *args, **kw)

    def get_presig(self, target, source, env):
        c = self.get_parent_class(env)
        return c.get_presig(self, target, source, env)

    def get_varlist(self, target, source, env, executor=None):
        c = self.get_parent_class(env)
        return c.get_varlist(self, target, source, env, executor)


class FunctionAction(_ActionAction):
    """Class for Python function actions."""

    def __init__(self, execfunction, kw):
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Action.FunctionAction')

        self.execfunction = execfunction
        try:
            self.funccontents = _callable_contents(execfunction)
        except AttributeError:
            try:
                # See if execfunction will do the heavy lifting for us.
                self.gc = execfunction.get_contents
            except AttributeError:
                # This is weird, just do the best we can.
                self.funccontents = _object_contents(execfunction)

        super().__init__(**kw)

    def function_name(self):
        try:
            return self.execfunction.__name__
        except AttributeError:
            try:
                return self.execfunction.__class__.__name__
            except AttributeError:
                return "unknown_python_function"

    def strfunction(self, target, source, env, executor=None):
        if self.cmdstr is None:
            return None
        if self.cmdstr is not _null:
            from SCons.Subst import SUBST_RAW
            if executor:
                c = env.subst(self.cmdstr, SUBST_RAW, executor=executor)
            else:
                c = env.subst(self.cmdstr, SUBST_RAW, target, source)
            if c:
                return c

        def array(a):
            def quote(s):
                try:
                    str_for_display = s.str_for_display
                except AttributeError:
                    s = repr(s)
                else:
                    s = str_for_display()
                return s
            return '[' + ", ".join(map(quote, a)) + ']'
        try:
            strfunc = self.execfunction.strfunction
        except AttributeError:
            pass
        else:
            if strfunc is None:
                return None
            if callable(strfunc):
                return strfunc(target, source, env)
        name = self.function_name()
        tstr = array(target)
        sstr = array(source)
        return "%s(%s, %s)" % (name, tstr, sstr)

    def __str__(self):
        name = self.function_name()
        if name == 'ActionCaller':
            return str(self.execfunction)
        return "%s(target, source, env)" % name

    def execute(self, target, source, env, executor=None):
        exc_info = (None,None,None)
        try:
            if executor:
                target = executor.get_all_targets()
                source = executor.get_all_sources()
            rsources = list(map(rfile, source))
            try:
                result = self.execfunction(target=target, source=rsources, env=env)
            except KeyboardInterrupt as e:
                raise
            except SystemExit as e:
                raise
            except Exception as e:
                result = e
                exc_info = sys.exc_info()

            if result:
                result = SCons.Errors.convert_to_BuildError(result, exc_info)
                result.node=target
                result.action=self
                try:
                    result.command=self.strfunction(target, source, env, executor)
                except TypeError:
                    result.command=self.strfunction(target, source, env)

                # FIXME: This maintains backward compatibility with respect to
                # which type of exceptions were returned by raising an
                # exception and which ones were returned by value. It would
                # probably be best to always return them by value here, but
                # some codes do not check the return value of Actions and I do
                # not have the time to modify them at this point.
                if (exc_info[1] and
                    not isinstance(exc_info[1],EnvironmentError)):
                    raise result

            return result
        finally:
            # Break the cycle between the traceback object and this
            # function stack frame. See the sys.exc_info() doc info for
            # more information about this issue.
            del exc_info

    def get_presig(self, target, source, env):
        """Return the signature contents of this callable action."""
        try:
            return self.gc(target, source, env)
        except AttributeError:
            return self.funccontents

    def get_implicit_deps(self, target, source, env):
        return []

class ListAction(ActionBase):
    """Class for lists of other actions."""
    def __init__(self, actionlist):
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Action.ListAction')
        def list_of_actions(x):
            if isinstance(x, ActionBase):
                return x
            return Action(x)
        self.list = list(map(list_of_actions, actionlist))
        # our children will have had any varlist
        # applied; we don't need to do it again
        self.varlist = ()
        self.targets = '$TARGETS'

    def genstring(self, target, source, env):
        return '\n'.join([a.genstring(target, source, env) for a in self.list])

    def __str__(self):
        return '\n'.join(map(str, self.list))

    def presub_lines(self, env):
        return SCons.Util.flatten_sequence(
            [a.presub_lines(env) for a in self.list])

    def get_presig(self, target, source, env):
        """Return the signature contents of this action list.

        Simple concatenation of the signatures of the elements.
        """
        return b"".join([bytes(x.get_contents(target, source, env)) for x in self.list])

    def __call__(self, target, source, env, exitstatfunc=_null, presub=_null,
                 show=_null, execute=_null, chdir=_null, executor=None):
        if executor:
            target = executor.get_all_targets()
            source = executor.get_all_sources()
        for act in self.list:
            stat = act(target, source, env, exitstatfunc, presub,
                       show, execute, chdir, executor)
            if stat:
                return stat
        return 0

    def get_implicit_deps(self, target, source, env):
        result = []
        for act in self.list:
            result.extend(act.get_implicit_deps(target, source, env))
        return result

    def get_varlist(self, target, source, env, executor=None):
        result = OrderedDict()
        for act in self.list:
            for var in act.get_varlist(target, source, env, executor):
                result[var] = True
        return list(result.keys())


class ActionCaller:
    """A class for delaying calling an Action function with specific
    (positional and keyword) arguments until the Action is actually
    executed.

    This class looks to the rest of the world like a normal Action object,
    but what it's really doing is hanging on to the arguments until we
    have a target, source and env to use for the expansion.
    """
    def __init__(self, parent, args, kw):
        self.parent = parent
        self.args = args
        self.kw = kw

    def get_contents(self, target, source, env):
        actfunc = self.parent.actfunc
        try:
            # "self.actfunc" is a function.
            contents = actfunc.__code__.co_code
        except AttributeError:
            # "self.actfunc" is a callable object.
            try:
                contents = actfunc.__call__.__func__.__code__.co_code
            except AttributeError:
                # No __call__() method, so it might be a builtin
                # or something like that.  Do the best we can.
                contents = repr(actfunc)

        return contents

    def subst(self, s, target, source, env):
        # If s is a list, recursively apply subst()
        # to every element in the list
        if is_List(s):
            result = []
            for elem in s:
                result.append(self.subst(elem, target, source, env))
            return self.parent.convert(result)

        # Special-case hack:  Let a custom function wrapped in an
        # ActionCaller get at the environment through which the action
        # was called by using this hard-coded value as a special return.
        if s == '$__env__':
            return env
        elif is_String(s):
            return env.subst(s, 1, target, source)
        return self.parent.convert(s)

    def subst_args(self, target, source, env):
        return [self.subst(x, target, source, env) for x in self.args]

    def subst_kw(self, target, source, env):
        kw = {}
        for key in list(self.kw.keys()):
            kw[key] = self.subst(self.kw[key], target, source, env)
        return kw

    def __call__(self, target, source, env, executor=None):
        args = self.subst_args(target, source, env)
        kw = self.subst_kw(target, source, env)
        return self.parent.actfunc(*args, **kw)

    def strfunction(self, target, source, env):
        args = self.subst_args(target, source, env)
        kw = self.subst_kw(target, source, env)
        return self.parent.strfunc(*args, **kw)

    def __str__(self):
        return self.parent.strfunc(*self.args, **self.kw)


class ActionFactory:
    """A factory class that will wrap up an arbitrary function
    as an SCons-executable Action object.

    The real heavy lifting here is done by the ActionCaller class.
    We just collect the (positional and keyword) arguments that we're
    called with and give them to the ActionCaller object we create,
    so it can hang onto them until it needs them.
    """
    def __init__(self, actfunc, strfunc, convert=lambda x: x):
        self.actfunc = actfunc
        self.strfunc = strfunc
        self.convert = convert

    def __call__(self, *args, **kw):
        ac = ActionCaller(self, args, kw)
        action = Action(ac, strfunction=ac.strfunction)
        return action

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/ActionTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#

# Define a null function and a null class for use as builder actions.
# Where these are defined in the file seems to affect their byte-code
# contents, so try to minimize changes by defining them here, before we
# even import anything.

def GlobalFunc():
    pass


class GlobalActFunc:
    def __call__(self):
        pass


import io
import os
import sys
import types
import unittest
import subprocess

import SCons.Action
import SCons.Environment
import SCons.Errors

import TestCmd

# Initial setup of the common environment for all tests,
# a temporary working directory containing a
# script for writing arguments to an output file.
#
# We don't do this as a setUp() method because it's
# unnecessary to create a separate directory and script
# for each test, they can just use the one.
test = TestCmd.TestCmd(workdir='')

test.write('act.py', """\
import os, string, sys

with open(sys.argv[1], 'w') as f:
    f.write("act.py: '" + "' '".join(sys.argv[2:]) + "'\\n")
    try:
        if sys.argv[3]:
            f.write("act.py: '" + os.environ[sys.argv[3]] + "'\\n")
    except:
        pass

if 'ACTPY_PIPE' in os.environ:
    if 'PIPE_STDOUT_FILE' in os.environ:
        with open(os.environ['PIPE_STDOUT_FILE'], 'r') as f:
            stdout_msg = f.read()
    else:
        stdout_msg = "act.py: stdout: executed act.py %s\\n" % ' '.join(sys.argv[1:])
    sys.stdout.write(stdout_msg)
    if 'PIPE_STDERR_FILE' in os.environ:
        with open(os.environ['PIPE_STDERR_FILE'], 'r') as f:
            stderr_msg = f.read()
    else:
        stderr_msg = "act.py: stderr: executed act.py %s\\n" % ' '.join(sys.argv[1:])
    sys.stderr.write(stderr_msg)
sys.exit(0)
""")

test.write('exit.py', """\
import sys
sys.exit(int(sys.argv[1]))
""")

act_py = test.workpath('act.py')
exit_py = test.workpath('exit.py')

outfile = test.workpath('outfile')
outfile2 = test.workpath('outfile2')
pipe_file = test.workpath('pipe.out')

scons_env = SCons.Environment.Environment()

# Capture all the stuff the Actions will print,
# so it doesn't clutter the output.
sys.stdout = io.StringIO()


class CmdStringHolder:
    def __init__(self, cmd, literal=None):
        self.data = str(cmd)
        self.literal = literal

    def is_literal(self):
        return self.literal

    def escape(self, escape_func):
        """Escape the string with the supplied function.

        The function is expected to take an arbitrary string, then
        return it with all special characters escaped and ready
        for passing to the command interpreter.

        After calling this function, the next call to str() will
        return the escaped string.
        """

        if self.is_literal():
            return escape_func(self.data)
        elif ' ' in self.data or '\t' in self.data:
            return '"%s"' % self.data
        else:
            return self.data


class Environment:
    def __init__(self, **kw):
        self.d = {}
        self.d['SHELL'] = scons_env['SHELL']
        self.d['SPAWN'] = scons_env['SPAWN']
        self.d['PSPAWN'] = scons_env['PSPAWN']
        self.d['ESCAPE'] = scons_env['ESCAPE']
        for k, v in kw.items():
            self.d[k] = v

    # Just use the underlying scons_subst*() utility methods.
    def subst(self, strSubst, raw=0, target=[], source=[], conv=None):
        return SCons.Subst.scons_subst(strSubst, self, raw,
                                       target, source, self.d, conv=conv)

    subst_target_source = subst

    def subst_list(self, strSubst, raw=0, target=[], source=[], conv=None):
        return SCons.Subst.scons_subst_list(strSubst, self, raw,
                                            target, source, self.d, conv=conv)

    def __getitem__(self, item):
        return self.d[item]

    def __setitem__(self, item, value):
        self.d[item] = value

    def __contains__(self, key):
        return key in self.d

    def get(self, key, value=None):
        return self.d.get(key, value)

    def items(self):
        return list(self.d.items())

    def Dictionary(self):
        return self.d

    def Clone(self, **kw):
        res = Environment()
        res.d = SCons.Util.semi_deepcopy(self.d)
        for k, v in kw.items():
            res.d[k] = v
        return res

    def sig_dict(self):
        d = {}
        for k, v in self.items(): d[k] = v
        d['TARGETS'] = ['__t1__', '__t2__', '__t3__', '__t4__', '__t5__', '__t6__']
        d['TARGET'] = d['TARGETS'][0]
        d['SOURCES'] = ['__s1__', '__s2__', '__s3__', '__s4__', '__s5__', '__s6__']
        d['SOURCE'] = d['SOURCES'][0]
        return d


class DummyNode:
    def __init__(self, name):
        self.name = name

    def str_for_display(self):
        return '"' + self.name + '"'

    def __str__(self):
        return self.name

    def rfile(self):
        return self

    def get_subst_proxy(self):
        return self


if os.name == 'java':
    python = os.path.join(sys.prefix, 'jython')
else:
    python = os.environ.get('python_executable', sys.executable)
_python_ = test.escape(python)

_null = SCons.Action._null


def test_varlist(pos_call, str_call, cmd, cmdstrfunc, **kw):
    def call_action(a, pos_call=pos_call, str_call=str_call, kw=kw):
        a = SCons.Action.Action(*a, **kw)
        # returned object must provide these entry points
        assert hasattr(a, '__call__')
        assert hasattr(a, 'get_contents')
        assert hasattr(a, 'genstring')
        pos_call(a)
        str_call(a)
        return a

    a = call_action((cmd, cmdstrfunc))
    assert a.varlist == (), a.varlist

    a = call_action((cmd, cmdstrfunc, 'foo'))
    assert a.varlist == ('foo',), a.varlist

    a = call_action((cmd, cmdstrfunc, 'a', 'b', 'c'))
    assert a.varlist == ('a', 'b', 'c'), a.varlist

    a = call_action((cmd, cmdstrfunc, ['a', 'b', 'c']))
    assert a.varlist == ('a', 'b', 'c'), a.varlist

    kw['varlist'] = 'foo'
    a = call_action((cmd, cmdstrfunc))
    assert a.varlist == ('foo',), a.varlist

    kw['varlist'] = ['x', 'y', 'z']
    a = call_action((cmd, cmdstrfunc))
    assert a.varlist == ('x', 'y', 'z'), a.varlist

    a = call_action((cmd, cmdstrfunc, 'foo'))
    assert a.varlist == ('foo', 'x', 'y', 'z'), a.varlist

    a = call_action((cmd, cmdstrfunc, 'a', 'b', 'c'))
    assert a.varlist == ('a', 'b', 'c', 'x', 'y', 'z'), a.varlist


def test_positional_args(pos_callback, cmd, **kw):
    """Test that Action returns the expected type and positional args work."""

    act = SCons.Action.Action(cmd, **kw)
    pos_callback(act)
    assert act.varlist == (), act.varlist

    if not isinstance(act, SCons.Action._ActionAction):
        # only valid cmdstrfunc is None
        def none(a):
            pass

        test_varlist(pos_callback, none, cmd, None, **kw)
    else:
        # _ActionAction should have set these
        assert hasattr(act, 'strfunction')
        assert act.cmdstr is _null, act.cmdstr
        assert act.presub is _null, act.presub
        assert act.chdir is None, act.chdir
        assert act.exitstatfunc is SCons.Action.default_exitstatfunc, \
            act.exitstatfunc

        def cmdstr(a):
            assert hasattr(a, 'strfunction')
            assert a.cmdstr == 'cmdstr', a.cmdstr

        test_varlist(pos_callback, cmdstr, cmd, 'cmdstr', **kw)

        def fun():
            pass

        def strfun(a, fun=fun):
            assert a.strfunction is fun, a.strfunction
            assert a.cmdstr == _null, a.cmdstr

        test_varlist(pos_callback, strfun, cmd, fun, **kw)

        def none(a):
            assert hasattr(a, 'strfunction')
            assert a.cmdstr is None, a.cmdstr

        test_varlist(pos_callback, none, cmd, None, **kw)

        # Test handling of bad cmdstrfunc arguments
        try:
            a = SCons.Action.Action(cmd, [], **kw)
        except SCons.Errors.UserError as e:
            s = str(e)
            m = 'Invalid command display variable'
            assert s.find(m) != -1, 'Unexpected string:  %s' % s
        else:
            raise Exception("did not catch expected UserError")

    return act


class ActionTestCase(unittest.TestCase):
    """Test the Action() factory function"""

    def test_FunctionAction(self):
        """Test the Action() factory's creation of FunctionAction objects."""

        def foo():
            pass

        def func_action(a, foo=foo):
            assert isinstance(a, SCons.Action.FunctionAction), a
            assert a.execfunction == foo, a.execfunction

        test_positional_args(func_action, foo)
        # a singleton list returns the contained action
        test_positional_args(func_action, [foo])

    def test_CommandAction(self):
        """Test the Action() factory's creation of CommandAction objects."""

        def cmd_action(a):
            assert isinstance(a, SCons.Action.CommandAction), a
            assert a.cmd_list == "string", a.cmd_list

        test_positional_args(cmd_action, "string")
        # a singleton list returns the contained action
        test_positional_args(cmd_action, ["string"])

        def line_action(a):
            assert isinstance(a, SCons.Action.CommandAction), a
            assert a.cmd_list == ["explicit", "command", "line"], a.cmd_list

        test_positional_args(line_action, [["explicit", "command", "line"]])

    def test_ListAction(self):
        """Test the Action() factory's creation of ListAction objects."""

        a1 = SCons.Action.Action(["x", "y", "z", ["a", "b", "c"]])
        assert isinstance(a1, SCons.Action.ListAction), a1
        assert a1.varlist == (), a1.varlist
        assert isinstance(a1.list[0], SCons.Action.CommandAction), a1.list[0]
        assert a1.list[0].cmd_list == "x", a1.list[0].cmd_list
        assert isinstance(a1.list[1], SCons.Action.CommandAction), a1.list[1]
        assert a1.list[1].cmd_list == "y", a1.list[1].cmd_list
        assert isinstance(a1.list[2], SCons.Action.CommandAction), a1.list[2]
        assert a1.list[2].cmd_list == "z", a1.list[2].cmd_list
        assert isinstance(a1.list[3], SCons.Action.CommandAction), a1.list[3]
        assert a1.list[3].cmd_list == ["a", "b", "c"], a1.list[3].cmd_list

        a2 = SCons.Action.Action("x\ny\nz")
        assert isinstance(a2, SCons.Action.ListAction), a2
        assert a2.varlist == (), a2.varlist
        assert isinstance(a2.list[0], SCons.Action.CommandAction), a2.list[0]
        assert a2.list[0].cmd_list == "x", a2.list[0].cmd_list
        assert isinstance(a2.list[1], SCons.Action.CommandAction), a2.list[1]
        assert a2.list[1].cmd_list == "y", a2.list[1].cmd_list
        assert isinstance(a2.list[2], SCons.Action.CommandAction), a2.list[2]
        assert a2.list[2].cmd_list == "z", a2.list[2].cmd_list

        def foo():
            pass

        a3 = SCons.Action.Action(["x", foo, "z"])
        assert isinstance(a3, SCons.Action.ListAction), a3
        assert a3.varlist == (), a3.varlist
        assert isinstance(a3.list[0], SCons.Action.CommandAction), a3.list[0]
        assert a3.list[0].cmd_list == "x", a3.list[0].cmd_list
        assert isinstance(a3.list[1], SCons.Action.FunctionAction), a3.list[1]
        assert a3.list[1].execfunction == foo, a3.list[1].execfunction
        assert isinstance(a3.list[2], SCons.Action.CommandAction), a3.list[2]
        assert a3.list[2].cmd_list == "z", a3.list[2].cmd_list

        a4 = SCons.Action.Action(["x", "y"], strfunction=foo)
        assert isinstance(a4, SCons.Action.ListAction), a4
        assert a4.varlist == (), a4.varlist
        assert isinstance(a4.list[0], SCons.Action.CommandAction), a4.list[0]
        assert a4.list[0].cmd_list == "x", a4.list[0].cmd_list
        assert a4.list[0].strfunction == foo, a4.list[0].strfunction
        assert isinstance(a4.list[1], SCons.Action.CommandAction), a4.list[1]
        assert a4.list[1].cmd_list == "y", a4.list[1].cmd_list
        assert a4.list[1].strfunction == foo, a4.list[1].strfunction

        a5 = SCons.Action.Action("x\ny", strfunction=foo)
        assert isinstance(a5, SCons.Action.ListAction), a5
        assert a5.varlist == (), a5.varlist
        assert isinstance(a5.list[0], SCons.Action.CommandAction), a5.list[0]
        assert a5.list[0].cmd_list == "x", a5.list[0].cmd_list
        assert a5.list[0].strfunction == foo, a5.list[0].strfunction
        assert isinstance(a5.list[1], SCons.Action.CommandAction), a5.list[1]
        assert a5.list[1].cmd_list == "y", a5.list[1].cmd_list
        assert a5.list[1].strfunction == foo, a5.list[1].strfunction

    def test_CommandGeneratorAction(self):
        """Test the Action factory's creation of CommandGeneratorAction objects."""

        def foo(): pass

        def gen_action(a, foo=foo):
            assert isinstance(a, SCons.Action.CommandGeneratorAction), a
            assert a.generator is foo, a.generator

        test_positional_args(gen_action, foo, generator=1)

    def test_LazyCmdGeneratorAction(self):
        """Test the Action factory's creation of lazy CommandGeneratorAction objects."""

        def lazy_action(a):
            assert isinstance(a, SCons.Action.LazyAction), a
            assert a.var == "FOO", a.var
            assert a.cmd_list == "${FOO}", a.cmd_list

        test_positional_args(lazy_action, "$FOO")
        test_positional_args(lazy_action, "${FOO}")

    def test_no_action(self):
        """Test when the Action() factory can't create an action object."""

        try:
            a5 = SCons.Action.Action(1)
        except TypeError:
            pass
        else:
            assert 0, "Should have thrown a TypeError creating Action from an int."

    def test_reentrance(self):
        """Test the Action factory when the action is already an Action object."""

        a1 = SCons.Action.Action("foo")
        a2 = SCons.Action.Action(a1)
        assert a2 is a1, a2


class _ActionActionTestCase(unittest.TestCase):

    def test__init__(self):
        """Test creation of _ActionAction objects."""

        def func1():
            pass

        def func2():
            pass

        def func3():
            pass

        a = SCons.Action._ActionAction()
        assert not hasattr(a, 'strfunction')
        assert a.cmdstr is _null, a.cmdstr
        assert a.varlist == (), a.varlist
        assert a.presub is _null, a.presub
        assert a.chdir is None, a.chdir
        assert a.exitstatfunc is SCons.Action.default_exitstatfunc, a.exitstatfunc

        assert SCons.Action._ActionAction(kwarg=1)
        assert not hasattr(a, 'kwarg')
        assert not hasattr(a, 'strfunction')
        assert a.cmdstr is _null, a.cmdstr
        assert a.varlist == (), a.varlist
        assert a.presub is _null, a.presub
        assert a.chdir is None, a.chdir
        assert a.exitstatfunc is SCons.Action.default_exitstatfunc, a.exitstatfunc

        a = SCons.Action._ActionAction(strfunction=func1)
        assert a.strfunction is func1, a.strfunction

        a = SCons.Action._ActionAction(strfunction=None)
        assert not hasattr(a, 'strfunction')
        assert a.cmdstr is None, a.cmdstr

        a = SCons.Action._ActionAction(cmdstr='cmdstr')
        assert not hasattr(a, 'strfunction')
        assert a.cmdstr == 'cmdstr', a.cmdstr

        a = SCons.Action._ActionAction(cmdstr=None)
        assert not hasattr(a, 'strfunction')
        assert a.cmdstr is None, a.cmdstr

        t = ('a', 'b', 'c')
        a = SCons.Action._ActionAction(varlist=t)
        assert a.varlist == t, a.varlist

        a = SCons.Action._ActionAction(presub=func1)
        assert a.presub is func1, a.presub

        a = SCons.Action._ActionAction(chdir=1)
        assert a.chdir == 1, a.chdir

        a = SCons.Action._ActionAction(exitstatfunc=func1)
        assert a.exitstatfunc is func1, a.exitstatfunc

        a = SCons.Action._ActionAction(
            # alphabetical order ...
            chdir='x',
            cmdstr='cmdstr',
            exitstatfunc=func3,
            presub=func2,
            strfunction=func1,
            varlist=t,
        )
        assert a.chdir == 'x', a.chdir
        assert a.cmdstr == 'cmdstr', a.cmdstr
        assert a.exitstatfunc is func3, a.exitstatfunc
        assert a.presub is func2, a.presub
        assert a.strfunction is func1, a.strfunction
        assert a.varlist is t, a.varlist

    def test_dup_keywords(self):
        """Test handling of both cmdstr and strfunction arguments."""

        def func():
            pass

        try:
            a = SCons.Action.Action('foo', cmdstr='string', strfunction=func)
        except SCons.Errors.UserError as e:
            s = str(e)
            m = 'Cannot have both strfunction and cmdstr args to Action()'
            assert s.find(m) != -1, 'Unexpected string:  %s' % s
        else:
            raise Exception("did not catch expected UserError")

    def test___cmp__(self):
        """Test Action comparison."""

        a1 = SCons.Action.Action("x")
        a2 = SCons.Action.Action("x")
        assert a1 == a2
        a3 = SCons.Action.Action("y")
        assert a1 != a3
        assert a2 != a3

    def test_print_cmd_lines(self):
        """Test the print_cmd_lines() method."""

        save_stdout = sys.stdout

        try:
            def execfunc(target, source, env):
                pass

            a = SCons.Action.Action(execfunc)

            sio = io.StringIO()
            sys.stdout = sio
            a.print_cmd_line("foo bar", None, None, None)
            s = sio.getvalue()
            assert s == "foo bar\n", s

        finally:
            sys.stdout = save_stdout

    def test___call__(self):
        """Test calling an Action."""

        save_stdout = sys.stdout

        save_print_actions = SCons.Action.print_actions
        save_print_actions_presub = SCons.Action.print_actions_presub
        save_execute_actions = SCons.Action.execute_actions
        # SCons.Action.print_actions = 0

        test = TestCmd.TestCmd(workdir='')
        test.subdir('sub', 'xyz')
        os.chdir(test.workpath())

        try:
            env = Environment()

            def execfunc(target, source, env):
                assert isinstance(target, list), type(target)
                assert isinstance(source, list), type(source)
                return 7

            a = SCons.Action.Action(execfunc)

            def firstfunc(target, source, env):
                assert isinstance(target, list), type(target)
                assert isinstance(source, list), type(source)
                return 0

            def lastfunc(target, source, env):
                assert isinstance(target, list), type(target)
                assert isinstance(source, list), type(source)
                return 9

            b = SCons.Action.Action([firstfunc, execfunc, lastfunc])

            sio = io.StringIO()
            sys.stdout = sio
            result = a("out", "in", env)
            assert result.status == 7, result
            s = sio.getvalue()
            assert s == "execfunc(['out'], ['in'])\n", s

            a.chdir = 'xyz'
            expect = "os.chdir(%s)\nexecfunc(['out'], ['in'])\nos.chdir(%s)\n"

            sio = io.StringIO()
            sys.stdout = sio
            result = a("out", "in", env)
            assert result.status == 7, result.status
            s = sio.getvalue()
            assert s == expect % (repr('xyz'), repr(test.workpath())), s

            sio = io.StringIO()
            sys.stdout = sio
            result = a("out", "in", env, chdir='sub')
            assert result.status == 7, result.status
            s = sio.getvalue()
            assert s == expect % (repr('sub'), repr(test.workpath())), s

            a.chdir = None

            sio = io.StringIO()
            sys.stdout = sio
            result = b("out", "in", env)
            assert result.status == 7, result.status
            s = sio.getvalue()
            assert s == "firstfunc(['out'], ['in'])\nexecfunc(['out'], ['in'])\n", s

            SCons.Action.execute_actions = 0

            sio = io.StringIO()
            sys.stdout = sio
            result = a("out", "in", env)
            assert result == 0, result
            s = sio.getvalue()
            assert s == "execfunc(['out'], ['in'])\n", s

            sio = io.StringIO()
            sys.stdout = sio
            result = b("out", "in", env)
            assert result == 0, result
            s = sio.getvalue()
            assert s == "firstfunc(['out'], ['in'])\nexecfunc(['out'], ['in'])\nlastfunc(['out'], ['in'])\n", s

            SCons.Action.print_actions_presub = 1
            SCons.Action.execute_actions = 1

            sio = io.StringIO()
            sys.stdout = sio
            result = a("out", "in", env)
            assert result.status == 7, result.status
            s = sio.getvalue()
            assert s == "Building out with action:\n  execfunc(target, source, env)\nexecfunc(['out'], ['in'])\n", s

            sio = io.StringIO()
            sys.stdout = sio
            result = a("out", "in", env, presub=0)
            assert result.status == 7, result.status
            s = sio.getvalue()
            assert s == "execfunc(['out'], ['in'])\n", s

            sio = io.StringIO()
            sys.stdout = sio
            result = a("out", "in", env, presub=1)
            assert result.status == 7, result.status
            s = sio.getvalue()
            assert s == "Building out with action:\n  execfunc(target, source, env)\nexecfunc(['out'], ['in'])\n", s

            sio = io.StringIO()
            sys.stdout = sio
            result = b(["out"], "in", env, presub=1)
            assert result.status == 7, result.status
            s = sio.getvalue()
            assert s == "Building out with action:\n  firstfunc(target, source, env)\nfirstfunc(['out'], ['in'])\nBuilding out with action:\n  execfunc(target, source, env)\nexecfunc(['out'], ['in'])\n", s

            sio = io.StringIO()
            sys.stdout = sio
            result = b(["out", "list"], "in", env, presub=1)
            assert result.status == 7, result.status
            s = sio.getvalue()
            assert s == "Building out and list with action:\n  firstfunc(target, source, env)\nfirstfunc(['out', 'list'], ['in'])\nBuilding out and list with action:\n  execfunc(target, source, env)\nexecfunc(['out', 'list'], ['in'])\n", s

            a2 = SCons.Action.Action(execfunc)

            sio = io.StringIO()
            sys.stdout = sio
            result = a2("out", "in", env)
            assert result.status == 7, result.status
            s = sio.getvalue()
            assert s == "Building out with action:\n  execfunc(target, source, env)\nexecfunc(['out'], ['in'])\n", s

            sio = io.StringIO()
            sys.stdout = sio
            result = a2("out", "in", env, presub=0)
            assert result.status == 7, result.status
            s = sio.getvalue()
            assert s == "execfunc(['out'], ['in'])\n", s

            SCons.Action.execute_actions = 0

            sio = io.StringIO()
            sys.stdout = sio
            result = a2("out", "in", env, presub=0)
            assert result == 0, result
            s = sio.getvalue()
            assert s == "execfunc(['out'], ['in'])\n", s

            sio = io.StringIO()
            sys.stdout = sio
            result = a("out", "in", env, presub=0, execute=1, show=0)
            assert result.status == 7, result.status
            s = sio.getvalue()
            assert s == '', s

            sys.stdout = save_stdout
            exitstatfunc_result = []

            def exitstatfunc(stat, result=exitstatfunc_result):
                result.append(stat)
                return stat

            result = a("out", "in", env, exitstatfunc=exitstatfunc)
            assert result == 0, result
            assert exitstatfunc_result == [], exitstatfunc_result

            result = a("out", "in", env, execute=1, exitstatfunc=exitstatfunc)
            assert result.status == 7, result.status
            assert exitstatfunc_result == [7], exitstatfunc_result

            SCons.Action.execute_actions = 1

            result = []

            def my_print_cmd_line(s, target, source, env, result=result):
                result.append(s)

            env['PRINT_CMD_LINE_FUNC'] = my_print_cmd_line
            a("output", "input", env)
            assert result == ["execfunc(['output'], ['input'])"], result


        finally:
            sys.stdout = save_stdout
            SCons.Action.print_actions = save_print_actions
            SCons.Action.print_actions_presub = save_print_actions_presub
            SCons.Action.execute_actions = save_execute_actions

    def test_presub_lines(self):
        """Test the presub_lines() method."""

        env = Environment()
        a = SCons.Action.Action("x")
        s = a.presub_lines(env)
        assert s == ['x'], s

        a = SCons.Action.Action(["y", "z"])
        s = a.presub_lines(env)
        assert s == ['y', 'z'], s

        def func():
            pass

        a = SCons.Action.Action(func)
        s = a.presub_lines(env)
        assert s == ["func(target, source, env)"], s

        def gen(target, source, env, for_signature):
            return 'generat' + env.get('GEN', 'or')

        a = SCons.Action.Action(gen, generator=1)
        s = a.presub_lines(env)
        assert s == ["generator"], s
        s = a.presub_lines(Environment(GEN='ed'))
        assert s == ["generated"], s

        a = SCons.Action.Action("$ACT")
        s = a.presub_lines(env)
        assert s == [''], s
        s = a.presub_lines(Environment(ACT='expanded action'))
        assert s == ['expanded action'], s

    def test_add(self):
        """Test adding Actions to stuff."""

        # Adding actions to other Actions or to stuff that can
        # be converted into an Action should produce a ListAction
        # containing all the Actions.
        def bar():
            return None

        baz = SCons.Action.Action(bar, generator=1)
        act1 = SCons.Action.Action('foo bar')
        act2 = SCons.Action.Action(['foo', bar])

        sum = act1 + act2
        assert isinstance(sum, SCons.Action.ListAction), str(sum)
        assert len(sum.list) == 3, len(sum.list)
        assert [isinstance(x, SCons.Action.ActionBase) for x in sum.list] == [1, 1, 1]

        sum = act1 + act1
        assert isinstance(sum, SCons.Action.ListAction), str(sum)
        assert len(sum.list) == 2, len(sum.list)

        sum = act2 + act2
        assert isinstance(sum, SCons.Action.ListAction), str(sum)
        assert len(sum.list) == 4, len(sum.list)

        # Should also be able to add command generators to each other
        # or to actions
        sum = baz + baz
        assert isinstance(sum, SCons.Action.ListAction), str(sum)
        assert len(sum.list) == 2, len(sum.list)

        sum = baz + act1
        assert isinstance(sum, SCons.Action.ListAction), str(sum)
        assert len(sum.list) == 2, len(sum.list)

        sum = act2 + baz
        assert isinstance(sum, SCons.Action.ListAction), str(sum)
        assert len(sum.list) == 3, len(sum.list)

        # Also should be able to add Actions to anything that can
        # be converted into an action.
        sum = act1 + bar
        assert isinstance(sum, SCons.Action.ListAction), str(sum)
        assert len(sum.list) == 2, len(sum.list)
        assert isinstance(sum.list[1], SCons.Action.FunctionAction)

        sum = 'foo bar' + act2
        assert isinstance(sum, SCons.Action.ListAction), str(sum)
        assert len(sum.list) == 3, len(sum.list)
        assert isinstance(sum.list[0], SCons.Action.CommandAction)

        sum = ['foo', 'bar'] + act1
        assert isinstance(sum, SCons.Action.ListAction), str(sum)
        assert len(sum.list) == 3, sum.list
        assert isinstance(sum.list[0], SCons.Action.CommandAction)
        assert isinstance(sum.list[1], SCons.Action.CommandAction)

        sum = act2 + [baz, bar]
        assert isinstance(sum, SCons.Action.ListAction), str(sum)
        assert len(sum.list) == 4, len(sum.list)
        assert isinstance(sum.list[2], SCons.Action.CommandGeneratorAction)
        assert isinstance(sum.list[3], SCons.Action.FunctionAction)

        # OK to add None on either side (should be ignored)
        sum = act1 + None
        assert sum == act1

        sum = None + act1
        assert sum == act1

        try:
            sum = act2 + 1
        except TypeError:
            pass
        else:
            assert 0, "Should have thrown a TypeError adding to an int."

        try:
            sum = 1 + act2
        except TypeError:
            pass
        else:
            assert 0, "Should have thrown a TypeError adding to an int."


class CommandActionTestCase(unittest.TestCase):

    def test___init__(self):
        """Test creation of a command Action."""

        a = SCons.Action.CommandAction(["xyzzy"])
        assert a.cmd_list == ["xyzzy"], a.cmd_list
        assert a.cmdstr is _null, a.cmdstr

        a = SCons.Action.CommandAction(["abra"], cmdstr="cadabra")
        assert a.cmd_list == ["abra"], a.cmd_list
        assert a.cmdstr == "cadabra", a.cmdstr

    def test___str__(self):
        """Test fetching the pre-substitution string for command Actions."""

        env = Environment()
        act = SCons.Action.CommandAction('xyzzy $TARGET $SOURCE')
        s = str(act)
        assert s == 'xyzzy $TARGET $SOURCE', s

        act = SCons.Action.CommandAction(['xyzzy',
                                          '$TARGET', '$SOURCE',
                                          '$TARGETS', '$SOURCES'])
        s = str(act)
        assert s == "xyzzy $TARGET $SOURCE $TARGETS $SOURCES", s

    def test_genstring(self):
        """Test the genstring() method for command Actions."""

        env = Environment()
        t1 = DummyNode('t1')
        t2 = DummyNode('t2')
        s1 = DummyNode('s1')
        s2 = DummyNode('s2')
        act = SCons.Action.CommandAction('xyzzy $TARGET $SOURCE')
        expect = 'xyzzy $TARGET $SOURCE'
        s = act.genstring([], [], env)
        assert s == expect, s
        s = act.genstring([t1], [s1], env)
        assert s == expect, s
        s = act.genstring([t1, t2], [s1, s2], env)
        assert s == expect, s

        act = SCons.Action.CommandAction('xyzzy $TARGETS $SOURCES')
        expect = 'xyzzy $TARGETS $SOURCES'
        s = act.genstring([], [], env)
        assert s == expect, s
        s = act.genstring([t1], [s1], env)
        assert s == expect, s
        s = act.genstring([t1, t2], [s1, s2], env)
        assert s == expect, s

        act = SCons.Action.CommandAction(['xyzzy',
                                          '$TARGET', '$SOURCE',
                                          '$TARGETS', '$SOURCES'])
        expect = "xyzzy $TARGET $SOURCE $TARGETS $SOURCES"
        s = act.genstring([], [], env)
        assert s == expect, s
        s = act.genstring([t1], [s1], env)
        assert s == expect, s
        s = act.genstring([t1, t2], [s1, s2], env)
        assert s == expect, s

    def test_strfunction(self):
        """Test fetching the string representation of command Actions."""

        env = Environment()
        t1 = DummyNode('t1')
        t2 = DummyNode('t2')
        s1 = DummyNode('s1')
        s2 = DummyNode('s2')
        act = SCons.Action.CommandAction('xyzzy $TARGET $SOURCE')
        s = act.strfunction([], [], env)
        assert s == 'xyzzy', s
        s = act.strfunction([t1], [s1], env)
        assert s == 'xyzzy t1 s1', s
        s = act.strfunction([t1, t2], [s1, s2], env)
        assert s == 'xyzzy t1 s1', s

        act = SCons.Action.CommandAction('xyzzy $TARGET $SOURCE',
                                         cmdstr='cmdstr - $SOURCE - $TARGET -')
        s = act.strfunction([], [], env)
        assert s == 'cmdstr -  -  -', s
        s = act.strfunction([t1], [s1], env)
        assert s == 'cmdstr - s1 - t1 -', s
        s = act.strfunction([t1, t2], [s1, s2], env)
        assert s == 'cmdstr - s1 - t1 -', s

        act = SCons.Action.CommandAction('xyzzy $TARGETS $SOURCES')
        s = act.strfunction([], [], env)
        assert s == 'xyzzy', s
        s = act.strfunction([t1], [s1], env)
        assert s == 'xyzzy t1 s1', s
        s = act.strfunction([t1, t2], [s1, s2], env)
        assert s == 'xyzzy t1 t2 s1 s2', s

        act = SCons.Action.CommandAction('xyzzy $TARGETS $SOURCES',
                                         cmdstr='cmdstr = $SOURCES = $TARGETS =')
        s = act.strfunction([], [], env)
        assert s == 'cmdstr =  =  =', s
        s = act.strfunction([t1], [s1], env)
        assert s == 'cmdstr = s1 = t1 =', s
        s = act.strfunction([t1, t2], [s1, s2], env)
        assert s == 'cmdstr = s1 s2 = t1 t2 =', s

        act = SCons.Action.CommandAction(['xyzzy',
                                          '$TARGET', '$SOURCE',
                                          '$TARGETS', '$SOURCES'])
        s = act.strfunction([], [], env)
        assert s == 'xyzzy', s
        s = act.strfunction([t1], [s1], env)
        assert s == 'xyzzy t1 s1 t1 s1', s
        s = act.strfunction([t1, t2], [s1, s2], env)
        assert s == 'xyzzy t1 s1 t1 t2 s1 s2', s

        act = SCons.Action.CommandAction('xyzzy $TARGETS $SOURCES',
                                         cmdstr='cmdstr\t$TARGETS\n$SOURCES   ')

        s = act.strfunction([], [], env)
        assert s == 'cmdstr\t\n   ', s
        s = act.strfunction([t1], [s1], env)
        assert s == 'cmdstr\tt1\ns1   ', s
        s = act.strfunction([t1, t2], [s1, s2], env)
        assert s == 'cmdstr\tt1 t2\ns1 s2   ', s

        def sf(target, source, env):
            return "sf was called"

        act = SCons.Action.CommandAction('foo', strfunction=sf)
        s = act.strfunction([], [], env)
        assert s == "sf was called", s

        class actclass1:
            def __init__(self, targets, sources, env):
                pass

            def __call__(self):
                return 1

        class actclass2:
            def __init__(self, targets, sources, env):
                self.strfunction = 5

            def __call__(self):
                return 2

        class actclass3:
            def __init__(self, targets, sources, env):
                pass

            def __call__(self):
                return 3

            def strfunction(self, targets, sources, env):
                return 'actclass3 on %s to get %s' % (str(sources[0]),
                                                      str(targets[0]))

        class actclass4:
            def __init__(self, targets, sources, env):
                pass

            def __call__(self):
                return 4

            strfunction = None

        act1 = SCons.Action.Action(actclass1([t1], [s1], env))
        s = act1.strfunction([t1], [s1], env)
        assert s == 'actclass1(["t1"], ["s1"])', s

        act2 = SCons.Action.Action(actclass2([t1], [s1], env))
        s = act2.strfunction([t1], [s1], env)
        assert s == 'actclass2(["t1"], ["s1"])', s

        act3 = SCons.Action.Action(actclass3([t1], [s1], env))
        s = act3.strfunction([t1], [s1], env)
        assert s == 'actclass3 on s1 to get t1', s

        act4 = SCons.Action.Action(actclass4([t1], [s1], env))
        s = act4.strfunction([t1], [s1], env)
        assert s is None, s

        act = SCons.Action.CommandAction("@foo bar")
        s = act.strfunction([], [], env)
        assert s == "", s

        act = SCons.Action.CommandAction("@-foo bar")
        s = act.strfunction([], [], env)
        assert s == "", s

        act = SCons.Action.CommandAction("-@foo bar")
        s = act.strfunction([], [], env)
        assert s == "", s

        act = SCons.Action.CommandAction("-foo bar")
        s = act.strfunction([], [], env)
        assert s == "foo bar", s

        act = SCons.Action.CommandAction("@ foo bar")
        s = act.strfunction([], [], env)
        assert s == "", s

        act = SCons.Action.CommandAction("@- foo bar")
        s = act.strfunction([], [], env)
        assert s == "", s

        act = SCons.Action.CommandAction("-@ foo bar")
        s = act.strfunction([], [], env)
        assert s == "", s

        act = SCons.Action.CommandAction("- foo bar")
        s = act.strfunction([], [], env)
        assert s == "foo bar", s

    def test_execute(self):
        """Test execution of command Actions."""

        try:
            env = self.env
        except AttributeError:
            env = Environment()

        cmd1 = r'%s %s %s xyzzy' % (_python_, act_py, outfile)

        act = SCons.Action.CommandAction(cmd1)
        r = act([], [], env.Clone())
        assert r == 0
        c = test.read(outfile, 'r')
        assert c == "act.py: 'xyzzy'\n", c

        cmd2 = r'%s %s %s $TARGET' % (_python_, act_py, outfile)

        act = SCons.Action.CommandAction(cmd2)
        r = act(DummyNode('foo'), [], env.Clone())
        assert r == 0
        c = test.read(outfile, 'r')
        assert c == "act.py: 'foo'\n", c

        cmd3 = r'%s %s %s ${TARGETS}' % (_python_, act_py, outfile)

        act = SCons.Action.CommandAction(cmd3)
        r = act(list(map(DummyNode, ['aaa', 'bbb'])), [], env.Clone())
        assert r == 0
        c = test.read(outfile, 'r')
        assert c == "act.py: 'aaa' 'bbb'\n", c

        cmd4 = r'%s %s %s $SOURCES' % (_python_, act_py, outfile)

        act = SCons.Action.CommandAction(cmd4)
        r = act([], [DummyNode('one'), DummyNode('two')], env.Clone())
        assert r == 0
        c = test.read(outfile, 'r')
        assert c == "act.py: 'one' 'two'\n", c

        cmd4 = r'%s %s %s ${SOURCES[:2]}' % (_python_, act_py, outfile)

        act = SCons.Action.CommandAction(cmd4)
        sources = [DummyNode('three'), DummyNode('four'), DummyNode('five')]
        env2 = env.Clone()
        r = act([], source=sources, env=env2)
        assert r == 0
        c = test.read(outfile, 'r')
        assert c == "act.py: 'three' 'four'\n", c

        cmd5 = r'%s %s %s $TARGET XYZZY' % (_python_, act_py, outfile)

        act = SCons.Action.CommandAction(cmd5)
        env5 = Environment()
        if 'ENV' in scons_env:
            env5['ENV'] = scons_env['ENV']
            PATH = scons_env['ENV'].get('PATH', '')
        else:
            env5['ENV'] = {}
            PATH = ''

        env5['ENV']['XYZZY'] = 'xyzzy'
        r = act(target=DummyNode('out5'), source=[], env=env5)

        act = SCons.Action.CommandAction(cmd5)
        r = act(target=DummyNode('out5'),
                source=[],
                env=env.Clone(ENV={'XYZZY': 'xyzzy5',
                                   'PATH': PATH}))
        assert r == 0
        c = test.read(outfile, 'r')
        assert c == "act.py: 'out5' 'XYZZY'\nact.py: 'xyzzy5'\n", c

        class Obj:
            def __init__(self, str):
                self._str = str

            def __str__(self):
                return self._str

            def rfile(self):
                return self

            def get_subst_proxy(self):
                return self

        cmd6 = r'%s %s %s ${TARGETS[1]} $TARGET ${SOURCES[:2]}' % (_python_, act_py, outfile)

        act = SCons.Action.CommandAction(cmd6)
        r = act(target=[Obj('111'), Obj('222')],
                source=[Obj('333'), Obj('444'), Obj('555')],
                env=env.Clone())
        assert r == 0
        c = test.read(outfile, 'r')
        assert c == "act.py: '222' '111' '333' '444'\n", c

        if os.name == 'nt':
            # NT treats execs of directories and non-executable files
            # as "file not found" errors
            expect_nonexistent = 1
            expect_nonexecutable_file = 1
            expect_nonexecutable_dir = 1
        elif sys.platform == 'cygwin':
            expect_nonexistent = 127
            # Newer cygwin seems to return 126 for following
            expect_nonexecutable_file = 126
            expect_nonexecutable_dir = 127
        else:
            expect_nonexistent = 127
            expect_nonexecutable_file = 126
            expect_nonexecutable_dir = 126

        # Test that a nonexistent command returns 127
        act = SCons.Action.CommandAction(python + "_no_such_command_")
        r = act([], [], env.Clone(out=outfile))
        assert r.status == expect_nonexistent, r.status

        # Test that trying to execute a directory returns 126
        dir, tail = os.path.split(python)
        act = SCons.Action.CommandAction(dir)
        r = act([], [], env.Clone(out=outfile))
        assert r.status == expect_nonexecutable_file, r.status

        # Test that trying to execute a non-executable file returns 126
        act = SCons.Action.CommandAction(outfile)
        r = act([], [], env.Clone(out=outfile))
        assert r.status == expect_nonexecutable_dir, r.status

        act = SCons.Action.CommandAction('%s %s 1' % (_python_, exit_py))
        r = act([], [], env)
        assert r.status == 1, r.status

        act = SCons.Action.CommandAction('@%s %s 1' % (_python_, exit_py))
        r = act([], [], env)
        assert r.status == 1, r.status

        act = SCons.Action.CommandAction('@-%s %s 1' % (_python_, exit_py))
        r = act([], [], env)
        assert r == 0, r

        act = SCons.Action.CommandAction('-%s %s 1' % (_python_, exit_py))
        r = act([], [], env)
        assert r == 0, r

        act = SCons.Action.CommandAction('@ %s %s 1' % (_python_, exit_py))
        r = act([], [], env)
        assert r.status == 1, r.status

        act = SCons.Action.CommandAction('@- %s %s 1' % (_python_, exit_py))
        r = act([], [], env)
        assert r == 0, r

        act = SCons.Action.CommandAction('- %s %s 1' % (_python_, exit_py))
        r = act([], [], env)
        assert r == 0, r

    def test_set_handler(self):
        """Test setting the command handler..."""

        class Test:
            def __init__(self):
                self.executed = 0

        t = Test()

        def func(sh, escape, cmd, args, env, test=t):
            test.executed = args
            test.shell = sh
            return 0

        def escape_func(cmd):
            return '**' + cmd + '**'

        class LiteralStr:
            def __init__(self, x):
                self.data = x

            def __str__(self):
                return self.data

            def escape(self, escape_func):
                return escape_func(self.data)

            def is_literal(self):
                return 1

        a = SCons.Action.CommandAction(["xyzzy"])
        e = Environment(SPAWN=func)
        a([], [], e)
        assert t.executed == ['xyzzy'], t.executed

        a = SCons.Action.CommandAction(["xyzzy"])
        e = Environment(SPAWN='$FUNC', FUNC=func)
        a([], [], e)
        assert t.executed == ['xyzzy'], t.executed

        a = SCons.Action.CommandAction(["xyzzy"])
        e = Environment(SPAWN=func, SHELL='fake shell')
        a([], [], e)
        assert t.executed == ['xyzzy'], t.executed
        assert t.shell == 'fake shell', t.shell

        a = SCons.Action.CommandAction([LiteralStr("xyzzy")])
        e = Environment(SPAWN=func, ESCAPE=escape_func)
        a([], [], e)
        assert t.executed == ['**xyzzy**'], t.executed

    def test_get_contents(self):
        """Test fetching the contents of a command Action."""

        def CmdGen(target, source, env, for_signature):
            assert for_signature
            return "%s %s" % \
                   (env["foo"], env["bar"])

        # The number 1 is there to make sure all args get converted to strings.
        a = SCons.Action.CommandAction(["|", "$(", "$foo", "|", "$(", "$bar",
                                        "$)", "stuff", "$)", "|", "$baz", 1])
        c = a.get_contents(target=[], source=[],
                           env=Environment(foo='FFF', bar='BBB',
                                           baz=CmdGen))
        assert c == b"| | FFF BBB 1", c

        # Make sure that CommandActions use an Environment's
        # subst_target_source() method for substitution.
        class SpecialEnvironment(Environment):
            def subst_target_source(self, strSubst, raw=0, target=[], source=[]):
                return 'subst_target_source: ' + strSubst

        c = a.get_contents(target=DummyNode('ttt'), source=DummyNode('sss'),
                           env=SpecialEnvironment(foo='GGG', bar='CCC',
                                                  baz='ZZZ'))
        assert c == b'subst_target_source: | $( $foo | $( $bar $) stuff $) | $baz 1', c

        # We've discussed using the real target and source names in a
        # CommandAction's signature contents.  This would have have the
        # advantage of recompiling when a file's name changes (keeping
        # debug info current), but it would currently break repository
        # logic that will change the file name based on whether the
        # files come from a repository or locally.  If we ever move to
        # that scheme, then all of the '__t1__' and '__s6__' file names
        # in the asserts below would change to 't1' and 's6' and the
        # like.
        t = list(map(DummyNode, ['t1', 't2', 't3', 't4', 't5', 't6']))
        s = list(map(DummyNode, ['s1', 's2', 's3', 's4', 's5', 's6']))
        env = Environment()

        a = SCons.Action.CommandAction(["$TARGET"])
        c = a.get_contents(target=t, source=s, env=env)
        assert c == b"t1", c

        a = SCons.Action.CommandAction(["$TARGETS"])
        c = a.get_contents(target=t, source=s, env=env)
        assert c == b"t1 t2 t3 t4 t5 t6", c

        a = SCons.Action.CommandAction(["${TARGETS[2]}"])
        c = a.get_contents(target=t, source=s, env=env)
        assert c == b"t3", c

        a = SCons.Action.CommandAction(["${TARGETS[3:5]}"])
        c = a.get_contents(target=t, source=s, env=env)
        assert c == b"t4 t5", c

        a = SCons.Action.CommandAction(["$SOURCE"])
        c = a.get_contents(target=t, source=s, env=env)
        assert c == b"s1", c

        a = SCons.Action.CommandAction(["$SOURCES"])
        c = a.get_contents(target=t, source=s, env=env)
        assert c == b"s1 s2 s3 s4 s5 s6", c

        a = SCons.Action.CommandAction(["${SOURCES[2]}"])
        c = a.get_contents(target=t, source=s, env=env)
        assert c == b"s3", c

        a = SCons.Action.CommandAction(["${SOURCES[3:5]}"])
        c = a.get_contents(target=t, source=s, env=env)
        assert c == b"s4 s5", c

    def test_get_implicit_deps(self):
        """Test getting the implicit dependencies of a command Action."""

        class SpecialEnvironment(Environment):
            def WhereIs(self, prog):
                return prog
            
            class fs:
                def File(name):
                    return name

        env = SpecialEnvironment()
        a = SCons.Action.CommandAction([_python_, exit_py])
        self.assertEqual(a.get_implicit_deps(target=[], source=[], env=env), [_python_])

        for false_like_value in [False, 0, None, "None", "False", "0", "NONE", "FALSE", "off", "OFF", "NO", "no"]:
            env = SpecialEnvironment(IMPLICIT_COMMAND_DEPENDENCIES=false_like_value)
            a = SCons.Action.CommandAction([_python_, exit_py])
            self.assertEqual(a.get_implicit_deps(target=[], source=[], env=env), [])

            env = SpecialEnvironment(IMPLICIT_COMMAND_DEPENDENCIES="$SUBST_VALUE", SUBST_VALUE=false_like_value)
            a = SCons.Action.CommandAction([_python_, exit_py])
            self.assertEqual(a.get_implicit_deps(target=[], source=[], env=env), [])

        for true_like_value in [True, 1, "True", "TRUE", "1"]:
            env = SpecialEnvironment(IMPLICIT_COMMAND_DEPENDENCIES=true_like_value)
            a = SCons.Action.CommandAction([_python_, exit_py])
            self.assertEqual(a.get_implicit_deps(target=[], source=[], env=env), [_python_])

            env = SpecialEnvironment(IMPLICIT_COMMAND_DEPENDENCIES="$SUBST_VALUE", SUBST_VALUE=true_like_value)
            a = SCons.Action.CommandAction([_python_, exit_py])
            self.assertEqual(a.get_implicit_deps(target=[], source=[], env=env), [_python_])

        for all_like_Value in ["all", "ALL", 2, "2"]:
            env = SpecialEnvironment(IMPLICIT_COMMAND_DEPENDENCIES=all_like_Value)
            a = SCons.Action.CommandAction([_python_, exit_py])
            self.assertEqual(a.get_implicit_deps(target=[], source=[], env=env), [_python_, exit_py])


class CommandGeneratorActionTestCase(unittest.TestCase):

    def factory(self, act, **kw):
        """Pass any keywords as a dict"""

        return SCons.Action.CommandGeneratorAction(act, kw)

    def test___init__(self):
        """Test creation of a command generator Action."""

        def f(target, source, env):
            pass

        a = self.factory(f)
        assert a.generator == f

    def test___str__(self):
        """Test the pre-substitution strings for command generator Actions."""

        def f(target, source, env, for_signature, self=self):
            # See if "env" is really a construction environment (or
            # looks like one) by accessing the FindIxes attribute.
            # (The Tool/mingw.py module has a generator that uses this,
            # and the __str__() method used to cause problems by passing
            # us a regular dictionary as a fallback.)

            env.FindIxes
            return "FOO"

        a = self.factory(f)
        s = str(a)
        assert s == 'FOO', s

    def test_genstring(self):
        """Test the command generator Action genstring() method."""

        def f(target, source, env, for_signature, self=self):
            dummy = env['dummy']
            self.dummy = dummy
            return "$FOO $TARGET $SOURCE $TARGETS $SOURCES"

        a = self.factory(f)
        self.dummy = 0
        s = a.genstring([], [], env=Environment(FOO='xyzzy', dummy=1))
        assert self.dummy == 1, self.dummy
        assert s == "$FOO $TARGET $SOURCE $TARGETS $SOURCES", s

    def test_execute(self):
        """Test executing a command generator Action."""

        def f(target, source, env, for_signature, self=self):
            dummy = env['dummy']
            self.dummy = dummy
            s = env.subst("$FOO")
            assert s == 'foo baz\nbar ack', s
            return "$FOO"

        def func_action(target, source, env, self=self):
            dummy = env['dummy']
            s = env.subst('$foo')
            assert s == 'bar', s
            self.dummy = dummy

        def f2(target, source, env, for_signature, f=func_action):
            return f

        def ch(sh, escape, cmd, args, env, self=self):
            self.cmd.append(cmd)
            self.args.append(args)

        a = self.factory(f)
        self.dummy = 0
        self.cmd = []
        self.args = []
        a([], [], env=Environment(FOO='foo baz\nbar ack',
                                  dummy=1,
                                  SPAWN=ch))
        assert self.dummy == 1, self.dummy
        assert self.cmd == ['foo', 'bar'], self.cmd
        assert self.args == [['foo', 'baz'], ['bar', 'ack']], self.args

        b = self.factory(f2)
        self.dummy = 0
        b(target=[], source=[], env=Environment(foo='bar',
                                                dummy=2))
        assert self.dummy == 2, self.dummy
        del self.dummy

        class DummyFile:
            def __init__(self, t):
                self.t = t

            def rfile(self):
                self.t.rfile_called = 1
                return self

            def get_subst_proxy(self):
                return self

        def f3(target, source, env, for_signature):
            return ''

        c = self.factory(f3)
        c(target=[], source=DummyFile(self), env=Environment())
        assert self.rfile_called

    def test_get_contents(self):
        """Test fetching the contents of a command generator Action."""

        def f(target, source, env, for_signature):
            foo = env['foo']
            bar = env['bar']
            assert for_signature, for_signature
            return [["guux", foo, "$(", "$ignore", "$)", bar,
                     '${test("$( foo $bar $)")}']]

        def test(mystr):
            assert mystr == "$( foo $bar $)", mystr
            return "test"

        env = Environment(foo='FFF', bar='BBB',
                          ignore='foo', test=test)
        a = self.factory(f)
        c = a.get_contents(target=[], source=[], env=env)
        assert c == b"guux FFF BBB test", c

    def test_get_contents_of_function_action(self):
        """Test contents of a CommandGeneratorAction-generated FunctionAction."""

        def LocalFunc():
            pass

        # Since the python bytecode has per version differences, we need different expected results per version
        func_matches = {
            (3, 5): bytearray(b'0, 0, 0, 0,(),(),(d\x00\x00S),(),()'),
            (3, 6): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 7): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 8): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 9): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 10): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 11): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'),
        }

        meth_matches = [
            b"1, 1, 0, 0,(),(),(d\000\000S),(),()",
            b"1, 1, 0, 0,(),(),(d\x00\x00S),(),()",
        ]

        def f_global(target, source, env, for_signature):
            return SCons.Action.Action(GlobalFunc)

        def f_local(target, source, env, for_signature):
            return SCons.Action.Action(LocalFunc)

        env = Environment(XYZ='foo')

        a = self.factory(f_global)
        c = a.get_contents(target=[], source=[], env=env)
        assert c == func_matches[sys.version_info[:2]], f"Got\n{c!r}\nExpected\n" + repr(
            func_matches[sys.version_info[:2]])

        a = self.factory(f_local)
        c = a.get_contents(target=[], source=[], env=env)
        assert c == func_matches[sys.version_info[:2]], f"Got\n{c!r}\nExpected\n" + repr(
            func_matches[sys.version_info[:2]])

        def f_global2(target, source, env, for_signature):
            return SCons.Action.Action(GlobalFunc, varlist=['XYZ'])

        def f_local2(target, source, env, for_signature):
            return SCons.Action.Action(LocalFunc, varlist=['XYZ'])

        matches_foo = func_matches[sys.version_info[:2]] + b'foo'

        a = self.factory(f_global2)
        c = a.get_contents(target=[], source=[], env=env)
        assert c in matches_foo, repr(c)

        a = self.factory(f_local2)
        c = a.get_contents(target=[], source=[], env=env)
        assert c in matches_foo, repr(c)


class FunctionActionTestCase(unittest.TestCase):

    def test___init__(self):
        """Test creation of a function Action."""

        def func1():
            pass

        def func2():
            pass

        def func3():
            pass

        def func4():
            pass

        a = SCons.Action.FunctionAction(func1, {})
        assert a.execfunction == func1, a.execfunction
        assert isinstance(a.strfunction, types.MethodType), type(a.strfunction)

        a = SCons.Action.FunctionAction(func2, {'strfunction': func3})
        assert a.execfunction == func2, a.execfunction
        assert a.strfunction == func3, a.strfunction

    def test___str__(self):
        """Test the __str__() method for function Actions."""

        def func1():
            pass

        a = SCons.Action.FunctionAction(func1, {})
        s = str(a)
        assert s == "func1(target, source, env)", s

        class class1:
            def __call__(self):
                pass

        a = SCons.Action.FunctionAction(class1(), {})
        s = str(a)
        assert s == "class1(target, source, env)", s

    def test_execute(self):
        """Test executing a function Action."""

        self.inc = 0

        def f(target, source, env):
            s = env['s']
            s.inc = s.inc + 1
            s.target = target
            s.source = source
            assert env.subst("$BAR") == 'foo bar', env.subst("$BAR")
            return 0

        a = SCons.Action.FunctionAction(f, {})
        a(target=1, source=2, env=Environment(BAR='foo bar',
                                              s=self))
        assert self.inc == 1, self.inc
        assert self.source == [2], self.source
        assert self.target == [1], self.target

        global count
        count = 0

        def function1(target, source, env):
            global count
            count = count + 1
            for t in target:
                with open(t, 'w') as f:
                    f.write("function1\n")
            return 1

        act = SCons.Action.FunctionAction(function1, {})
        r = act(target=[outfile, outfile2], source=[], env=Environment())
        assert r.status == 1, r.status

        assert count == 1, count
        c = test.read(outfile, 'r')
        assert c == "function1\n", c
        c = test.read(outfile2, 'r')
        assert c == "function1\n", c

        class class1a:
            def __init__(self, target, source, env):
                with open(env['out'], 'w') as f:
                    f.write("class1a\n")

        act = SCons.Action.FunctionAction(class1a, {})
        r = act([], [], Environment(out=outfile))
        assert isinstance(r.status, class1a), r.status
        c = test.read(outfile, 'r')
        assert c == "class1a\n", c

        class class1b:
            def __call__(self, target, source, env):
                with open(env['out'], 'w') as f:
                    f.write("class1b\n")
                return 2

        act = SCons.Action.FunctionAction(class1b(), {})
        r = act([], [], Environment(out=outfile))
        assert r.status == 2, r.status
        c = test.read(outfile, 'r')
        assert c == "class1b\n", c

        def build_it(target, source, env, executor=None, self=self):
            self.build_it = 1
            return 0

        def string_it(target, source, env, executor=None, self=self):
            self.string_it = 1
            return None

        act = SCons.Action.FunctionAction(build_it,
                                          {'strfunction': string_it})
        r = act([], [], Environment())
        assert r == 0, r
        assert self.build_it
        assert self.string_it

    def test_get_contents(self):
        """Test fetching the contents of a function Action."""

        def LocalFunc():
            pass

        func_matches = {
            (3, 5): bytearray(b'0, 0, 0, 0,(),(),(d\x00\x00S),(),()'),
            (3, 6): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 7): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 8): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 9): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 10): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 11): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'),

        }

        meth_matches = {
            (3, 5): bytearray(b'1, 1, 0, 0,(),(),(d\x00\x00S),(),()'),
            (3, 6): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 7): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 8): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 9): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 10): bytearray(b'1, 1, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 11): bytearray(b'1, 1, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'),
        }

        def factory(act, **kw):
            return SCons.Action.FunctionAction(act, kw)

        a = factory(GlobalFunc)
        c = a.get_contents(target=[], source=[], env=Environment())
        assert (
            c == func_matches[sys.version_info[:2]]
        ), f"Got\n{c!r}\nExpected one of \n" + repr(func_matches[sys.version_info[:2]])

        a = factory(LocalFunc)
        c = a.get_contents(target=[], source=[], env=Environment())
        assert (
            c == func_matches[sys.version_info[:2]]
        ), f"Got\n{c!r}\nExpected one of \n" + repr(func_matches[sys.version_info[:2]])

        matches_foo = func_matches[sys.version_info[:2]] + b'foo'

        a = factory(GlobalFunc, varlist=['XYZ'])
        c = a.get_contents(target=[], source=[], env=Environment())
        assert (
            c == func_matches[sys.version_info[:2]]
        ), f"Got\n{c!r}\nExpected one of \n" + repr(func_matches[sys.version_info[:2]])
        # assert c in func_matches, repr(c)

        c = a.get_contents(target=[], source=[], env=Environment(XYZ='foo'))
        assert c == matches_foo, repr(c)

        ##TODO: is this set of tests still needed?
        # Make sure a bare string varlist works
        a = factory(GlobalFunc, varlist='XYZ')
        c = a.get_contents(target=[], source=[], env=Environment())
        # assert c in func_matches, repr(c)
        assert (
            c == func_matches[sys.version_info[:2]]
        ), f"Got\n{c!r}\nExpected one of \n" + repr(func_matches[sys.version_info[:2]])

        c = a.get_contents(target=[], source=[], env=Environment(XYZ='foo'))
        assert c in matches_foo, repr(c)

        class Foo:
            def get_contents(self, target, source, env):
                return b'xyzzy'

        a = factory(Foo())
        c = a.get_contents(target=[], source=[], env=Environment())
        assert c == b'xyzzy', repr(c)

        class LocalClass:
            def LocalMethod(self):
                pass

        lc = LocalClass()
        a = factory(lc.LocalMethod)
        c = a.get_contents(target=[], source=[], env=Environment())
        assert (
            c == meth_matches[sys.version_info[:2]]
        ), f"Got\n{c!r}\nExpected one of \n" + repr(meth_matches[sys.version_info[:2]])

    def test_strfunction(self):
        """Test the FunctionAction.strfunction() method."""

        def func():
            pass

        def factory(act, **kw):
            return SCons.Action.FunctionAction(act, kw)

        a = factory(func)
        s = a.strfunction(target=[], source=[], env=Environment())
        assert s == 'func([], [])', s

        a = factory(func, strfunction=None)
        s = a.strfunction(target=[], source=[], env=Environment())
        assert s is None, s

        a = factory(func, cmdstr='function')
        s = a.strfunction(target=[], source=[], env=Environment())
        assert s == 'function', s


class ListActionTestCase(unittest.TestCase):

    def test___init__(self):
        """Test creation of a list of subsidiary Actions."""

        def func():
            pass

        a = SCons.Action.ListAction(["x", func, ["y", "z"]])
        assert isinstance(a.list[0], SCons.Action.CommandAction)
        assert isinstance(a.list[1], SCons.Action.FunctionAction)
        assert isinstance(a.list[2], SCons.Action.ListAction)
        assert a.list[2].list[0].cmd_list == 'y'

    def test___str__(self):
        """Test the __str__() method for a list of subsidiary Actions."""

        def f(target, source, env):
            pass

        def g(target, source, env):
            pass

        a = SCons.Action.ListAction([f, g, "XXX", f])
        s = str(a)
        assert s == "f(target, source, env)\ng(target, source, env)\nXXX\nf(target, source, env)", s

    def test_genstring(self):
        """Test the genstring() method for a list of subsidiary Actions."""

        def f(target, source, env):
            pass

        def g(target, source, env, for_signature):
            return 'generated %s %s' % (target[0], source[0])

        g = SCons.Action.Action(g, generator=1)
        a = SCons.Action.ListAction([f, g, "XXX", f])
        s = a.genstring(['foo.x'], ['bar.y'], Environment())
        assert s == "f(target, source, env)\ngenerated foo.x bar.y\nXXX\nf(target, source, env)", s

    def test_execute(self):
        """Test executing a list of subsidiary Actions."""
        self.inc = 0

        def f(target, source, env):
            s = env['s']
            s.inc = s.inc + 1

        a = SCons.Action.ListAction([f, f, f])
        a([], [], Environment(s=self))
        assert self.inc == 3, self.inc

        cmd2 = r'%s %s %s syzygy' % (_python_, act_py, outfile)

        def function2(target, source, env):
            with open(env['out'], 'a') as f:
                f.write("function2\n")
            return 0

        class class2a:
            def __call__(self, target, source, env):
                with open(env['out'], 'a') as f:
                    f.write("class2a\n")
                return 0

        class class2b:
            def __init__(self, target, source, env):
                with open(env['out'], 'a') as f:
                    f.write("class2b\n")

        act = SCons.Action.ListAction([cmd2, function2, class2a(), class2b])
        r = act([], [], Environment(out=outfile))
        assert isinstance(r.status, class2b), r.status
        c = test.read(outfile, 'r')
        assert c == "act.py: 'syzygy'\nfunction2\nclass2a\nclass2b\n", c

    def test_get_contents(self):
        """Test fetching the contents of a list of subsidiary Actions."""

        self.foo = 0

        def gen(target, source, env, for_signature):
            s = env['s']
            s.foo = 1
            return "y"

        a = SCons.Action.ListAction(["x",
                                     SCons.Action.Action(gen, generator=1),
                                     "z"])
        c = a.get_contents(target=[], source=[], env=Environment(s=self))
        assert self.foo == 1, self.foo
        assert c == b"xyz", c


class LazyActionTestCase(unittest.TestCase):
    def test___init__(self):
        """Test creation of a lazy-evaluation Action."""

        # Environment variable references should create a special type
        # of LazyAction that lazily evaluates the variable for whether
        # it's a string or something else before doing anything.
        a9 = SCons.Action.Action('$FOO')
        assert isinstance(a9, SCons.Action.LazyAction), a9
        assert a9.var == 'FOO', a9.var

        a10 = SCons.Action.Action('${FOO}')
        assert isinstance(a10, SCons.Action.LazyAction), a10
        assert a10.var == 'FOO', a10.var

    def test_genstring(self):
        """Test the lazy-evaluation Action genstring() method."""

        def f(target, source, env):
            pass

        a = SCons.Action.Action('$BAR')
        env1 = Environment(BAR=f, s=self)
        env2 = Environment(BAR='xxx', s=self)
        s = a.genstring([], [], env=env1)
        assert s == "f(target, source, env)", s
        s = a.genstring([], [], env=env2)
        assert s == 'xxx', s

    def test_execute(self):
        """Test executing a lazy-evaluation Action."""

        def f(target, source, env):
            s = env['s']
            s.test = 1
            return 0

        a = SCons.Action.Action('$BAR')
        a([], [], env=Environment(BAR=f, s=self))
        assert self.test == 1, self.test
        cmd = r'%s %s %s lazy' % (_python_, act_py, outfile)
        a([], [], env=Environment(BAR=cmd, s=self))
        c = test.read(outfile, 'r')
        assert c == "act.py: 'lazy'\n", c

    def test_get_contents(self):
        """Test fetching the contents of a lazy-evaluation Action."""

        a = SCons.Action.Action("${FOO}")
        env = Environment(FOO=[["This", "is", "a", "test"]])
        c = a.get_contents(target=[], source=[], env=env)
        assert c == b"This is a test", c

    def test_get_contents_of_function_action(self):
        """Test fetching the contents of a lazy-evaluation FunctionAction."""

        def LocalFunc():
            pass

        func_matches = {
            (3, 5): bytearray(b'0, 0, 0, 0,(),(),(d\x00\x00S),(),()'),
            (3, 6): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 7): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 8): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 9): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 10): bytearray(b'0, 0, 0, 0,(),(),(d\x00S\x00),(),()'),
            (3, 11): bytearray(b'0, 0, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()'),
        }

        meth_matches = [
            b"1, 1, 0, 0,(),(),(d\000\000S),(),()",
            b"1, 1, 0, 0,(),(),(d\x00\x00S),(),()",
        ]

        def factory(act, **kw):
            return SCons.Action.FunctionAction(act, kw)

        a = SCons.Action.Action("${FOO}")

        env = Environment(FOO=factory(GlobalFunc))
        c = a.get_contents(target=[], source=[], env=env)
        # assert c in func_matches, "Got\n"+repr(c)+"\nExpected one of \n"+"\n".join([repr(f) for f in func_matches])
        assert (
            c == func_matches[sys.version_info[:2]]
        ), f"Got\n{c!r}\nExpected one of \n" + repr(func_matches[sys.version_info[:2]])

        env = Environment(FOO=factory(LocalFunc))
        c = a.get_contents(target=[], source=[], env=env)
        assert (
            c == func_matches[sys.version_info[:2]]
        ), f"Got\n{c!r}\nExpected one of \n" + repr(func_matches[sys.version_info[:2]])

        # matches_foo = [x + b"foo" for x in func_matches]
        matches_foo = func_matches[sys.version_info[:2]] + b'foo'

        env = Environment(FOO=factory(GlobalFunc, varlist=['XYZ']))
        c = a.get_contents(target=[], source=[], env=env)
        assert (
            c == func_matches[sys.version_info[:2]]
        ), f"Got\n{c!r}\nExpected one of \n" + repr(func_matches[sys.version_info[:2]])

        env['XYZ'] = 'foo'
        c = a.get_contents(target=[], source=[], env=env)
        assert c in matches_foo, repr(c)


class ActionCallerTestCase(unittest.TestCase):
    def test___init__(self):
        """Test creation of an ActionCaller"""

        ac = SCons.Action.ActionCaller(1, [2, 3], {'FOO': 4, 'BAR': 5})
        assert ac.parent == 1, ac.parent
        assert ac.args == [2, 3], ac.args
        assert ac.kw == {'FOO': 4, 'BAR': 5}, ac.kw

    def test_get_contents(self):
        """Test fetching the contents of an ActionCaller"""

        def strfunc():
            pass

        def LocalFunc():
            pass

        matches = {
            (3, 5): b'd\x00\x00S',
            (3, 6): b'd\x00S\x00',
            (3, 7): b'd\x00S\x00',
            (3, 8): b'd\x00S\x00',
            (3, 9): b'd\x00S\x00',
            (3, 10): b'd\x00S\x00',
            (3, 11): b'\x97\x00d\x00S\x00',
        }

        af = SCons.Action.ActionFactory(GlobalFunc, strfunc)
        ac = SCons.Action.ActionCaller(af, [], {})
        c = ac.get_contents([], [], Environment())
        assert c == matches[sys.version_info[:2]], (
            f"Got\n{c!r}\nExpected one of \n"
            + repr(matches[sys.version_info[:2]])
        )

        af = SCons.Action.ActionFactory(LocalFunc, strfunc)
        ac = SCons.Action.ActionCaller(af, [], {})
        c = ac.get_contents([], [], Environment())
        assert c == matches[sys.version_info[:2]], (
            f"Got\n{c!r}\nExpected one of \n"
            + repr(matches[sys.version_info[:2]])
        )

        class LocalActFunc:
            def __call__(self):
                pass

        af = SCons.Action.ActionFactory(GlobalActFunc(), strfunc)
        ac = SCons.Action.ActionCaller(af, [], {})
        c = ac.get_contents([], [], Environment())
        assert c == matches[sys.version_info[:2]], (
            f"Got\n{c!r}\nExpected one of \n"
            + repr(matches[sys.version_info[:2]])
        )

        af = SCons.Action.ActionFactory(LocalActFunc(), strfunc)
        ac = SCons.Action.ActionCaller(af, [], {})
        c = ac.get_contents([], [], Environment())
        assert c == matches[sys.version_info[:2]], (
            f"Got\n{c!r}\nExpected one of \n"
            + repr(matches[sys.version_info[:2]])
        )

        matches = [
            b"<built-in function str>",
            b"<type 'str'>",
        ]

        af = SCons.Action.ActionFactory(str, strfunc)
        ac = SCons.Action.ActionCaller(af, [], {})
        c = ac.get_contents([], [], Environment())
        assert c in ("<built-in function str>", "<type 'str'>", "<class 'str'>"), repr(c)
        # ^^ class str for python3

    def test___call__(self):
        """Test calling an ActionCaller"""

        actfunc_args = []

        def actfunc(a1, a2, a3, args=actfunc_args):
            args.extend([a1, a2, a3])

        def strfunc(a1, a2, a3):
            pass

        e = Environment(FOO=2, BAR=5)

        af = SCons.Action.ActionFactory(actfunc, strfunc)
        ac = SCons.Action.ActionCaller(af, ['$__env__', '$FOO', 3], {})
        ac([], [], e)
        assert actfunc_args[0] is e, actfunc_args
        assert actfunc_args[1] == '2', actfunc_args
        assert actfunc_args[2] == 3, actfunc_args
        del actfunc_args[:]

        ac = SCons.Action.ActionCaller(af, [], {'a3': '$__env__', 'a2': '$BAR', 'a1': 4})
        ac([], [], e)
        assert actfunc_args[0] == 4, actfunc_args
        assert actfunc_args[1] == '5', actfunc_args
        assert actfunc_args[2] is e, actfunc_args
        del actfunc_args[:]

    def test_strfunction(self):
        """Test calling the ActionCaller strfunction() method"""

        strfunc_args = []

        def actfunc(a1, a2, a3, a4):
            pass

        def strfunc(a1, a2, a3, a4, args=strfunc_args):
            args.extend([a1, a2, a3, a4])

        af = SCons.Action.ActionFactory(actfunc, strfunc)
        ac = SCons.Action.ActionCaller(af, [1, '$FOO', 3, '$WS'], {})
        ac.strfunction([], [], Environment(FOO=2, WS='white   space'))
        assert strfunc_args == [1, '2', 3, 'white   space'], strfunc_args

        del strfunc_args[:]
        d = {'a3': 6, 'a2': '$BAR', 'a1': 4, 'a4': '$WS'}
        ac = SCons.Action.ActionCaller(af, [], d)
        ac.strfunction([], [], Environment(BAR=5, WS='w   s'))
        assert strfunc_args == [4, '5', 6, 'w   s'], strfunc_args


class ActionFactoryTestCase(unittest.TestCase):
    def test___init__(self):
        """Test creation of an ActionFactory"""

        def actfunc():
            pass

        def strfunc():
            pass

        ac = SCons.Action.ActionFactory(actfunc, strfunc)
        assert ac.actfunc is actfunc, ac.actfunc
        assert ac.strfunc is strfunc, ac.strfunc

    def test___call__(self):
        """Test calling whatever's returned from an ActionFactory"""

        actfunc_args = []
        strfunc_args = []

        def actfunc(a1, a2, a3, args=actfunc_args):
            args.extend([a1, a2, a3])

        def strfunc(a1, a2, a3, args=strfunc_args):
            args.extend([a1, a2, a3])

        af = SCons.Action.ActionFactory(actfunc, strfunc)
        af(3, 6, 9)([], [], Environment())
        assert actfunc_args == [3, 6, 9], actfunc_args
        assert strfunc_args == [3, 6, 9], strfunc_args


class ActionCompareTestCase(unittest.TestCase):
    def test_1_solo_name(self):
        """Test Lazy Cmd Generator Action get_name alone.

        Basically ensures we can locate the builder, comparing it to
        itself along the way."""

        bar = SCons.Builder.Builder(action={})
        env = Environment(BUILDERS={'BAR': bar})
        name = bar.get_name(env)
        assert name == 'BAR', name

    def test_2_multi_name(self):
        """Test LazyCmdGenerator Action get_name multi builders.

        Ensure that we can compare builders (and thereby actions) to
        each other safely."""

        foo = SCons.Builder.Builder(action='$FOO', suffix='.foo')
        bar = SCons.Builder.Builder(action={})
        assert foo != bar
        assert foo.action != bar.action
        env = Environment(BUILDERS={'FOO': foo, 'BAR': bar})
        name = foo.get_name(env)
        assert name == 'FOO', name
        name = bar.get_name(env)
        assert name == 'BAR', name

    def test_3_dict_names(self):
        """Test Action/Suffix dicts with get_name.

        Verifies that Action/Suffix dictionaries work correctly,
        especially two builders that can generate the same suffix,
        where one of the builders has a suffix dictionary with a None
        key."""

        foo = SCons.Builder.Builder(action='$FOO', suffix='.foo')
        bar = SCons.Builder.Builder(action={}, suffix={None: '.bar'})
        bar.add_action('.cow', "$MOO")
        dog = SCons.Builder.Builder(suffix='.bar')
        env = Environment(BUILDERS={'FOO': foo, 'BAR': bar, 'DOG': dog})

        assert foo.get_name(env) == 'FOO', foo.get_name(env)
        assert bar.get_name(env) == 'BAR', bar.get_name(env)
        assert dog.get_name(env) == 'DOG', dog.get_name(env)


class TestClass:
    """A test class used by ObjectContentsTestCase.test_object_contents"""

    def __init__(self):
        self.a = "a"
        self.b = "b"

    def method(self, arg):
        pass


class ObjectContentsTestCase(unittest.TestCase):
    def test_function_contents(self):
        """Test that Action._function_contents works"""

        def func1(a, b, c):
            """A test function"""
            return a

        # Since the python bytecode has per version differences,
        # we need different expected results per version
        expected = {
            (3, 5): (bytearray(b'3, 3, 0, 0,(),(),(|\x00\x00S),(),()')),
            (3, 6): (bytearray(b'3, 3, 0, 0,(),(),(|\x00S\x00),(),()')),
            (3, 7): (bytearray(b'3, 3, 0, 0,(),(),(|\x00S\x00),(),()')),
            (3, 8): (bytearray(b'3, 3, 0, 0,(),(),(|\x00S\x00),(),()')),
            (3, 9): (bytearray(b'3, 3, 0, 0,(),(),(|\x00S\x00),(),()')),
            (3, 10): (
                bytearray(b'3, 3, 0, 0,(N.),(),(|\x00S\x00),(),()'),
                bytearray(b'3, 3, 0, 0,(),(),(|\x00S\x00),(),()'),
            ),  # 3.10.1, 3.10.2
            (3, 11): bytearray(b'3, 3, 0, 0,(),(),(\x97\x00|\x00S\x00),(),()'),
        }

        c = SCons.Action._function_contents(func1)
        assert c in expected[sys.version_info[:2]], f"Got\n{c!r}\nExpected\n" + repr(
            expected[sys.version_info[:2]]
        )

    def test_object_contents(self):
        """Test that Action._object_contents works"""

        # See definition above
        o = TestClass()
        c = SCons.Action._object_contents(o)

        # c = SCons.Action._object_instance_content(o)

        # Since the python bytecode has per version differences,
        # we need different expected results per version
        expected = {
            (3, 5): bytearray(
                b"{TestClass:__main__}[[[(<class \'object\'>, ()), [(<class \'__main__.TestClass\'>, (<class \'object\'>,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01\x00|\x00\x00_\x00\x00d\x02\x00|\x00\x00_\x01\x00d\x00\x00S),(),(),2, 2, 0, 0,(),(),(d\x00\x00S),(),()}}{{{a=a,b=b}}}"
            ),
            (3, 6): bytearray(
                b"{TestClass:__main__}[[[(<class \'object\'>, ()), [(<class \'__main__.TestClass\'>, (<class \'object\'>,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}"
            ),
            (3, 7): bytearray(
                b"{TestClass:__main__}[[[(<class \'object\'>, ()), [(<class \'__main__.TestClass\'>, (<class \'object\'>,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}"
            ),
            (3, 8): bytearray(
                b"{TestClass:__main__}[[[(<class \'object\'>, ()), [(<class \'__main__.TestClass\'>, (<class \'object\'>,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}"
            ),
            (3, 9): bytearray(
                b"{TestClass:__main__}[[[(<class \'object\'>, ()), [(<class \'__main__.TestClass\'>, (<class \'object\'>,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}"
            ),
            (3, 10): bytearray(
                b"{TestClass:__main__}[[[(<class \'object\'>, ()), [(<class \'__main__.TestClass\'>, (<class \'object\'>,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(d\x01|\x00_\x00d\x02|\x00_\x01d\x00S\x00),(),(),2, 2, 0, 0,(),(),(d\x00S\x00),(),()}}{{{a=a,b=b}}}"
            ),
            (3, 11): bytearray(
                b"{TestClass:__main__}[[[(<class \'object\'>, ()), [(<class \'__main__.TestClass\'>, (<class \'object\'>,))]]]]{{1, 1, 0, 0,(a,b),(a,b),(\x97\x00d\x01|\x00_\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x02|\x00_\x01\x00\x00\x00\x00\x00\x00\x00\x00d\x00S\x00),(),(),2, 2, 0, 0,(),(),(\x97\x00d\x00S\x00),(),()}}{{{a=a,b=b}}}"),
        }

        assert c == expected[sys.version_info[:2]], f"Got\n{c!r}\nExpected\n" + repr(
            expected[sys.version_info[:2]]
        )

    def test_code_contents(self):
        """Test that Action._code_contents works"""

        code = compile("print('Hello, World!')", '<string>', 'exec')
        c = SCons.Action._code_contents(code)

        # Since the python bytecode has per version differences, we need different expected results per version
        expected = {
            (3, 5): bytearray(
                b'0, 0, 0, 0,(Hello, World!),(print),(e\x00\x00d\x00\x00\x83\x01\x00\x01d\x01\x00S)'
            ),
            (3, 6): bytearray(
                b'0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)'
            ),
            (3, 7): bytearray(
                b'0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)'
            ),
            (3, 8): bytearray(
                b'0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)'
            ),
            (3, 9): bytearray(
                b'0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)'
            ),
            (3, 10): bytearray(
                b'0, 0, 0, 0,(Hello, World!),(print),(e\x00d\x00\x83\x01\x01\x00d\x01S\x00)'
            ),
            (3, 11): bytearray(
                b'0, 0, 0, 0,(Hello, World!),(print),(\x97\x00\x02\x00e\x00d\x00\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00d\x01S\x00)'),
        }

        assert c == expected[sys.version_info[:2]], f"Got\n{c!r}\nExpected\n" + repr(
            expected[sys.version_info[:2]]
        )

    def test_uncaught_exception_bubbles(self):
        """Test that _subproc bubbles uncaught exceptions"""

        try:
            pobj = SCons.Action._subproc(
                Environment(),
                None,
                stdin='devnull',
                stderr='devnull',
                stdout=subprocess.PIPE,
            )
            pobj.wait()
        except EnvironmentError:
            pass
        except Exception:
            # pass the test
            return

        raise Exception("expected a non-EnvironmentError exception")

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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/Builder.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
SCons.Builder

Builder object subsystem.

A Builder object is a callable that encapsulates information about how
to execute actions to create a target Node (file) from source Nodes
(files), and how to create those dependencies for tracking.

The main entry point here is the Builder() factory method.  This provides
a procedural interface that creates the right underlying Builder object
based on the keyword arguments supplied and the types of the arguments.

The goal is for this external interface to be simple enough that the
vast majority of users can create new Builders as necessary to support
building new types of files in their configurations, without having to
dive any deeper into this subsystem.

The base class here is BuilderBase.  This is a concrete base class which
does, in fact, represent the Builder objects that we (or users) create.

There is also a proxy that looks like a Builder:

    CompositeBuilder

        This proxies for a Builder with an action that is actually a
        dictionary that knows how to map file suffixes to a specific
        action.  This is so that we can invoke different actions
        (compilers, compile options) for different flavors of source
        files.

Builders and their proxies have the following public interface methods
used by other modules:

    - __call__()
        THE public interface.  Calling a Builder object (with the
        use of internal helper methods) sets up the target and source
        dependencies, appropriate mapping to a specific action, and the
        environment manipulation necessary for overridden construction
        variable.  This also takes care of warning about possible mistakes
        in keyword arguments.

    - add_emitter()
        Adds an emitter for a specific file suffix, used by some Tool
        modules to specify that (for example) a yacc invocation on a .y
        can create a .h *and* a .c file.

    - add_action()
        Adds an action for a specific file suffix, heavily used by
        Tool modules to add their specific action(s) for turning
        a source file into an object file to the global static
        and shared object file Builders.

There are the following methods for internal use within this module:

    - _execute()
        The internal method that handles the heavily lifting when a
        Builder is called.  This is used so that the __call__() methods
        can set up warning about possible mistakes in keyword-argument
        overrides, and *then* execute all of the steps necessary so that
        the warnings only occur once.

    - get_name()
        Returns the Builder's name within a specific Environment,
        primarily used to try to return helpful information in error
        messages.

    - adjust_suffix()
    - get_prefix()
    - get_suffix()
    - get_src_suffix()
    - set_src_suffix()
        Miscellaneous stuff for handling the prefix and suffix
        manipulation we use in turning source file names into target
        file names.

"""

from collections import UserDict, UserList

import SCons.Action
import SCons.Debug
import SCons.Executor
import SCons.Memoize
import SCons.Util
import SCons.Warnings
from SCons.Debug import logInstanceCreation
from SCons.Errors import InternalError, UserError

class _Null:
    pass

_null = _Null

def match_splitext(path, suffixes = []):
    if suffixes:
        matchsuf = [S for S in suffixes if path[-len(S):] == S]
        if matchsuf:
            suf = max([(len(_f),_f) for _f in matchsuf])[1]
            return [path[:-len(suf)], path[-len(suf):]]
    return SCons.Util.splitext(path)

class DictCmdGenerator(SCons.Util.Selector):
    """This is a callable class that can be used as a
    command generator function.  It holds on to a dictionary
    mapping file suffixes to Actions.  It uses that dictionary
    to return the proper action based on the file suffix of
    the source file."""

    def __init__(self, mapping=None, source_ext_match=True):
        super().__init__(mapping)
        self.source_ext_match = source_ext_match

    def src_suffixes(self):
        return list(self.keys())

    def add_action(self, suffix, action):
        """Add a suffix-action pair to the mapping.
        """
        self[suffix] = action

    def __call__(self, target, source, env, for_signature):
        if not source:
            return []

        if self.source_ext_match:
            suffixes = self.src_suffixes()
            ext = None
            for src in map(str, source):
                my_ext = match_splitext(src, suffixes)[1]
                if ext and my_ext != ext:
                    raise UserError("While building `%s' from `%s': Cannot build multiple sources with different extensions: %s, %s"
                             % (repr(list(map(str, target))), src, ext, my_ext))
                ext = my_ext
        else:
            ext = match_splitext(str(source[0]), self.src_suffixes())[1]

        if not ext:
            #return ext
            raise UserError("While building `%s': "
                            "Cannot deduce file extension from source files: %s"
                 % (repr(list(map(str, target))), repr(list(map(str, source)))))

        try:
            ret = SCons.Util.Selector.__call__(self, env, source, ext)
        except KeyError as e:
            raise UserError("Ambiguous suffixes after environment substitution: %s == %s == %s" % (e.args[0], e.args[1], e.args[2]))
        if ret is None:
            raise UserError("While building `%s' from `%s': Don't know how to build from a source file with suffix `%s'.  Expected a suffix in this list: %s." % \
                            (repr(list(map(str, target))), repr(list(map(str, source))), ext, repr(list(self.keys()))))
        return ret

class CallableSelector(SCons.Util.Selector):
    """A callable dictionary that will, in turn, call the value it
    finds if it can."""
    def __call__(self, env, source):
        value = SCons.Util.Selector.__call__(self, env, source)
        if callable(value):
            value = value(env, source)
        return value

class DictEmitter(SCons.Util.Selector):
    """A callable dictionary that maps file suffixes to emitters.
    When called, it finds the right emitter in its dictionary for the
    suffix of the first source file, and calls that emitter to get the
    right lists of targets and sources to return.  If there's no emitter
    for the suffix in its dictionary, the original target and source are
    returned.
    """
    def __call__(self, target, source, env):
        emitter = SCons.Util.Selector.__call__(self, env, source)
        if emitter:
            target, source = emitter(target, source, env)
        return (target, source)

class ListEmitter(UserList):
    """A callable list of emitters that calls each in sequence,
    returning the result.
    """
    def __call__(self, target, source, env):
        for e in self.data:
            target, source = e(target, source, env)
        return (target, source)

# These are a common errors when calling a Builder;
# they are similar to the 'target' and 'source' keyword args to builders,
# so we issue warnings when we see them.  The warnings can, of course,
# be disabled.
misleading_keywords = {
    'targets'   : 'target',
    'sources'   : 'source',
}

class OverrideWarner(UserDict):
    """A class for warning about keyword arguments that we use as
    overrides in a Builder call.

    This class exists to handle the fact that a single Builder call
    can actually invoke multiple builders.  This class only emits the
    warnings once, no matter how many Builders are invoked.
    """
    def __init__(self, mapping):
        super().__init__(mapping)
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Builder.OverrideWarner')
        self.already_warned = None

    def warn(self):
        if self.already_warned:
            return
        for k in self.keys():
            if k in misleading_keywords:
                alt = misleading_keywords[k]
                msg = "Did you mean to use `%s' instead of `%s'?" % (alt, k)
                SCons.Warnings.warn(SCons.Warnings.MisleadingKeywordsWarning, msg)
        self.already_warned = 1

def Builder(**kw):
    """A factory for builder objects."""
    composite = None
    if 'generator' in kw:
        if 'action' in kw:
            raise UserError("You must not specify both an action and a generator.")
        kw['action'] = SCons.Action.CommandGeneratorAction(kw['generator'], {})
        del kw['generator']
    elif 'action' in kw:
        source_ext_match = kw.get('source_ext_match', True)
        if 'source_ext_match' in kw:
            del kw['source_ext_match']
        if SCons.Util.is_Dict(kw['action']):
            composite = DictCmdGenerator(kw['action'], source_ext_match)
            kw['action'] = SCons.Action.CommandGeneratorAction(composite, {})
            kw['src_suffix'] = composite.src_suffixes()
        else:
            kw['action'] = SCons.Action.Action(kw['action'])

    if 'emitter' in kw:
        emitter = kw['emitter']
        if SCons.Util.is_String(emitter):
            # This allows users to pass in an Environment
            # variable reference (like "$FOO") as an emitter.
            # We will look in that Environment variable for
            # a callable to use as the actual emitter.
            var = SCons.Util.get_environment_var(emitter)
            if not var:
                raise UserError("Supplied emitter '%s' does not appear to refer to an Environment variable" % emitter)
            kw['emitter'] = EmitterProxy(var)
        elif SCons.Util.is_Dict(emitter):
            kw['emitter'] = DictEmitter(emitter)
        elif SCons.Util.is_List(emitter):
            kw['emitter'] = ListEmitter(emitter)

    result = BuilderBase(**kw)

    if composite is not None:
        result = CompositeBuilder(result, composite)

    return result

def _node_errors(builder, env, tlist, slist):
    """Validate that the lists of target and source nodes are
    legal for this builder and environment.  Raise errors or
    issue warnings as appropriate.
    """

    # First, figure out if there are any errors in the way the targets
    # were specified.
    for t in tlist:
        if t.side_effect:
            raise UserError("Multiple ways to build the same target were specified for: %s" % t)
        if t.has_explicit_builder():
            # Check for errors when the environments are different
            # No error if environments are the same Environment instance
            if (t.env is not None and t.env is not env and
                    # Check OverrideEnvironment case - no error if wrapped Environments
                    # are the same instance, and overrides lists match
                    not (getattr(t.env, '__subject', 0) is getattr(env, '__subject', 1) and
                         getattr(t.env, 'overrides', 0) == getattr(env, 'overrides', 1) and
                         not builder.multi)):
                action = t.builder.action
                t_contents = t.builder.action.get_contents(tlist, slist, t.env)
                contents = builder.action.get_contents(tlist, slist, env)

                if t_contents == contents:
                    msg = "Two different environments were specified for target %s,\n\tbut they appear to have the same action: %s" % (t, action.genstring(tlist, slist, t.env))
                    SCons.Warnings.warn(SCons.Warnings.DuplicateEnvironmentWarning, msg)
                else:
                    try:
                        msg = "Two environments with different actions were specified for the same target: %s\n(action 1: %s)\n(action 2: %s)" % (t,t_contents.decode('utf-8'),contents.decode('utf-8'))
                    except UnicodeDecodeError:
                        msg = "Two environments with different actions were specified for the same target: %s"%t
                    raise UserError(msg)
            if builder.multi:
                if t.builder != builder:
                    msg = "Two different builders (%s and %s) were specified for the same target: %s" % (t.builder.get_name(env), builder.get_name(env), t)
                    raise UserError(msg)
                # TODO(batch):  list constructed each time!
                if t.get_executor().get_all_targets() != tlist:
                    msg = "Two different target lists have a target in common: %s  (from %s and from %s)" % (t, list(map(str, t.get_executor().get_all_targets())), list(map(str, tlist)))
                    raise UserError(msg)
            elif t.sources != slist:
                msg = "Multiple ways to build the same target were specified for: %s  (from %s and from %s)" % (t, list(map(str, t.sources)), list(map(str, slist)))
                raise UserError(msg)

    if builder.single_source:
        if len(slist) > 1:
            raise UserError("More than one source given for single-source builder: targets=%s sources=%s" % (list(map(str,tlist)), list(map(str,slist))))

class EmitterProxy:
    """This is a callable class that can act as a
    Builder emitter.  It holds on to a string that
    is a key into an Environment dictionary, and will
    look there at actual build time to see if it holds
    a callable.  If so, we will call that as the actual
    emitter."""
    def __init__(self, var):
        self.var = SCons.Util.to_String(var)

    def __call__(self, target, source, env):
        emitter = self.var

        # Recursively substitute the variable.
        # We can't use env.subst() because it deals only
        # in strings.  Maybe we should change that?
        while SCons.Util.is_String(emitter) and emitter in env:
            emitter = env[emitter]
        if callable(emitter):
            target, source = emitter(target, source, env)
        elif SCons.Util.is_List(emitter):
            for e in emitter:
                target, source = e(target, source, env)

        return (target, source)

    def __eq__(self, other):
        return self.var == other.var

    def __lt__(self, other):
        return self.var < other.var

    def __le__(self, other):
        return self.var <= other.var

    def __gt__(self, other):
        return self.var > other.var

    def __ge__(self, other):
        return self.var >= other.var

class BuilderBase:
    """Base class for Builders, objects that create output
    nodes (files) from input nodes (files).
    """

    def __init__(self,  action = None,
                        prefix = '',
                        suffix = '',
                        src_suffix = '',
                        target_factory = None,
                        source_factory = None,
                        target_scanner = None,
                        source_scanner = None,
                        emitter = None,
                        multi = 0,
                        env = None,
                        single_source = 0,
                        name = None,
                        chdir = _null,
                        is_explicit = 1,
                        src_builder = None,
                        ensure_suffix = False,
                        **overrides):
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Builder.BuilderBase')
        self._memo = {}
        self.action = action
        self.multi = multi
        if SCons.Util.is_Dict(prefix):
            prefix = CallableSelector(prefix)
        self.prefix = prefix
        if SCons.Util.is_Dict(suffix):
            suffix = CallableSelector(suffix)
        self.env = env
        self.single_source = single_source
        if 'overrides' in overrides:
            msg =  "The \"overrides\" keyword to Builder() creation has been removed;\n" +\
                "\tspecify the items as keyword arguments to the Builder() call instead."
            raise TypeError(msg)
        if 'scanner' in overrides:
            msg = "The \"scanner\" keyword to Builder() creation has been removed;\n" +\
                "\tuse: source_scanner or target_scanner as appropriate."
            raise TypeError(msg)
        self.overrides = overrides

        self.set_suffix(suffix)
        self.set_src_suffix(src_suffix)
        self.ensure_suffix = ensure_suffix

        self.target_factory = target_factory
        self.source_factory = source_factory
        self.target_scanner = target_scanner
        self.source_scanner = source_scanner

        self.emitter = emitter

        # Optional Builder name should only be used for Builders
        # that don't get attached to construction environments.
        if name:
            self.name = name
        self.executor_kw = {}
        if chdir is not _null:
            self.executor_kw['chdir'] = chdir
        self.is_explicit = is_explicit

        if src_builder is None:
            src_builder = []
        elif not SCons.Util.is_List(src_builder):
            src_builder = [ src_builder ]
        self.src_builder = src_builder

    def __bool__(self):
        raise InternalError("Do not test for the Node.builder attribute directly; use Node.has_builder() instead")

    def get_name(self, env):
        """Attempts to get the name of the Builder.

        Look at the BUILDERS variable of env, expecting it to be a
        dictionary containing this Builder, and return the key of the
        dictionary.  If there's no key, then return a directly-configured
        name (if there is one) or the name of the class (by default)."""

        try:
            index = list(env['BUILDERS'].values()).index(self)
            return list(env['BUILDERS'].keys())[index]
        except (AttributeError, KeyError, TypeError, ValueError):
            try:
                return self.name
            except AttributeError:
                return str(self.__class__)

    def __eq__(self, other):
        return self.__dict__ == other.__dict__

    def splitext(self, path, env=None):
        if not env:
            env = self.env
        if env:
            suffixes = self.src_suffixes(env)
        else:
            suffixes = []
        return match_splitext(path, suffixes)

    def _adjustixes(self, files, pre, suf, ensure_suffix=False):
        if not files:
            return []
        result = []
        if not SCons.Util.is_List(files):
            files = [files]

        for f in files:
            if SCons.Util.is_String(f):
                f = SCons.Util.adjustixes(f, pre, suf, ensure_suffix)
            result.append(f)
        return result

    def _create_nodes(self, env, target = None, source = None):
        """Create and return lists of target and source nodes.
        """
        src_suf = self.get_src_suffix(env)

        target_factory = env.get_factory(self.target_factory)
        source_factory = env.get_factory(self.source_factory)

        source = self._adjustixes(source, None, src_suf)
        slist = env.arg2nodes(source, source_factory)

        pre = self.get_prefix(env, slist)
        suf = self.get_suffix(env, slist)

        if target is None:
            try:
                t_from_s = slist[0].target_from_source
            except AttributeError:
                raise UserError("Do not know how to create a target from source `%s'" % slist[0])
            except IndexError:
                tlist = []
            else:
                splitext = lambda S: self.splitext(S,env)
                tlist = [ t_from_s(pre, suf, splitext) ]
        else:
            # orig_target = target
            target = self._adjustixes(target, pre, suf, self.ensure_suffix)
            tlist = env.arg2nodes(target, target_factory, target=target, source=source)

        if self.emitter:
            # The emitter is going to do str(node), but because we're
            # being called *from* a builder invocation, the new targets
            # don't yet have a builder set on them and will look like
            # source files.  Fool the emitter's str() calls by setting
            # up a temporary builder on the new targets.
            new_targets = []
            for t in tlist:
                if not t.is_derived():
                    t.builder_set(self)
                    new_targets.append(t)

            orig_tlist = tlist[:]
            orig_slist = slist[:]

            target, source = self.emitter(target=tlist, source=slist, env=env)

            # Now delete the temporary builders that we attached to any
            # new targets, so that _node_errors() doesn't do weird stuff
            # to them because it thinks they already have builders.
            for t in new_targets:
                if t.builder is self:
                    # Only delete the temporary builder if the emitter
                    # didn't change it on us.
                    t.builder_set(None)

            # Have to call arg2nodes yet again, since it is legal for
            # emitters to spit out strings as well as Node instances.
            tlist = env.arg2nodes(target, target_factory,
                                  target=orig_tlist, source=orig_slist)
            slist = env.arg2nodes(source, source_factory,
                                  target=orig_tlist, source=orig_slist)

        return tlist, slist

    def _execute(self, env, target, source, overwarn={}, executor_kw={}):
        # We now assume that target and source are lists or None.
        if self.src_builder:
            source = self.src_builder_sources(env, source, overwarn)

        if self.single_source and len(source) > 1 and target is None:
            result = []
            if target is None: target = [None]*len(source)
            for tgt, src in zip(target, source):
                if tgt is not None:
                    tgt = [tgt]
                if src is not None:
                    src = [src]
                result.extend(self._execute(env, tgt, src, overwarn))
            return SCons.Node.NodeList(result)

        overwarn.warn()

        tlist, slist = self._create_nodes(env, target, source)

        # If there is more than one target ensure that if we need to reset
        # the implicit list to new scan of dependency all targets implicit lists
        # are cleared. (SCons GH Issue #2811 and MongoDB SERVER-33111)
        if len(tlist) > 1:
            for t in tlist:
                t.target_peers = tlist

        # Check for errors with the specified target/source lists.
        _node_errors(self, env, tlist, slist)

        # The targets are fine, so find or make the appropriate Executor to
        # build this particular list of targets from this particular list of
        # sources.

        executor = None
        key = None

        if self.multi:
            try:
                executor = tlist[0].get_executor(create = 0)
            except (AttributeError, IndexError):
                pass
            else:
                executor.add_sources(slist)

        if executor is None:
            if not self.action:
                fmt = "Builder %s must have an action to build %s."
                raise UserError(fmt % (self.get_name(env or self.env),
                                        list(map(str,tlist))))
            key = self.action.batch_key(env or self.env, tlist, slist)
            if key:
                try:
                    executor = SCons.Executor.GetBatchExecutor(key)
                except KeyError:
                    pass
                else:
                    executor.add_batch(tlist, slist)

        if executor is None:
            executor = SCons.Executor.Executor(self.action, env, [],
                                               tlist, slist, executor_kw)
            if key:
                SCons.Executor.AddBatchExecutor(key, executor)

        # Now set up the relevant information in the target Nodes themselves.
        for t in tlist:
            t.cwd = env.fs.getcwd()
            t.builder_set(self)
            t.env_set(env)
            t.add_source(slist)
            t.set_executor(executor)
            t.set_explicit(self.is_explicit)

        if env.get("SCONF_NODE"):
            for node in tlist:
                node.attributes.conftest_node = 1

        return SCons.Node.NodeList(tlist)

    def __call__(self, env, target=None, source=None, chdir=_null, **kw):
        # We now assume that target and source are lists or None.
        # The caller (typically Environment.BuilderWrapper) is
        # responsible for converting any scalar values to lists.
        if chdir is _null:
            ekw = self.executor_kw
        else:
            ekw = self.executor_kw.copy()
            ekw['chdir'] = chdir
        if 'chdir' in ekw and SCons.Util.is_String(ekw['chdir']):
            ekw['chdir'] = env.subst(ekw['chdir'])
        if kw:
            if 'srcdir' in kw:
                def prependDirIfRelative(f, srcdir=kw['srcdir']):
                    import os.path
                    if SCons.Util.is_String(f) and not os.path.isabs(f):
                        f = os.path.join(srcdir, f)
                    return f
                if not SCons.Util.is_List(source):
                    source = [source]
                source = list(map(prependDirIfRelative, source))
                del kw['srcdir']
            if self.overrides:
                env_kw = self.overrides.copy()
                env_kw.update(kw)
            else:
                env_kw = kw
        else:
            env_kw = self.overrides

        # TODO if env_kw: then the following line. there's no purpose in calling if no overrides.
        env = env.Override(env_kw)
        return self._execute(env, target, source, OverrideWarner(kw), ekw)

    def adjust_suffix(self, suff):
        if suff and not suff[0] in [ '.', '_', '$' ]:
            return '.' + suff
        return suff

    def get_prefix(self, env, sources=[]):
        prefix = self.prefix
        if callable(prefix):
            prefix = prefix(env, sources)
        return env.subst(prefix)

    def set_suffix(self, suffix):
        if not callable(suffix):
            suffix = self.adjust_suffix(suffix)
        self.suffix = suffix

    def get_suffix(self, env, sources=[]):
        suffix = self.suffix
        if callable(suffix):
            suffix = suffix(env, sources)
        return env.subst(suffix)

    def set_src_suffix(self, src_suffix):
        if not src_suffix:
            src_suffix = []
        elif not SCons.Util.is_List(src_suffix):
            src_suffix = [ src_suffix ]
        self.src_suffix = [callable(suf) and suf or self.adjust_suffix(suf) for suf in src_suffix]

    def get_src_suffix(self, env):
        """Get the first src_suffix in the list of src_suffixes."""
        ret = self.src_suffixes(env)
        if not ret:
            return ''
        return ret[0]

    def add_emitter(self, suffix, emitter):
        """Add a suffix-emitter mapping to this Builder.

        This assumes that emitter has been initialized with an
        appropriate dictionary type, and will throw a TypeError if
        not, so the caller is responsible for knowing that this is an
        appropriate method to call for the Builder in question.
        """
        self.emitter[suffix] = emitter

    def add_src_builder(self, builder):
        """
        Add a new Builder to the list of src_builders.

        This requires wiping out cached values so that the computed
        lists of source suffixes get re-calculated.
        """
        self._memo = {}
        self.src_builder.append(builder)

    def _get_sdict(self, env):
        """
        Returns a dictionary mapping all of the source suffixes of all
        src_builders of this Builder to the underlying Builder that
        should be called first.

        This dictionary is used for each target specified, so we save a
        lot of extra computation by memoizing it for each construction
        environment.

        Note that this is re-computed each time, not cached, because there
        might be changes to one of our source Builders (or one of their
        source Builders, and so on, and so on...) that we can't "see."

        The underlying methods we call cache their computed values,
        though, so we hope repeatedly aggregating them into a dictionary
        like this won't be too big a hit.  We may need to look for a
        better way to do this if performance data show this has turned
        into a significant bottleneck.
        """
        sdict = {}
        for bld in self.get_src_builders(env):
            for suf in bld.src_suffixes(env):
                sdict[suf] = bld
        return sdict

    def src_builder_sources(self, env, source, overwarn={}):
        sdict = self._get_sdict(env)

        src_suffixes = self.src_suffixes(env)

        lengths = list(set(map(len, src_suffixes)))

        def match_src_suffix(name, src_suffixes=src_suffixes, lengths=lengths):
            node_suffixes = [name[-l:] for l in lengths]
            for suf in src_suffixes:
                if suf in node_suffixes:
                    return suf
            return None

        result = []
        for s in SCons.Util.flatten(source):
            if SCons.Util.is_String(s):
                match_suffix = match_src_suffix(env.subst(s))
                if not match_suffix and '.' not in s:
                    src_suf = self.get_src_suffix(env)
                    s = self._adjustixes(s, None, src_suf)[0]
            else:
                match_suffix = match_src_suffix(s.name)
            if match_suffix:
                try:
                    bld = sdict[match_suffix]
                except KeyError:
                    result.append(s)
                else:
                    tlist = bld._execute(env, None, [s], overwarn)
                    # If the subsidiary Builder returned more than one
                    # target, then filter out any sources that this
                    # Builder isn't capable of building.
                    if len(tlist) > 1:
                        tlist = [t for t in tlist if match_src_suffix(t.name)]
                    result.extend(tlist)
            else:
                result.append(s)

        source_factory = env.get_factory(self.source_factory)

        return env.arg2nodes(result, source_factory)

    def _get_src_builders_key(self, env):
        return id(env)

    @SCons.Memoize.CountDictCall(_get_src_builders_key)
    def get_src_builders(self, env):
        """
        Returns the list of source Builders for this Builder.

        This exists mainly to look up Builders referenced as
        strings in the 'BUILDER' variable of the construction
        environment and cache the result.
        """
        memo_key = id(env)
        try:
            memo_dict = self._memo['get_src_builders']
        except KeyError:
            memo_dict = {}
            self._memo['get_src_builders'] = memo_dict
        else:
            try:
                return memo_dict[memo_key]
            except KeyError:
                pass

        builders = []
        for bld in self.src_builder:
            if SCons.Util.is_String(bld):
                try:
                    bld = env['BUILDERS'][bld]
                except KeyError:
                    continue
            builders.append(bld)

        memo_dict[memo_key] = builders
        return builders

    def _subst_src_suffixes_key(self, env):
        return id(env)

    @SCons.Memoize.CountDictCall(_subst_src_suffixes_key)
    def subst_src_suffixes(self, env):
        """
        The suffix list may contain construction variable expansions,
        so we have to evaluate the individual strings.  To avoid doing
        this over and over, we memoize the results for each construction
        environment.
        """
        memo_key = id(env)
        try:
            memo_dict = self._memo['subst_src_suffixes']
        except KeyError:
            memo_dict = {}
            self._memo['subst_src_suffixes'] = memo_dict
        else:
            try:
                return memo_dict[memo_key]
            except KeyError:
                pass
        suffixes = [env.subst(x) for x in self.src_suffix]
        memo_dict[memo_key] = suffixes
        return suffixes

    def src_suffixes(self, env):
        """
        Returns the list of source suffixes for all src_builders of this
        Builder.

        This is essentially a recursive descent of the src_builder "tree."
        (This value isn't cached because there may be changes in a
        src_builder many levels deep that we can't see.)
        """
        sdict = {}
        suffixes = self.subst_src_suffixes(env)
        for s in suffixes:
            sdict[s] = 1
        for builder in self.get_src_builders(env):
            for s in builder.src_suffixes(env):
                if s not in sdict:
                    sdict[s] = 1
                    suffixes.append(s)
        return suffixes

class CompositeBuilder(SCons.Util.Proxy):
    """A Builder Proxy whose main purpose is to always have
    a DictCmdGenerator as its action, and to provide access
    to the DictCmdGenerator's add_action() method.
    """

    def __init__(self, builder, cmdgen):
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Builder.CompositeBuilder')
        super().__init__(builder)

        # cmdgen should always be an instance of DictCmdGenerator.
        self.cmdgen = cmdgen
        self.builder = builder

    __call__ = SCons.Util.Delegate('__call__')

    def add_action(self, suffix, action):
        self.cmdgen.add_action(suffix, action)
        self.set_src_suffix(self.cmdgen.src_suffixes())

def is_a_Builder(obj):
    """"Returns True if the specified obj is one of our Builder classes.

    The test is complicated a bit by the fact that CompositeBuilder
    is a proxy, not a subclass of BuilderBase.
    """
    return (isinstance(obj, BuilderBase)
            or isinstance(obj, CompositeBuilder)
            or callable(obj))

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/BuilderTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import SCons.compat

# Define a null function for use as a builder action.
# Where this is defined in the file seems to affect its
# byte-code contents, so try to minimize changes by
# defining it here, before we even import anything.
def Func():
    pass

from collections import UserList
import io
import os.path
import re
import sys
import unittest

import TestCmd

import SCons.Action
import SCons.Builder
import SCons.Environment
import SCons.Errors
import SCons.Subst
import SCons.Util

sys.stdout = io.StringIO()

# Initial setup of the common environment for all tests,
# a temporary working directory containing a
# script for writing arguments to an output file.
#
# We don't do this as a setUp() method because it's
# unnecessary to create a separate directory and script
# for each test, they can just use the one.
test = TestCmd.TestCmd(workdir = '')

outfile = test.workpath('outfile')
outfile2 = test.workpath('outfile2')

infile = test.workpath('infile')
test.write(infile, "infile\n")

show_string = None

scons_env = SCons.Environment.Environment()

env_arg2nodes_called = None

class Environment:
    def __init__(self, **kw):
        self.d = {}
        self.d['SHELL'] = scons_env['SHELL']
        self.d['SPAWN'] = scons_env['SPAWN']
        self.d['ESCAPE'] = scons_env['ESCAPE']
        for k, v in kw.items():
            self.d[k] = v
        global env_arg2nodes_called
        env_arg2nodes_called = None
        self.scanner = None
        self.fs = SCons.Node.FS.FS()
    def subst(self, s):
        if not SCons.Util.is_String(s):
            return s
        def substitute(m, d=self.d):
            return d.get(m.group(1), '')
        return re.sub(r'\$(\w+)', substitute, s)
    def subst_target_source(self, string, raw=0, target=None,
                            source=None, dict=None, conv=None):
        return SCons.Subst.scons_subst(string, self, raw, target,
                                       source, dict, conv)
    def subst_list(self, string, raw=0, target=None, source=None, conv=None):
        return SCons.Subst.scons_subst_list(string, self, raw, target,
                                            source, {}, {}, conv)
    def arg2nodes(self, args, factory, **kw):
        global env_arg2nodes_called
        env_arg2nodes_called = 1
        if not SCons.Util.is_List(args):
            args = [args]
        list = []
        for a in args:
            if SCons.Util.is_String(a):
                a = factory(self.subst(a))
            list.append(a)
        return list
    def get_factory(self, factory):
        return factory or self.fs.File
    def get_scanner(self, ext):
        return self.scanner
    def Dictionary(self):
        return {}
    def autogenerate(self, dir=''):
        return {}
    def __setitem__(self, item, var):
        self.d[item] = var
    def __getitem__(self, item):
        return self.d[item]
    def __contains__(self, key):
        return key in self.d
    def keys(self):
        return list(self.d.keys())
    def get(self, key, value=None):
        return self.d.get(key, value)
    def Override(self, overrides):
        env = Environment(**self.d)
        env.d.update(overrides)
        env.scanner = self.scanner
        return env
    def _update(self, dict):
        self.d.update(dict)
    def items(self):
        return list(self.d.items())
    def sig_dict(self):
        d = {}
        for k,v in self.items(): d[k] = v
        d['TARGETS'] = ['__t1__', '__t2__', '__t3__', '__t4__', '__t5__', '__t6__']
        d['TARGET'] = d['TARGETS'][0]
        d['SOURCES'] = ['__s1__', '__s2__', '__s3__', '__s4__', '__s5__', '__s6__']
        d['SOURCE'] = d['SOURCES'][0]
        return d
    def __eq__(self, other):
        return self.scanner == other.scanner or self.d == other.d

class MyAction:
    def __init__(self, action):
        self.action = action
    def __call__(self, *args, **kw):
        pass
    def get_executor(self, env, overrides, tlist, slist, executor_kw):
        return ['executor'] + [self.action]

class MyNode_without_target_from_source:
    def __init__(self, name):
        self.name = name
        self.sources = []
        self.builder = None
        self.is_explicit = None
        self.side_effect = 0
    def get_suffix(self):
        return os.path.splitext(self.name)[1]
    def disambiguate(self):
        return self
    def __str__(self):
        return self.name
    def builder_set(self, builder):
        self.builder = builder
    def has_builder(self):
        return self.builder is not None
    def set_explicit(self, is_explicit):
        self.is_explicit = is_explicit
    def has_explicit_builder(self):
        return self.is_explicit
    def env_set(self, env, safe=0):
        self.env = env
    def add_source(self, source):
        self.sources.extend(source)
    def scanner_key(self):
        return self.name
    def is_derived(self):
        return self.has_builder()
    def generate_build_env(self, env):
        return env
    def get_build_env(self):
        return self.executor.get_build_env()
    def set_executor(self, executor):
        self.executor = executor
    def get_executor(self, create=1):
        return self.executor

class MyNode(MyNode_without_target_from_source):
    def target_from_source(self, prefix, suffix, stripext):
        return MyNode(prefix + stripext(str(self))[0] + suffix)

class BuilderTestCase(unittest.TestCase):

    def test__init__(self):
        """Test simple Builder creation
        """
        builder = SCons.Builder.Builder(action="foo")
        assert builder is not None, builder
        builder = SCons.Builder.Builder(action="foo", OVERRIDE='x')
        x = builder.overrides['OVERRIDE']
        assert x == 'x', x

    def test__bool__(self):
        """Test a builder raising an exception when __bool__ is called. """

        # basic test: explicitly call it
        builder = SCons.Builder.Builder(action="foo")
        exc_caught = None
        try:
            builder.__bool__()
        except SCons.Errors.InternalError:
            exc_caught = 1
        assert exc_caught, "did not catch expected InternalError exception"

        class Node:
             pass

        # the interesting test: checking the attribute this way
        # should call back to Builder.__bool__ - so we can tell
        # people not to check that way (for performance).
        # TODO: workaround #3860: with just a "pass" in the check body,
        # py3.10a optimizes out the whole thing, so add a "real" stmt
        n = Node()
        n.builder = builder
        exc_caught = None
        try:
            if n.builder:
                #pass
                _ = True
        except SCons.Errors.InternalError:
            exc_caught = 1
        assert exc_caught, "did not catch expected InternalError exception"

    def test__call__(self):
        """Test calling a builder to establish source dependencies
        """
        env = Environment()
        builder = SCons.Builder.Builder(action="foo",
                                        target_factory=MyNode,
                                        source_factory=MyNode)

        tgt = builder(env, source=[])
        assert tgt == [], tgt

        n1 = MyNode("n1")
        n2 = MyNode("n2")
        builder(env, target = n1, source = n2)
        assert env_arg2nodes_called
        assert n1.env == env, n1.env
        assert n1.builder == builder, n1.builder
        assert n1.sources == [n2], n1.sources
        assert n1.executor, "no executor found"
        assert not hasattr(n2, 'env')

        l = [1]
        ul = UserList([2])
        try:
            l.extend(ul)
        except TypeError:
            def mystr(l):
                return str(list(map(str, l)))
        else:
            mystr = str

        nnn1 = MyNode("nnn1")
        nnn2 = MyNode("nnn2")
        tlist = builder(env, target = [nnn1, nnn2], source = [])
        s = mystr(tlist)
        assert s == "['nnn1', 'nnn2']", s
        l = list(map(str, tlist))
        assert l == ['nnn1', 'nnn2'], l

        tlist = builder(env, target = 'n3', source = 'n4')
        s = mystr(tlist)
        assert s == "['n3']", s
        target = tlist[0]
        l = list(map(str, tlist))
        assert l == ['n3'], l
        assert target.name == 'n3'
        assert target.sources[0].name == 'n4'

        tlist = builder(env, target = 'n4 n5', source = ['n6 n7'])
        s = mystr(tlist)
        assert s == "['n4 n5']", s
        l = list(map(str, tlist))
        assert l == ['n4 n5'], l
        target = tlist[0]
        assert target.name == 'n4 n5'
        assert target.sources[0].name == 'n6 n7'

        tlist = builder(env, target = ['n8 n9'], source = 'n10 n11')
        s = mystr(tlist)
        assert s == "['n8 n9']", s
        l = list(map(str, tlist))
        assert l == ['n8 n9'], l
        target = tlist[0]
        assert target.name == 'n8 n9'
        assert target.sources[0].name == 'n10 n11'

        # A test to be uncommented when we freeze the environment
        # as part of calling the builder.
        #env1 = Environment(VAR='foo')
        #target = builder(env1, target = 'n12', source = 'n13')
        #env1['VAR'] = 'bar'
        #be = target.get_build_env()
        #assert be['VAR'] == 'foo', be['VAR']

        n20 = MyNode_without_target_from_source('n20')
        flag = 0
        try:
            target = builder(env, None, source=n20)
        except SCons.Errors.UserError as e:
            flag = 1
        assert flag, "UserError should be thrown if a source node can't create a target."

        builder = SCons.Builder.Builder(action="foo",
                                        target_factory=MyNode,
                                        source_factory=MyNode,
                                        prefix='p-',
                                        suffix='.s')
        target = builder(env, None, source='n21')[0]
        assert target.name == 'p-n21.s', target

        builder = SCons.Builder.Builder(misspelled_action="foo",
                                        suffix = '.s')
        try:
            builder(env, target = 'n22', source = 'n22')
        except SCons.Errors.UserError as e:
            pass
        else:
            raise Exception("Did not catch expected UserError.")

        builder = SCons.Builder.Builder(action="foo")
        target = builder(env, None, source='n22', srcdir='src_dir')[0]
        p = target.sources[0].get_internal_path()
        assert p == os.path.join('src_dir', 'n22'), p

    def test_mistaken_variables(self):
        """Test keyword arguments that are often mistakes
        """
        import SCons.Warnings
        env = Environment()
        builder = SCons.Builder.Builder(action="foo")

        save_warn = SCons.Warnings.warn
        warned = []
        def my_warn(exception, warning, warned=warned):
            warned.append(warning)
        SCons.Warnings.warn = my_warn

        try:
            target = builder(env, 'mistaken1', sources='mistaken1.c')
            assert warned == ["Did you mean to use `source' instead of `sources'?"], warned
            del warned[:]

            target = builder(env, 'mistaken2', targets='mistaken2.c')
            assert warned == ["Did you mean to use `target' instead of `targets'?"], warned
            del warned[:]

            target = builder(env, 'mistaken3', targets='mistaken3', sources='mistaken3.c')
            assert "Did you mean to use `source' instead of `sources'?" in warned, warned
            assert "Did you mean to use `target' instead of `targets'?" in warned, warned
            del warned[:]
        finally:
            SCons.Warnings.warn = save_warn

    def test_action(self):
        """Test Builder creation

        Verify that we can retrieve the supplied action attribute.
        """
        builder = SCons.Builder.Builder(action="foo")
        assert builder.action.cmd_list == "foo"

        def func():
            pass
        builder = SCons.Builder.Builder(action=func)
        assert isinstance(builder.action, SCons.Action.FunctionAction)
        # Preserve the following so that the baseline test will fail.
        # Remove it in favor of the previous test at some convenient
        # point in the future.
        assert builder.action.execfunction == func

    def test_generator(self):
        """Test Builder creation given a generator function."""

        def generator():
            pass

        builder = SCons.Builder.Builder(generator=generator)
        assert builder.action.generator == generator

    def test_cmp(self):
        """Test simple comparisons of Builder objects
        """
        b1 = SCons.Builder.Builder(src_suffix = '.o')
        b2 = SCons.Builder.Builder(src_suffix = '.o')
        assert b1 == b2
        b3 = SCons.Builder.Builder(src_suffix = '.x')
        assert b1 != b3
        assert b2 != b3

    def test_target_factory(self):
        """Test a Builder that creates target nodes of a specified class
        """
        class Foo:
            pass
        def FooFactory(target):
            global Foo
            return Foo(target)
        builder = SCons.Builder.Builder(target_factory = FooFactory)
        assert builder.target_factory is FooFactory
        assert builder.source_factory is not FooFactory

    def test_source_factory(self):
        """Test a Builder that creates source nodes of a specified class
        """
        class Foo:
            pass
        def FooFactory(source):
            global Foo
            return Foo(source)
        builder = SCons.Builder.Builder(source_factory = FooFactory)
        assert builder.target_factory is not FooFactory
        assert builder.source_factory is FooFactory

    def test_splitext(self):
        """Test the splitext() method attached to a Builder."""
        b = SCons.Builder.Builder()
        assert b.splitext('foo') == ('foo','')
        assert b.splitext('foo.bar') == ('foo','.bar')
        assert b.splitext(os.path.join('foo.bar', 'blat')) == (os.path.join('foo.bar', 'blat'),'')

        class MyBuilder(SCons.Builder.BuilderBase):
            def splitext(self, path):
                return "called splitext()"

        b = MyBuilder()
        ret = b.splitext('xyz.c')
        assert ret == "called splitext()", ret

    def test_adjust_suffix(self):
        """Test how a Builder adjusts file suffixes
        """
        b = SCons.Builder.Builder()
        assert b.adjust_suffix('.foo') == '.foo'
        assert b.adjust_suffix('foo') == '.foo'
        assert b.adjust_suffix('$foo') == '$foo'

        class MyBuilder(SCons.Builder.BuilderBase):
            def adjust_suffix(self, suff):
                return "called adjust_suffix()"

        b = MyBuilder()
        ret = b.adjust_suffix('.foo')
        assert ret == "called adjust_suffix()", ret

    def test_prefix(self):
        """Test Builder creation with a specified target prefix

        Make sure that there is no '.' separator appended.
        """
        env = Environment()
        builder = SCons.Builder.Builder(prefix = 'lib.')
        assert builder.get_prefix(env) == 'lib.'
        builder = SCons.Builder.Builder(prefix = 'lib', action='')
        assert builder.get_prefix(env) == 'lib'
        tgt = builder(env, target = 'tgt1', source = 'src1')[0]
        assert tgt.get_internal_path() == 'libtgt1', \
                "Target has unexpected name: %s" % tgt.get_internal_path()
        tgt = builder(env, target = 'tgt2a tgt2b', source = 'src2')[0]
        assert tgt.get_internal_path() == 'libtgt2a tgt2b', \
                "Target has unexpected name: %s" % tgt.get_internal_path()
        tgt = builder(env, target = None, source = 'src3')[0]
        assert tgt.get_internal_path() == 'libsrc3', \
                "Target has unexpected name: %s" % tgt.get_internal_path()
        tgt = builder(env, target = None, source = 'lib/src4')[0]
        assert tgt.get_internal_path() == os.path.join('lib', 'libsrc4'), \
                "Target has unexpected name: %s" % tgt.get_internal_path()
        tgt = builder(env, target = 'lib/tgt5', source = 'lib/src5')[0]
        assert tgt.get_internal_path() == os.path.join('lib', 'libtgt5'), \
                "Target has unexpected name: %s" % tgt.get_internal_path()

        def gen_prefix(env, sources):
            return "gen_prefix() says " + env['FOO']
        my_env = Environment(FOO = 'xyzzy')
        builder = SCons.Builder.Builder(prefix = gen_prefix)
        assert builder.get_prefix(my_env) == "gen_prefix() says xyzzy"
        my_env['FOO'] = 'abracadabra'
        assert builder.get_prefix(my_env) == "gen_prefix() says abracadabra"

        def my_emit(env, sources):
            return env.subst('$EMIT')
        my_env = Environment(FOO = '.foo', EMIT = 'emit-')
        builder = SCons.Builder.Builder(prefix = {None   : 'default-',
                                                  '.in'  : 'out-',
                                                  '.x'   : 'y-',
                                                  '$FOO' : 'foo-',
                                                  '.zzz' : my_emit},
                                        action = '')
        tgt = builder(my_env, target = None, source = 'f1')[0]
        assert tgt.get_internal_path() == 'default-f1', tgt.get_internal_path()
        tgt = builder(my_env, target = None, source = 'f2.c')[0]
        assert tgt.get_internal_path() == 'default-f2', tgt.get_internal_path()
        tgt = builder(my_env, target = None, source = 'f3.in')[0]
        assert tgt.get_internal_path() == 'out-f3', tgt.get_internal_path()
        tgt = builder(my_env, target = None, source = 'f4.x')[0]
        assert tgt.get_internal_path() == 'y-f4', tgt.get_internal_path()
        tgt = builder(my_env, target = None, source = 'f5.foo')[0]
        assert tgt.get_internal_path() == 'foo-f5', tgt.get_internal_path()
        tgt = builder(my_env, target = None, source = 'f6.zzz')[0]
        assert tgt.get_internal_path() == 'emit-f6', tgt.get_internal_path()

    def test_set_suffix(self):
        """Test the set_suffix() method"""
        b = SCons.Builder.Builder(action='')
        env = Environment(XSUFFIX = '.x')

        s = b.get_suffix(env)
        assert s == '', s

        b.set_suffix('.foo')
        s = b.get_suffix(env)
        assert s == '.foo', s

        b.set_suffix('$XSUFFIX')
        s = b.get_suffix(env)
        assert s == '.x', s

    def test_src_suffix(self):
        """Test Builder creation with a specified source file suffix

        Make sure that the '.' separator is appended to the
        beginning if it isn't already present.
        """
        env = Environment(XSUFFIX = '.x', YSUFFIX = '.y')

        b1 = SCons.Builder.Builder(src_suffix = '.c', action='')
        assert b1.src_suffixes(env) == ['.c'], b1.src_suffixes(env)

        tgt = b1(env, target = 'tgt2', source = 'src2')[0]
        assert tgt.sources[0].get_internal_path() == 'src2.c', \
                "Source has unexpected name: %s" % tgt.sources[0].get_internal_path()

        tgt = b1(env, target = 'tgt3', source = 'src3a src3b')[0]
        assert len(tgt.sources) == 1
        assert tgt.sources[0].get_internal_path() == 'src3a src3b.c', \
                "Unexpected tgt.sources[0] name: %s" % tgt.sources[0].get_internal_path()

        b2 = SCons.Builder.Builder(src_suffix = '.2', src_builder = b1)
        r = sorted(b2.src_suffixes(env))
        assert r == ['.2', '.c'], r

        b3 = SCons.Builder.Builder(action = {'.3a' : '', '.3b' : ''})
        s = sorted(b3.src_suffixes(env))
        assert s == ['.3a', '.3b'], s

        b4 = SCons.Builder.Builder(src_suffix = '$XSUFFIX')
        assert b4.src_suffixes(env) == ['.x'], b4.src_suffixes(env)

        b5 = SCons.Builder.Builder(action = { '.y' : ''})
        assert b5.src_suffixes(env) == ['.y'], b5.src_suffixes(env)

    def test_srcsuffix_nonext(self):
        """Test target generation from non-extension source suffixes"""
        env = Environment()
        b6 = SCons.Builder.Builder(action = '',
                                   src_suffix='_src.a',
                                   suffix='.b')
        tgt = b6(env, target=None, source='foo_src.a')
        assert str(tgt[0]) == 'foo.b', str(tgt[0])

        b7 = SCons.Builder.Builder(action = '',
                                   src_suffix='_source.a',
                                   suffix='_obj.b')
        b8 = SCons.Builder.Builder(action = '',
                                   src_builder=b7,
                                   suffix='.c')
        tgt = b8(env, target=None, source='foo_source.a')
        assert str(tgt[0]) == 'foo_obj.c', str(tgt[0])
        src = env.fs.File('foo_source.a')
        tgt = b8(env, target=None, source=src)
        assert str(tgt[0]) == 'foo_obj.c', str(tgt[0])

        b9 = SCons.Builder.Builder(action={'_src.a' : 'srcaction'},
                                   suffix='.c')
        b9.add_action('_altsrc.b', 'altaction')
        tgt = b9(env, target=None, source='foo_altsrc.b')
        assert str(tgt[0]) == 'foo.c', str(tgt[0])

    def test_src_suffix_expansion(self):
        """Test handling source suffixes when an expansion is involved"""
        env = Environment(OBJSUFFIX = '.obj')

        b1 = SCons.Builder.Builder(action = '',
                                   src_suffix='.c',
                                   suffix='.obj')
        b2 = SCons.Builder.Builder(action = '',
                                   src_builder=b1,
                                   src_suffix='.obj',
                                   suffix='.exe')
        tgt = b2(env, target=None, source=['foo$OBJSUFFIX'])
        s = list(map(str, tgt[0].sources))
        assert s == ['foo.obj'], s

    def test_suffix(self):
        """Test Builder creation with a specified target suffix

        Make sure that the '.' separator is appended to the
        beginning if it isn't already present.
        """
        env = Environment()
        builder = SCons.Builder.Builder(suffix = '.o')
        assert builder.get_suffix(env) == '.o', builder.get_suffix(env)
        builder = SCons.Builder.Builder(suffix = 'o', action='')
        assert builder.get_suffix(env) == '.o', builder.get_suffix(env)
        tgt = builder(env, target = 'tgt3', source = 'src3')[0]
        assert tgt.get_internal_path() == 'tgt3.o', \
                "Target has unexpected name: %s" % tgt.get_internal_path()
        tgt = builder(env, target = 'tgt4a tgt4b', source = 'src4')[0]
        assert tgt.get_internal_path() == 'tgt4a tgt4b.o', \
                "Target has unexpected name: %s" % tgt.get_internal_path()
        tgt = builder(env, target = None, source = 'src5')[0]
        assert tgt.get_internal_path() == 'src5.o', \
                "Target has unexpected name: %s" % tgt.get_internal_path()

        def gen_suffix(env, sources):
            return "gen_suffix() says " + env['BAR']
        my_env = Environment(BAR = 'hocus pocus')
        builder = SCons.Builder.Builder(suffix = gen_suffix)
        assert builder.get_suffix(my_env) == "gen_suffix() says hocus pocus", builder.get_suffix(my_env)
        my_env['BAR'] = 'presto chango'
        assert builder.get_suffix(my_env) == "gen_suffix() says presto chango"

        def my_emit(env, sources):
            return env.subst('$EMIT')
        my_env = Environment(BAR = '.bar', EMIT = '.emit')
        builder = SCons.Builder.Builder(suffix = {None   : '.default',
                                                  '.in'  : '.out',
                                                  '.x'   : '.y',
                                                  '$BAR' : '.new',
                                                  '.zzz' : my_emit},
                                        action='')
        tgt = builder(my_env, target = None, source = 'f1')[0]
        assert tgt.get_internal_path() == 'f1.default', tgt.get_internal_path()
        tgt = builder(my_env, target = None, source = 'f2.c')[0]
        assert tgt.get_internal_path() == 'f2.default', tgt.get_internal_path()
        tgt = builder(my_env, target = None, source = 'f3.in')[0]
        assert tgt.get_internal_path() == 'f3.out', tgt.get_internal_path()
        tgt = builder(my_env, target = None, source = 'f4.x')[0]
        assert tgt.get_internal_path() == 'f4.y', tgt.get_internal_path()
        tgt = builder(my_env, target = None, source = 'f5.bar')[0]
        assert tgt.get_internal_path() == 'f5.new', tgt.get_internal_path()
        tgt = builder(my_env, target = None, source = 'f6.zzz')[0]
        assert tgt.get_internal_path() == 'f6.emit', tgt.get_internal_path()

    def test_single_source(self):
        """Test Builder with single_source flag set"""
        def func(target, source, env):
            """create the file"""
            with open(str(target[0]), "w"):
                pass
            if len(source) == 1 and len(target) == 1:
                env['CNT'][0] = env['CNT'][0] + 1

        env = Environment()
        infiles = []
        outfiles = []
        for i in range(10):
            infiles.append(test.workpath('%d.in' % i))
            outfiles.append(test.workpath('%d.out' % i))
            test.write(infiles[-1], "\n")
        builder = SCons.Builder.Builder(action=SCons.Action.Action(func,None),
                                        single_source = 1, suffix='.out')
        env['CNT'] = [0]
        tgt = builder(env, target=outfiles[0], source=infiles[0])[0]
        s = str(tgt)
        t = os.path.normcase(test.workpath('0.out'))
        assert os.path.normcase(s) == t, s
        tgt.prepare()
        tgt.build()
        assert env['CNT'][0] == 1, env['CNT'][0]
        tgt = builder(env, outfiles[1], infiles[1])[0]
        s = str(tgt)
        t = os.path.normcase(test.workpath('1.out'))
        assert os.path.normcase(s) == t, s
        tgt.prepare()
        tgt.build()
        assert env['CNT'][0] == 2
        tgts = builder(env, None, infiles[2:4])
        s = list(map(str, tgts))
        expect = [test.workpath('2.out'), test.workpath('3.out')]
        expect = list(map(os.path.normcase, expect))
        assert list(map(os.path.normcase, s)) == expect, s
        for t in tgts: t.prepare()
        tgts[0].build()
        tgts[1].build()
        assert env['CNT'][0] == 4
        try:
            tgt = builder(env, outfiles[4], infiles[4:6])
        except SCons.Errors.UserError:
            pass
        else:
            assert 0
        try:
            # The builder may output more than one target per input file.
            tgt = builder(env, outfiles[4:6], infiles[4:6])
        except SCons.Errors.UserError:
            pass
        else:
            assert 0


    def test_lists(self):
        """Testing handling lists of targets and source"""
        def function2(target, source, env, tlist = [outfile, outfile2], **kw):
            for t in target:
                with open(str(t), 'w') as f:
                    f.write("function2\n")
            for t in tlist:
                if t not in list(map(str, target)):
                    with open(t, 'w') as f:
                        f.write("function2\n")
            return 1

        env = Environment()
        builder = SCons.Builder.Builder(action = function2)

        tgts = builder(env, source=[])
        assert tgts == [], tgts

        tgts = builder(env, target = [outfile, outfile2], source = infile)
        for t in tgts:
            t.prepare()
        try:
            tgts[0].build()
        except SCons.Errors.BuildError:
            pass
        c = test.read(outfile, 'r')
        assert c == "function2\n", c
        c = test.read(outfile2, 'r')
        assert c == "function2\n", c

        sub1_out = test.workpath('sub1', 'out')
        sub2_out = test.workpath('sub2', 'out')

        def function3(target, source, env, tlist = [sub1_out, sub2_out]):
            for t in target:
                with open(str(t), 'w') as f:
                    f.write("function3\n")
            for t in tlist:
                if t not in list(map(str, target)):
                    with open(t, 'w') as f:
                        f.write("function3\n")
            return 1

        builder = SCons.Builder.Builder(action = function3)
        tgts = builder(env, target = [sub1_out, sub2_out], source = infile)
        for t in tgts:
            t.prepare()
        try:
            tgts[0].build()
        except SCons.Errors.BuildError:
            pass
        c = test.read(sub1_out, 'r')
        assert c == "function3\n", c
        c = test.read(sub2_out, 'r')
        assert c == "function3\n", c
        assert os.path.exists(test.workpath('sub1'))
        assert os.path.exists(test.workpath('sub2'))

    def test_src_builder(self):
        """Testing Builders with src_builder"""
        # These used to be MultiStepBuilder objects until we
        # eliminated it as a separate class
        env = Environment()
        builder1 = SCons.Builder.Builder(action='foo',
                                         src_suffix='.bar',
                                         suffix='.foo')
        builder2 = SCons.Builder.Builder(action=MyAction('act'),
                                         src_builder = builder1,
                                         src_suffix = '.foo')

        tgt = builder2(env, source=[])
        assert tgt == [], tgt

        sources = ['test.bar', 'test2.foo', 'test3.txt', 'test4']
        tgt = builder2(env, target='baz', source=sources)[0]
        s = str(tgt)
        assert s == 'baz', s
        s = list(map(str, tgt.sources))
        assert s == ['test.foo', 'test2.foo', 'test3.txt', 'test4.foo'], s
        s = list(map(str, tgt.sources[0].sources))
        assert s == ['test.bar'], s

        tgt = builder2(env, None, 'aaa.bar')[0]
        s = str(tgt)
        assert s == 'aaa', s
        s = list(map(str, tgt.sources))
        assert s == ['aaa.foo'], s
        s = list(map(str, tgt.sources[0].sources))
        assert s == ['aaa.bar'], s

        builder3 = SCons.Builder.Builder(action='bld3')
        assert builder3.src_builder is not builder1.src_builder

        builder4 = SCons.Builder.Builder(action='bld4',
                                         src_suffix='.i',
                                         suffix='_wrap.c')
        builder5 = SCons.Builder.Builder(action=MyAction('act'),
                                         src_builder=builder4,
                                         suffix='.obj',
                                         src_suffix='.c')
        builder6 = SCons.Builder.Builder(action=MyAction('act'),
                                         src_builder=builder5,
                                         suffix='.exe',
                                         src_suffix='.obj')
        tgt = builder6(env, 'test', 'test.i')[0]
        s = str(tgt)
        assert s == 'test.exe', s
        s = list(map(str, tgt.sources))
        assert s == ['test_wrap.obj'], s
        s = list(map(str, tgt.sources[0].sources))
        assert s == ['test_wrap.c'], s
        s = list(map(str, tgt.sources[0].sources[0].sources))
        assert s == ['test.i'], s

    def test_target_scanner(self):
        """Testing ability to set target and source scanners through a builder."""
        global instanced
        class TestScanner:
            pass
        tscan = TestScanner()
        sscan = TestScanner()
        env = Environment()
        builder = SCons.Builder.Builder(target_scanner=tscan,
                                        source_scanner=sscan,
                                        action='')
        tgt = builder(env, target='foo2', source='bar')[0]
        assert tgt.builder.target_scanner == tscan, tgt.builder.target_scanner
        assert tgt.builder.source_scanner == sscan, tgt.builder.source_scanner

        builder1 = SCons.Builder.Builder(action='foo',
                                         src_suffix='.bar',
                                         suffix='.foo')
        builder2 = SCons.Builder.Builder(action='foo',
                                         src_builder = builder1,
                                         target_scanner = tscan,
                                         source_scanner = tscan)
        tgt = builder2(env, target='baz2', source='test.bar test2.foo test3.txt')[0]
        assert tgt.builder.target_scanner == tscan, tgt.builder.target_scanner
        assert tgt.builder.source_scanner == tscan, tgt.builder.source_scanner

    def test_actual_scanner(self):
        """Test usage of actual Scanner objects."""

        import SCons.Scanner

        def func(self):
            pass

        scanner = SCons.Scanner.ScannerBase(func, name='fooscan')

        b1 = SCons.Builder.Builder(action='bld', target_scanner=scanner)
        b2 = SCons.Builder.Builder(action='bld', target_scanner=scanner)
        b3 = SCons.Builder.Builder(action='bld')

        assert b1 == b2
        assert b1 != b3

    def test_src_scanner(self):
        """Testing ability to set a source file scanner through a builder."""
        class TestScanner:
            def key(self, env):
                 return 'TestScannerkey'
            def instance(self, env):
                 return self
            def select(self, node):
                 return self
            name = 'TestScanner'
            def __str__(self):
                return self.name

        scanner = TestScanner()
        builder = SCons.Builder.Builder(action='action')

        # With no scanner specified, source_scanner and
        # backup_source_scanner are None.
        bar_y = MyNode('bar.y')
        env1 = Environment()
        tgt = builder(env1, target='foo1.x', source='bar.y')[0]
        src = tgt.sources[0]
        assert tgt.builder.target_scanner != scanner, tgt.builder.target_scanner
        assert tgt.builder.source_scanner is None, tgt.builder.source_scanner
        assert tgt.get_source_scanner(bar_y) is None, tgt.get_source_scanner(bar_y)
        assert not src.has_builder(), src.has_builder()
        s = src.get_source_scanner(bar_y)
        assert isinstance(s, SCons.Util.Null), repr(s)

        # An Environment that has suffix-specified SCANNERS should
        # provide a source scanner to the target.
        class EnvTestScanner:
            def key(self, env):
                 return '.y'
            def instance(self, env):
                 return self
            name = 'EnvTestScanner'
            def __str__(self):
                return self.name
            def select(self, node):
                return self
            def path(self, env, dir=None):
                return ()
            def __call__(self, node, env, path):
                return []
        env3 = Environment(SCANNERS = [EnvTestScanner()])
        env3.scanner = EnvTestScanner() # test env's version of SCANNERS
        tgt = builder(env3, target='foo2.x', source='bar.y')[0]
        src = tgt.sources[0]
        assert tgt.builder.target_scanner != scanner, tgt.builder.target_scanner
        assert not tgt.builder.source_scanner, tgt.builder.source_scanner
        assert tgt.get_source_scanner(bar_y), tgt.get_source_scanner(bar_y)
        assert str(tgt.get_source_scanner(bar_y)) == 'EnvTestScanner', tgt.get_source_scanner(bar_y)
        assert not src.has_builder(), src.has_builder()
        s = src.get_source_scanner(bar_y)
        assert isinstance(s, SCons.Util.Null), repr(s)

        # Can't simply specify the scanner as a builder argument; it's
        # global to all invocations of this builder.
        tgt = builder(env3, target='foo3.x', source='bar.y', source_scanner = scanner)[0]
        src = tgt.sources[0]
        assert tgt.builder.target_scanner != scanner, tgt.builder.target_scanner
        assert not tgt.builder.source_scanner, tgt.builder.source_scanner
        assert tgt.get_source_scanner(bar_y), tgt.get_source_scanner(bar_y)
        assert str(tgt.get_source_scanner(bar_y)) == 'EnvTestScanner', tgt.get_source_scanner(bar_y)
        assert not src.has_builder(), src.has_builder()
        s = src.get_source_scanner(bar_y)
        assert isinstance(s, SCons.Util.Null), s

        # Now use a builder that actually has scanners and ensure that
        # the target is set accordingly (using the specified scanner
        # instead of the Environment's scanner)
        builder = SCons.Builder.Builder(action='action',
                                        source_scanner=scanner,
                                        target_scanner=scanner)
        tgt = builder(env3, target='foo4.x', source='bar.y')[0]
        src = tgt.sources[0]
        assert tgt.builder.target_scanner == scanner, tgt.builder.target_scanner
        assert tgt.builder.source_scanner, tgt.builder.source_scanner
        assert tgt.builder.source_scanner == scanner, tgt.builder.source_scanner
        assert str(tgt.builder.source_scanner) == 'TestScanner', str(tgt.builder.source_scanner)
        assert tgt.get_source_scanner(bar_y), tgt.get_source_scanner(bar_y)
        assert tgt.get_source_scanner(bar_y) == scanner, tgt.get_source_scanner(bar_y)
        assert str(tgt.get_source_scanner(bar_y)) == 'TestScanner', tgt.get_source_scanner(bar_y)
        assert not src.has_builder(), src.has_builder()
        s = src.get_source_scanner(bar_y)
        assert isinstance(s, SCons.Util.Null), s



    def test_Builder_API(self):
        """Test Builder interface.

        Some of this is tested elsewhere in this file, but this is a
        quick collection of common operations on builders with various
        forms of component specifications."""

        builder = SCons.Builder.Builder()
        env = Environment(BUILDERS={'Bld':builder})

        r = builder.get_name(env)
        assert r == 'Bld', r
        r = builder.get_prefix(env)
        assert r == '', r
        r = builder.get_suffix(env)
        assert r == '', r
        r = builder.get_src_suffix(env)
        assert r == '', r
        r = builder.src_suffixes(env)
        assert r == [], r

        # src_suffix can be a single string or a list of strings
        # src_suffixes() caches its return value, so we use a new
        # Builder each time we do any of these tests

        bld = SCons.Builder.Builder()
        env = Environment(BUILDERS={'Bld':bld})

        bld.set_src_suffix('.foo')
        r = bld.get_src_suffix(env)
        assert r == '.foo', r
        r = bld.src_suffixes(env)
        assert r == ['.foo'], r

        bld = SCons.Builder.Builder()
        env = Environment(BUILDERS={'Bld':bld})

        bld.set_src_suffix(['.foo', '.bar'])
        r = bld.get_src_suffix(env)
        assert r == '.foo', r
        r = bld.src_suffixes(env)
        assert r == ['.foo', '.bar'], r

        bld = SCons.Builder.Builder()
        env = Environment(BUILDERS={'Bld':bld})

        bld.set_src_suffix(['.bar', '.foo'])
        r = bld.get_src_suffix(env)
        assert r == '.bar', r
        r = sorted(bld.src_suffixes(env))
        assert r == ['.bar', '.foo'], r

        # adjust_suffix normalizes the suffix, adding a `.' if needed

        r = builder.adjust_suffix('.foo')
        assert r == '.foo', r
        r = builder.adjust_suffix('_foo')
        assert r == '_foo', r
        r = builder.adjust_suffix('$foo')
        assert r == '$foo', r
        r = builder.adjust_suffix('foo')
        assert r == '.foo', r
        r = builder.adjust_suffix('f._$oo')
        assert r == '.f._$oo', r

        # prefix and suffix can be one of:
        #   1. a string (adjusted and env variables substituted),
        #   2. a function (passed (env,sources), returns suffix string)
        #   3. a dict of src_suffix:suffix settings, key==None is
        #      default suffix (special case of #2, so adjust_suffix
        #      not applied)

        builder = SCons.Builder.Builder(prefix='lib', suffix='foo')

        env = Environment(BUILDERS={'Bld':builder})
        r = builder.get_name(env)
        assert r == 'Bld', r
        r = builder.get_prefix(env)
        assert r == 'lib', r
        r = builder.get_suffix(env)
        assert r == '.foo', r

        mkpref = lambda env,sources: 'Lib'
        mksuff = lambda env,sources: '.Foo'
        builder = SCons.Builder.Builder(prefix=mkpref, suffix=mksuff)

        env = Environment(BUILDERS={'Bld':builder})
        r = builder.get_name(env)
        assert r == 'Bld', r
        r = builder.get_prefix(env)
        assert r == 'Lib', r
        r = builder.get_suffix(env)
        assert r == '.Foo', r

        builder = SCons.Builder.Builder(prefix='$PREF', suffix='$SUFF')

        env = Environment(BUILDERS={'Bld':builder},PREF="LIB",SUFF=".FOO")
        r = builder.get_name(env)
        assert r == 'Bld', r
        r = builder.get_prefix(env)
        assert r == 'LIB', r
        r = builder.get_suffix(env)
        assert r == '.FOO', r

        builder = SCons.Builder.Builder(prefix={None:'A_',
                                                '.C':'E_'},
                                        suffix={None:'.B',
                                                '.C':'.D'})

        env = Environment(BUILDERS={'Bld':builder})
        r = builder.get_name(env)
        assert r == 'Bld', r
        r = builder.get_prefix(env)
        assert r == 'A_', r
        r = builder.get_suffix(env)
        assert r == '.B', r
        r = builder.get_prefix(env, [MyNode('X.C')])
        assert r == 'E_', r
        r = builder.get_suffix(env, [MyNode('X.C')])
        assert r == '.D', r

        builder = SCons.Builder.Builder(prefix='A_', suffix={}, action={})
        env = Environment(BUILDERS={'Bld':builder})

        r = builder.get_name(env)
        assert r == 'Bld', r
        r = builder.get_prefix(env)
        assert r == 'A_', r
        r = builder.get_suffix(env)
        assert r is None, r
        r = builder.get_src_suffix(env)
        assert r == '', r
        r = builder.src_suffixes(env)
        assert r == [], r

        # Builder actions can be a string, a list, or a dictionary
        # whose keys are the source suffix.  The add_action()
        # specifies a new source suffix/action binding.

        builder = SCons.Builder.Builder(prefix='A_', suffix={}, action={})
        env = Environment(BUILDERS={'Bld':builder})
        builder.add_action('.src_sfx1', 'FOO')

        r = builder.get_name(env)
        assert r == 'Bld', r
        r = builder.get_prefix(env)
        assert r == 'A_', r
        r = builder.get_suffix(env)
        assert r is None, r
        r = builder.get_suffix(env, [MyNode('X.src_sfx1')])
        assert r is None, r
        r = builder.get_src_suffix(env)
        assert r == '.src_sfx1', r
        r = builder.src_suffixes(env)
        assert r == ['.src_sfx1'], r

        builder = SCons.Builder.Builder(prefix='A_', suffix={}, action={})
        env = Environment(BUILDERS={'Bld':builder})
        builder.add_action('.src_sfx1', 'FOO')
        builder.add_action('.src_sfx2', 'BAR')

        r = builder.get_name(env)
        assert r == 'Bld', r
        r = builder.get_prefix(env)
        assert r == 'A_', r
        r = builder.get_suffix(env)
        assert r is None, r
        r = builder.get_src_suffix(env)
        assert r == '.src_sfx1', r
        r = sorted(builder.src_suffixes(env))
        assert r == ['.src_sfx1', '.src_sfx2'], r


    def test_Builder_Args(self):
        """Testing passing extra args to a builder."""
        def buildFunc(target, source, env, s=self):
            s.foo=env['foo']
            s.bar=env['bar']
            assert env['CC'] == 'mycc'

        env=Environment(CC='cc')

        builder = SCons.Builder.Builder(action=buildFunc)
        tgt = builder(env, target='foo', source='bar', foo=1, bar=2, CC='mycc')[0]
        tgt.build()
        assert self.foo == 1, self.foo
        assert self.bar == 2, self.bar

    def test_emitter(self):
        """Test emitter functions."""
        def emit(target, source, env):
            foo = env.get('foo', 0)
            bar = env.get('bar', 0)
            for t in target:
                assert isinstance(t, MyNode)
                assert t.has_builder()
            for s in source:
                assert isinstance(s, MyNode)
            if foo:
                target.append("bar%d"%foo)
            if bar:
                source.append("baz")
            return ( target, source )

        env = Environment()
        builder = SCons.Builder.Builder(action='foo',
                                        emitter=emit,
                                        target_factory=MyNode,
                                        source_factory=MyNode)
        tgt = builder(env, target='foo2', source='bar')[0]
        assert str(tgt) == 'foo2', str(tgt)
        assert str(tgt.sources[0]) == 'bar', str(tgt.sources[0])

        tgt = builder(env, target='foo3', source='bar', foo=1)
        assert len(tgt) == 2, len(tgt)
        assert 'foo3' in list(map(str, tgt)), list(map(str, tgt))
        assert 'bar1' in list(map(str, tgt)), list(map(str, tgt))

        tgt = builder(env, target='foo4', source='bar', bar=1)[0]
        assert str(tgt) == 'foo4', str(tgt)
        assert len(tgt.sources) == 2, len(tgt.sources)
        assert 'baz' in list(map(str, tgt.sources)), list(map(str, tgt.sources))
        assert 'bar' in list(map(str, tgt.sources)), list(map(str, tgt.sources))

        env2=Environment(FOO=emit)
        builder2=SCons.Builder.Builder(action='foo',
                                       emitter="$FOO",
                                       target_factory=MyNode,
                                       source_factory=MyNode)

        builder2a=SCons.Builder.Builder(action='foo',
                                        emitter="$FOO",
                                        target_factory=MyNode,
                                        source_factory=MyNode)

        assert builder2 == builder2a, repr(builder2.__dict__) + "\n" + repr(builder2a.__dict__)

        tgt = builder2(env2, target='foo5', source='bar')[0]
        assert str(tgt) == 'foo5', str(tgt)
        assert str(tgt.sources[0]) == 'bar', str(tgt.sources[0])

        tgt = builder2(env2, target='foo6', source='bar', foo=2)
        assert len(tgt) == 2, len(tgt)
        assert 'foo6' in list(map(str, tgt)), list(map(str, tgt))
        assert 'bar2' in list(map(str, tgt)), list(map(str, tgt))

        tgt = builder2(env2, target='foo7', source='bar', bar=1)[0]
        assert str(tgt) == 'foo7', str(tgt)
        assert len(tgt.sources) == 2, len(tgt.sources)
        assert 'baz' in list(map(str, tgt.sources)), list(map(str, tgt.sources))
        assert 'bar' in list(map(str, tgt.sources)), list(map(str, tgt.sources))

    def test_emitter_preserve_builder(self):
        """Test an emitter not overwriting a newly-set builder"""
        env = Environment()

        new_builder = SCons.Builder.Builder(action='new')
        node = MyNode('foo8')
        new_node = MyNode('foo8.new')

        def emit(target, source, env, nb=new_builder, nn=new_node):
            for t in target:
                t.builder = nb
            return [nn], source

        builder=SCons.Builder.Builder(action='foo',
                                      emitter=emit,
                                      target_factory=MyNode,
                                      source_factory=MyNode)
        tgt = builder(env, target=node, source='bar')[0]
        assert tgt is new_node, tgt
        assert tgt.builder is builder, tgt.builder
        assert node.builder is new_builder, node.builder

    def test_emitter_suffix_map(self):
        """Test mapping file suffixes to emitter functions"""
        env = Environment()

        def emit4a(target, source, env):
            source = list(map(str, source))
            target = ['emit4a-' + x[:-3] for x in source]
            return (target, source)
        def emit4b(target, source, env):
            source = list(map(str, source))
            target = ['emit4b-' + x[:-3] for x in source]
            return (target, source)

        builder = SCons.Builder.Builder(action='foo',
                                        emitter={'.4a':emit4a,
                                                 '.4b':emit4b},
                                        target_factory=MyNode,
                                        source_factory=MyNode)
        tgt = builder(env, None, source='aaa.4a')[0]
        assert str(tgt) == 'emit4a-aaa', str(tgt)
        tgt = builder(env, None, source='bbb.4b')[0]
        assert str(tgt) == 'emit4b-bbb', str(tgt)
        tgt = builder(env, None, source='ccc.4c')[0]
        assert str(tgt) == 'ccc', str(tgt)

        def emit4c(target, source, env):
            source = list(map(str, source))
            target = ['emit4c-' + x[:-3] for x in source]
            return (target, source)

        builder.add_emitter('.4c', emit4c)
        tgt = builder(env, None, source='ccc.4c')[0]
        assert str(tgt) == 'emit4c-ccc', str(tgt)

    def test_emitter_function_list(self):
        """Test lists of emitter functions"""
        env = Environment()

        def emit1a(target, source, env):
            source = list(map(str, source))
            target = target + ['emit1a-' + x[:-2] for x in source]
            return (target, source)
        def emit1b(target, source, env):
            source = list(map(str, source))
            target = target + ['emit1b-' + x[:-2] for x in source]
            return (target, source)
        builder1 = SCons.Builder.Builder(action='foo',
                                         emitter=[emit1a, emit1b],
                                         node_factory=MyNode)

        tgts = builder1(env, target='target-1', source='aaa.1')
        tgts = list(map(str, tgts))
        assert tgts == ['target-1', 'emit1a-aaa', 'emit1b-aaa'], tgts

        # Test a list of emitter functions through the environment.
        def emit2a(target, source, env):
            source = list(map(str, source))
            target = target + ['emit2a-' + x[:-2] for x in source]
            return (target, source)
        def emit2b(target, source, env):
            source = list(map(str, source))
            target = target + ['emit2b-' + x[:-2] for x in source]
            return (target, source)
        builder2 = SCons.Builder.Builder(action='foo',
                                         emitter='$EMITTERLIST',
                                         node_factory=MyNode)

        env = Environment(EMITTERLIST = [emit2a, emit2b])

        tgts = builder2(env, target='target-2', source='aaa.2')
        tgts = list(map(str, tgts))
        assert tgts == ['target-2', 'emit2a-aaa', 'emit2b-aaa'], tgts

    def test_emitter_TARGET_SOURCE(self):
        """Test use of $TARGET and $SOURCE in emitter results"""

        env = SCons.Environment.Environment()

        def emit(target, source, env):
            return (target + ['${SOURCE}.s1', '${TARGET}.t1'],
                    source + ['${TARGET}.t2', '${SOURCE}.s2'])

        builder = SCons.Builder.Builder(action='foo',
                                        emitter = emit,
                                        node_factory = MyNode)

        targets = builder(env, target = 'TTT', source ='SSS')
        sources = targets[0].sources
        targets = list(map(str, targets))
        sources = list(map(str, sources))
        assert targets == ['TTT', 'SSS.s1', 'TTT.t1'], targets
        assert sources == ['SSS', 'TTT.t2', 'SSS.s2'], targets

    def test_no_target(self):
        """Test deducing the target from the source."""

        env = Environment()
        b = SCons.Builder.Builder(action='foo', suffix='.o')

        tgt = b(env, None, 'aaa')[0]
        assert str(tgt) == 'aaa.o', str(tgt)
        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
        assert str(tgt.sources[0]) == 'aaa', list(map(str, tgt.sources))

        tgt = b(env, None, 'bbb.c')[0]
        assert str(tgt) == 'bbb.o', str(tgt)
        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
        assert str(tgt.sources[0]) == 'bbb.c', list(map(str, tgt.sources))

        tgt = b(env, None, 'ccc.x.c')[0]
        assert str(tgt) == 'ccc.x.o', str(tgt)
        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
        assert str(tgt.sources[0]) == 'ccc.x.c', list(map(str, tgt.sources))

        tgt = b(env, None, ['d0.c', 'd1.c'])[0]
        assert str(tgt) == 'd0.o', str(tgt)
        assert len(tgt.sources) == 2,  list(map(str, tgt.sources))
        assert str(tgt.sources[0]) == 'd0.c', list(map(str, tgt.sources))
        assert str(tgt.sources[1]) == 'd1.c', list(map(str, tgt.sources))

        tgt = b(env, target = None, source='eee')[0]
        assert str(tgt) == 'eee.o', str(tgt)
        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
        assert str(tgt.sources[0]) == 'eee', list(map(str, tgt.sources))

        tgt = b(env, target = None, source='fff.c')[0]
        assert str(tgt) == 'fff.o', str(tgt)
        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
        assert str(tgt.sources[0]) == 'fff.c', list(map(str, tgt.sources))

        tgt = b(env, target = None, source='ggg.x.c')[0]
        assert str(tgt) == 'ggg.x.o', str(tgt)
        assert len(tgt.sources) == 1, list(map(str, tgt.sources))
        assert str(tgt.sources[0]) == 'ggg.x.c', list(map(str, tgt.sources))

        tgt = b(env, target = None, source=['h0.c', 'h1.c'])[0]
        assert str(tgt) == 'h0.o', str(tgt)
        assert len(tgt.sources) == 2,  list(map(str, tgt.sources))
        assert str(tgt.sources[0]) == 'h0.c', list(map(str, tgt.sources))
        assert str(tgt.sources[1]) == 'h1.c', list(map(str, tgt.sources))

        w = b(env, target='i0.w', source=['i0.x'])[0]
        y = b(env, target='i1.y', source=['i1.z'])[0]
        tgt = b(env, None, source=[w, y])[0]
        assert str(tgt) == 'i0.o', str(tgt)
        assert len(tgt.sources) == 2, list(map(str, tgt.sources))
        assert str(tgt.sources[0]) == 'i0.w', list(map(str, tgt.sources))
        assert str(tgt.sources[1]) == 'i1.y', list(map(str, tgt.sources))

    def test_get_name(self):
        """Test getting name of builder.

        Each type of builder should return its environment-specific
        name when queried appropriately.  """

        b1 = SCons.Builder.Builder(action='foo', suffix='.o')
        b2 = SCons.Builder.Builder(action='foo', suffix='.c')
        b3 = SCons.Builder.Builder(action='bar', src_suffix = '.foo',
                                                 src_builder = b1)
        b4 = SCons.Builder.Builder(action={})
        b5 = SCons.Builder.Builder(action='foo', name='builder5')
        b6 = SCons.Builder.Builder(action='foo')
        assert isinstance(b4, SCons.Builder.CompositeBuilder)
        assert isinstance(b4.action, SCons.Action.CommandGeneratorAction)

        env = Environment(BUILDERS={'bldr1': b1,
                                    'bldr2': b2,
                                    'bldr3': b3,
                                    'bldr4': b4})
        env2 = Environment(BUILDERS={'B1': b1,
                                     'B2': b2,
                                     'B3': b3,
                                     'B4': b4})
        # With no name, get_name will return the class.  Allow
        # for caching...
        b6_names = [
            'SCons.Builder.BuilderBase',
            "<class 'SCons.Builder.BuilderBase'>",
            'SCons.Memoize.BuilderBase',
            "<class 'SCons.Memoize.BuilderBase'>",
        ]

        assert b1.get_name(env) == 'bldr1', b1.get_name(env)
        assert b2.get_name(env) == 'bldr2', b2.get_name(env)
        assert b3.get_name(env) == 'bldr3', b3.get_name(env)
        assert b4.get_name(env) == 'bldr4', b4.get_name(env)
        assert b5.get_name(env) == 'builder5', b5.get_name(env)
        assert b6.get_name(env) in b6_names, b6.get_name(env)

        assert b1.get_name(env2) == 'B1', b1.get_name(env2)
        assert b2.get_name(env2) == 'B2', b2.get_name(env2)
        assert b3.get_name(env2) == 'B3', b3.get_name(env2)
        assert b4.get_name(env2) == 'B4', b4.get_name(env2)
        assert b5.get_name(env2) == 'builder5', b5.get_name(env2)
        assert b6.get_name(env2) in b6_names, b6.get_name(env2)

        assert b5.get_name(None) == 'builder5', b5.get_name(None)
        assert b6.get_name(None) in b6_names, b6.get_name(None)

        # This test worked before adding batch builders, but we must now
        # be able to disambiguate a CompositeAction into a more specific
        # action based on file suffix at call time.  Leave this commented
        # out (for now) in case this reflects a real-world use case that
        # we must accomodate and we want to resurrect this test.
        #tgt = b4(env, target = 'moo', source='cow')
        #assert tgt[0].builder.get_name(env) == 'bldr4'

class CompositeBuilderTestCase(unittest.TestCase):

    def setUp(self):
        def func_action(target, source, env):
            return 0

        builder = SCons.Builder.Builder(action={ '.foo' : func_action,
                                                 '.bar' : func_action})

        self.func_action = func_action
        self.builder = builder

    def test___init__(self):
        """Test CompositeBuilder creation"""
        env = Environment()
        builder = SCons.Builder.Builder(action={})

        tgt = builder(env, source=[])
        assert tgt == [], tgt

        assert isinstance(builder, SCons.Builder.CompositeBuilder)
        assert isinstance(builder.action, SCons.Action.CommandGeneratorAction)

    def test_target_action(self):
        """Test CompositeBuilder setting of target builder actions"""
        env = Environment()
        builder = self.builder

        tgt = builder(env, target='test1', source='test1.foo')[0]
        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)
        assert tgt.builder.action is builder.action

        tgt = builder(env, target='test2', source='test1.bar')[0]
        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)
        assert tgt.builder.action is builder.action

    def test_multiple_suffix_error(self):
        """Test the CompositeBuilder multiple-source-suffix error"""
        env = Environment()
        builder = self.builder

        flag = 0
        try:
            builder(env, target='test3', source=['test2.bar', 'test1.foo'])[0]
        except SCons.Errors.UserError as e:
            flag = 1
            err = e
        assert flag, "UserError should be thrown when we call a builder with files of different suffixes."
        expect = "While building `['test3']' from `test1.foo': Cannot build multiple sources with different extensions: .bar, .foo"
        assert str(err) == expect, err

    def test_source_ext_match(self):
        """Test the CompositeBuilder source_ext_match argument"""
        env = Environment()
        func_action = self.func_action
        builder = SCons.Builder.Builder(action={ '.foo' : func_action,
                                                 '.bar' : func_action},
                                        source_ext_match = None)

        tgt = builder(env, target='test3', source=['test2.bar', 'test1.foo'])[0]
        tgt.build()

    def test_suffix_variable(self):
        """Test CompositeBuilder defining action suffixes through a variable"""
        env = Environment(BAR_SUFFIX = '.BAR2', FOO_SUFFIX = '.FOO2')
        func_action = self.func_action
        builder = SCons.Builder.Builder(action={ '.foo' : func_action,
                                                 '.bar' : func_action,
                                                 '$BAR_SUFFIX' : func_action,
                                                 '$FOO_SUFFIX' : func_action })

        tgt = builder(env, target='test4', source=['test4.BAR2'])[0]
        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)
        try:
            tgt.build()
            flag = 1
        except SCons.Errors.UserError as e:
            print(e)
            flag = 0
        assert flag, "It should be possible to define actions in composite builders using variables."
        env['FOO_SUFFIX'] = '.BAR2'
        builder.add_action('$NEW_SUFFIX', func_action)
        flag = 0
        try:
            builder(env, target='test5', source=['test5.BAR2'])[0]
        except SCons.Errors.UserError:
            flag = 1
        assert flag, "UserError should be thrown when we call a builder with ambigous suffixes."

    def test_src_builder(self):
        """Test CompositeBuilder's use of a src_builder"""
        env = Environment()

        foo_bld = SCons.Builder.Builder(action = 'a-foo',
                                        src_suffix = '.ina',
                                        suffix = '.foo')
        assert isinstance(foo_bld, SCons.Builder.BuilderBase)
        builder = SCons.Builder.Builder(action = { '.foo' : 'foo',
                                                   '.bar' : 'bar' },
                                        src_builder = foo_bld)
        assert isinstance(builder, SCons.Builder.CompositeBuilder)
        assert isinstance(builder.action, SCons.Action.CommandGeneratorAction)

        tgt = builder(env, target='t1', source='t1a.ina t1b.ina')[0]
        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)

        tgt = builder(env, target='t2', source='t2a.foo t2b.ina')[0]
        assert isinstance(tgt.builder, SCons.Builder.BuilderBase), tgt.builder.__dict__

        bar_bld = SCons.Builder.Builder(action = 'a-bar',
                                        src_suffix = '.inb',
                                        suffix = '.bar')
        assert isinstance(bar_bld, SCons.Builder.BuilderBase)
        builder = SCons.Builder.Builder(action = { '.foo' : 'foo'},
                                        src_builder = [foo_bld, bar_bld])
        assert isinstance(builder, SCons.Builder.CompositeBuilder)
        assert isinstance(builder.action, SCons.Action.CommandGeneratorAction)

        builder.add_action('.bar', 'bar')

        tgt = builder(env, target='t3-foo', source='t3a.foo t3b.ina')[0]
        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)

        tgt = builder(env, target='t3-bar', source='t3a.bar t3b.inb')[0]
        assert isinstance(tgt.builder, SCons.Builder.BuilderBase)

        flag = 0
        try:
            builder(env, target='t5', source=['test5a.foo', 'test5b.inb'])[0]
        except SCons.Errors.UserError as e:
            flag = 1
            err = e
        assert flag, "UserError should be thrown when we call a builder with files of different suffixes."
        expect = "While building `['t5']' from `test5b.bar': Cannot build multiple sources with different extensions: .foo, .bar"
        assert str(err) == expect, err

        flag = 0
        try:
            builder(env, target='t6', source=['test6a.bar', 'test6b.ina'])[0]
        except SCons.Errors.UserError as e:
            flag = 1
            err = e
        assert flag, "UserError should be thrown when we call a builder with files of different suffixes."
        expect = "While building `['t6']' from `test6b.foo': Cannot build multiple sources with different extensions: .bar, .foo"
        assert str(err) == expect, err

        flag = 0
        try:
            builder(env, target='t4', source=['test4a.ina', 'test4b.inb'])[0]
        except SCons.Errors.UserError as e:
            flag = 1
            err = e
        assert flag, "UserError should be thrown when we call a builder with files of different suffixes."
        expect = "While building `['t4']' from `test4b.bar': Cannot build multiple sources with different extensions: .foo, .bar"
        assert str(err) == expect, err

        flag = 0
        try:
            builder(env, target='t7', source=[env.fs.File('test7')])[0]
        except SCons.Errors.UserError as e:
            flag = 1
            err = e
        assert flag, "UserError should be thrown when we call a builder with files of different suffixes."
        expect = "While building `['t7']': Cannot deduce file extension from source files: ['test7']"
        assert str(err) == expect, err

        flag = 0
        try:
            builder(env, target='t8', source=['test8.unknown'])[0]
        except SCons.Errors.UserError as e:
            flag = 1
            err = e
        assert flag, "UserError should be thrown when we call a builder target with an unknown suffix."
        expect = "While building `['t8']' from `['test8.unknown']': Don't know how to build from a source file with suffix `.unknown'.  Expected a suffix in this list: ['.foo', '.bar']."
        assert str(err) == expect, err

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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/CacheDir.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""CacheDir support
"""

import atexit
import json
import os
import stat
import sys
import uuid

import SCons.Action
import SCons.Errors
import SCons.Warnings
import SCons

cache_enabled = True
cache_debug = False
cache_force = False
cache_show = False
cache_readonly = False
cache_tmp_uuid = uuid.uuid4().hex

def CacheRetrieveFunc(target, source, env):
    t = target[0]
    fs = t.fs
    cd = env.get_CacheDir()
    cd.requests += 1
    cachedir, cachefile = cd.cachepath(t)
    if not fs.exists(cachefile):
        cd.CacheDebug('CacheRetrieve(%s):  %s not in cache\n', t, cachefile)
        return 1
    cd.hits += 1
    cd.CacheDebug('CacheRetrieve(%s):  retrieving from %s\n', t, cachefile)
    if SCons.Action.execute_actions:
        if fs.islink(cachefile):
            fs.symlink(fs.readlink(cachefile), t.get_internal_path())
        else:
            cd.copy_from_cache(env, cachefile, t.get_internal_path())
            try:
                os.utime(cachefile, None)
            except OSError:
                pass
        st = fs.stat(cachefile)
        fs.chmod(t.get_internal_path(), stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
    return 0

def CacheRetrieveString(target, source, env):
    t = target[0]
    fs = t.fs
    cd = env.get_CacheDir()
    cachedir, cachefile = cd.cachepath(t)
    if t.fs.exists(cachefile):
        return "Retrieved `%s' from cache" % t.get_internal_path()
    return None

CacheRetrieve = SCons.Action.Action(CacheRetrieveFunc, CacheRetrieveString)

CacheRetrieveSilent = SCons.Action.Action(CacheRetrieveFunc, None)

def CachePushFunc(target, source, env):
    if cache_readonly:
        return

    t = target[0]
    if t.nocache:
        return
    fs = t.fs
    cd = env.get_CacheDir()
    cachedir, cachefile = cd.cachepath(t)
    if fs.exists(cachefile):
        # Don't bother copying it if it's already there.  Note that
        # usually this "shouldn't happen" because if the file already
        # existed in cache, we'd have retrieved the file from there,
        # not built it.  This can happen, though, in a race, if some
        # other person running the same build pushes their copy to
        # the cache after we decide we need to build it but before our
        # build completes.
        cd.CacheDebug('CachePush(%s):  %s already exists in cache\n', t, cachefile)
        return

    cd.CacheDebug('CachePush(%s):  pushing to %s\n', t, cachefile)

    tempfile = "%s.tmp%s"%(cachefile,cache_tmp_uuid)
    errfmt = "Unable to copy %s to cache. Cache file is %s"

    try:
        fs.makedirs(cachedir, exist_ok=True)
    except OSError:
        msg = errfmt % (str(target), cachefile)
        raise SCons.Errors.SConsEnvironmentError(msg)
    try:
        if fs.islink(t.get_internal_path()):
            fs.symlink(fs.readlink(t.get_internal_path()), tempfile)
        else:
            cd.copy_to_cache(env, t.get_internal_path(), tempfile)
        fs.rename(tempfile, cachefile)

    except EnvironmentError:
        # It's possible someone else tried writing the file at the
        # same time we did, or else that there was some problem like
        # the CacheDir being on a separate file system that's full.
        # In any case, inability to push a file to cache doesn't affect
        # the correctness of the build, so just print a warning.
        msg = errfmt % (str(t), cachefile)
        cd.CacheDebug(errfmt + '\n', str(t), cachefile)
        SCons.Warnings.warn(SCons.Warnings.CacheWriteErrorWarning, msg)

CachePush = SCons.Action.Action(CachePushFunc, None)


class CacheDir:

    def __init__(self, path):
        """
        Initialize a CacheDir object.

        The cache configuration is stored in the object. It
        is read from the config file in the supplied path if
        one exists,  if not the config file is created and
        the default config is written, as well as saved in the object.
        """
        self.requests = 0
        self.hits = 0
        self.path = path
        self.current_cache_debug = None
        self.debugFP = None
        self.config = dict()
        if path is None:
            return

        self._readconfig(path)


    def _readconfig(self, path):
        """
        Read the cache config.

        If directory or config file do not exist, create.  Take advantage
        of Py3 capability in os.makedirs() and in file open(): just try
        the operation and handle failure appropriately.

        Omit the check for old cache format, assume that's old enough
        there will be none of those left to worry about.

        :param path: path to the cache directory
        """
        config_file = os.path.join(path, 'config')
        try:
            os.makedirs(path, exist_ok=True)
        except FileExistsError:
            pass
        except OSError:
            msg = "Failed to create cache directory " + path
            raise SCons.Errors.SConsEnvironmentError(msg)

        try:
            with open(config_file, 'x') as config:
                self.config['prefix_len'] = 2
                try:
                    json.dump(self.config, config)
                except Exception:
                    msg = "Failed to write cache configuration for " + path
                    raise SCons.Errors.SConsEnvironmentError(msg)
        except FileExistsError:
            try:
                with open(config_file) as config:
                    self.config = json.load(config)
            except ValueError:
                msg = "Failed to read cache configuration for " + path
                raise SCons.Errors.SConsEnvironmentError(msg)

    def CacheDebug(self, fmt, target, cachefile):
        if cache_debug != self.current_cache_debug:
            if cache_debug == '-':
                self.debugFP = sys.stdout
            elif cache_debug:
                def debug_cleanup(debugFP):
                    debugFP.close()

                self.debugFP = open(cache_debug, 'w')
                atexit.register(debug_cleanup, self.debugFP)
            else:
                self.debugFP = None
            self.current_cache_debug = cache_debug
        if self.debugFP:
            self.debugFP.write(fmt % (target, os.path.split(cachefile)[1]))
            self.debugFP.write("requests: %d, hits: %d, misses: %d, hit rate: %.2f%%\n" %
                               (self.requests, self.hits, self.misses, self.hit_ratio))

    @classmethod
    def copy_from_cache(cls, env, src, dst):
        if env.cache_timestamp_newer:
            return env.fs.copy(src, dst)
        else:
            return env.fs.copy2(src, dst)

    @classmethod
    def copy_to_cache(cls, env, src, dst):
        try:
            result = env.fs.copy2(src, dst)
            fs = env.File(src).fs
            st = fs.stat(src)
            fs.chmod(dst, stat.S_IMODE(st[stat.ST_MODE]) | stat.S_IWRITE)
            return result
        except AttributeError as ex:
            raise EnvironmentError from ex

    @property
    def hit_ratio(self):
        return (100.0 * self.hits / self.requests if self.requests > 0 else 100)

    @property
    def misses(self):
        return self.requests - self.hits

    def is_enabled(self):
        return cache_enabled and self.path is not None

    def is_readonly(self):
        return cache_readonly

    def get_cachedir_csig(self, node):
        cachedir, cachefile = self.cachepath(node)
        if cachefile and os.path.exists(cachefile):
            return SCons.Util.hash_file_signature(cachefile, SCons.Node.FS.File.hash_chunksize)

    def cachepath(self, node):
        """
        """
        if not self.is_enabled():
            return None, None

        sig = node.get_cachedir_bsig()

        subdir = sig[:self.config['prefix_len']].upper()

        dir = os.path.join(self.path, subdir)
        return dir, os.path.join(dir, sig)

    def retrieve(self, node):
        """
        This method is called from multiple threads in a parallel build,
        so only do thread safe stuff here. Do thread unsafe stuff in
        built().

        Note that there's a special trick here with the execute flag
        (one that's not normally done for other actions).  Basically
        if the user requested a no_exec (-n) build, then
        SCons.Action.execute_actions is set to 0 and when any action
        is called, it does its showing but then just returns zero
        instead of actually calling the action execution operation.
        The problem for caching is that if the file does NOT exist in
        cache then the CacheRetrieveString won't return anything to
        show for the task, but the Action.__call__ won't call
        CacheRetrieveFunc; instead it just returns zero, which makes
        the code below think that the file *was* successfully
        retrieved from the cache, therefore it doesn't do any
        subsequent building.  However, the CacheRetrieveString didn't
        print anything because it didn't actually exist in the cache,
        and no more build actions will be performed, so the user just
        sees nothing.  The fix is to tell Action.__call__ to always
        execute the CacheRetrieveFunc and then have the latter
        explicitly check SCons.Action.execute_actions itself.
        """
        if not self.is_enabled():
            return False

        env = node.get_build_env()
        if cache_show:
            if CacheRetrieveSilent(node, [], env, execute=1) == 0:
                node.build(presub=0, execute=0)
                return True
        else:
            if CacheRetrieve(node, [], env, execute=1) == 0:
                return True

        return False

    def push(self, node):
        if self.is_readonly() or not self.is_enabled():
            return
        return CachePush(node, [], node.get_build_env())

    def push_if_forced(self, node):
        if cache_force:
            return self.push(node)

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/CacheDirTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import os.path
import shutil
import sys
import unittest
import tempfile
import stat

from TestCmd import TestCmd

import SCons.CacheDir

built_it = None

class Action:
    def __call__(self, targets, sources, env, **kw):
        global built_it
        if kw.get('execute', 1):
            built_it = 1
        return 0
    def genstring(self, target, source, env):
        return str(self)
    def get_contents(self, target, source, env):
        return bytearray('','utf-8')

class Builder:
    def __init__(self, environment, action):
        self.env = environment
        self.action = action
        self.overrides = {}
        self.source_scanner = None
        self.target_scanner = None

class Environment:
    def __init__(self, cachedir):
        self.cachedir = cachedir
    def Override(self, overrides):
        return self
    def get_CacheDir(self):
        return self.cachedir

class BaseTestCase(unittest.TestCase):
    """
    Base fixtures common to our other unittest classes.
    """
    def setUp(self):
        self.test = TestCmd(workdir='')

        import SCons.Node.FS
        self.fs = SCons.Node.FS.FS()

        self._CacheDir = SCons.CacheDir.CacheDir('cache')

    def File(self, name, bsig=None, action=Action()):
        node = self.fs.File(name)
        node.builder_set(Builder(Environment(self._CacheDir), action))
        if bsig:
            node.cachesig = bsig
            #node.binfo = node.BuildInfo(node)
            #node.binfo.ninfo.bsig = bsig
        return node

    def tearDown(self):
        os.remove(os.path.join(self._CacheDir.path, 'config'))
        os.rmdir(self._CacheDir.path)
        # Should that be shutil.rmtree?

class CacheDirTestCase(BaseTestCase):
    """
    Test calling CacheDir code directly.
    """
    def test_cachepath(self):
        """Test the cachepath() method"""

        # Verify how the cachepath() method determines the name
        # of the file in cache.
        def my_collect(list, hash_format=None):
            return list[0]
        save_collect = SCons.Util.hash_collect
        SCons.Util.hash_collect = my_collect

        try:
            name = 'a_fake_bsig'
            f5 = self.File("cd.f5", name)
            result = self._CacheDir.cachepath(f5)
            len = self._CacheDir.config['prefix_len']
            dirname = os.path.join('cache', name.upper()[:len])
            filename = os.path.join(dirname, name)
            assert result == (dirname, filename), result
        finally:
            SCons.Util.hash_collect = save_collect

class ExceptionTestCase(unittest.TestCase):
    """Test that the correct exceptions are thrown by CacheDir."""

    # Don't inherit from BaseTestCase, we're by definition trying to
    # break things so we really want a clean slate for each test.
    def setUp(self):
        self.tmpdir = tempfile.mkdtemp()
        self._CacheDir = SCons.CacheDir.CacheDir(self.tmpdir)

    def tearDown(self):
        shutil.rmtree(self.tmpdir)

    @unittest.skipIf(sys.platform.startswith("win"), "This fixture will not trigger an OSError on Windows")
    def test_throws_correct_on_OSError(self):
        """Test that the correct error is thrown when cache directory cannot be created."""
        privileged_dir = os.path.join(self.tmpdir, "privileged")
        try:
            os.mkdir(privileged_dir)
            os.chmod(privileged_dir, stat.S_IREAD)
            cd = SCons.CacheDir.CacheDir(os.path.join(privileged_dir, "cache"))
            assert False, "Should have raised exception and did not"
        except SCons.Errors.SConsEnvironmentError as e:
            assert str(e) == "Failed to create cache directory {}".format(os.path.join(privileged_dir, "cache"))
        finally:
            os.chmod(privileged_dir, stat.S_IWRITE | stat.S_IEXEC | stat.S_IREAD)
            shutil.rmtree(privileged_dir)


    def test_throws_correct_when_failed_to_write_configfile(self):
        class Unserializable:
            """A class which the JSON should not be able to serialize"""

            def __init__(self, oldconfig):
                self.something = 1 # Make the object unserializable
                # Pretend to be the old config just enough
                self.__dict__["prefix_len"] = oldconfig["prefix_len"]

            def __getitem__(self, name, default=None):
                if name == "prefix_len":
                    return self.__dict__["prefix_len"]
                else:
                    return None

            def __setitem__(self, name, value):
                self.__dict__[name] = value

        oldconfig = self._CacheDir.config
        self._CacheDir.config = Unserializable(oldconfig)
        # Remove the config file that got created on object creation
        # so that _readconfig* will try to rewrite it
        old_config = os.path.join(self._CacheDir.path, "config")
        os.remove(old_config)
        
        try:
            self._CacheDir._readconfig(self._CacheDir.path)
            assert False, "Should have raised exception and did not"
        except SCons.Errors.SConsEnvironmentError as e:
            assert str(e) == "Failed to write cache configuration for {}".format(self._CacheDir.path)

    def test_raise_environment_error_on_invalid_json(self):
        config_file = os.path.join(self._CacheDir.path, "config")
        with open(config_file, "r") as cfg:
            content = cfg.read()
        # This will make JSON load raise a ValueError
        content += "{}"
        with open(config_file, "w") as cfg:
            cfg.write(content)

        try:
            # Construct a new cache dir that will try to read the invalid config
            new_cache_dir = SCons.CacheDir.CacheDir(self._CacheDir.path)
            assert False, "Should have raised exception and did not"
        except SCons.Errors.SConsEnvironmentError as e:
            assert str(e) == "Failed to read cache configuration for {}".format(self._CacheDir.path)

class FileTestCase(BaseTestCase):
    """
    Test calling CacheDir code through Node.FS.File interfaces.
    """
    # These tests were originally in Nodes/FSTests.py and got moved
    # when the CacheDir support was refactored into its own module.
    # Look in the history for Node/FSTests.py if any of this needs
    # to be re-examined.
    def retrieve_succeed(self, target, source, env, execute=1):
        self.retrieved.append(target)
        return 0

    def retrieve_fail(self, target, source, env, execute=1):
        self.retrieved.append(target)
        return 1

    def push(self, target, source, env):
        self.pushed.append(target)
        return 0

    def test_CacheRetrieve(self):
        """Test the CacheRetrieve() function"""

        save_CacheRetrieve = SCons.CacheDir.CacheRetrieve
        self.retrieved = []

        f1 = self.File("cd.f1")
        try:
            SCons.CacheDir.CacheRetrieve = self.retrieve_succeed
            self.retrieved = []
            built_it = None

            r = f1.retrieve_from_cache()
            assert r == 1, r
            assert self.retrieved == [f1], self.retrieved
            assert built_it is None, built_it

            SCons.CacheDir.CacheRetrieve = self.retrieve_fail
            self.retrieved = []
            built_it = None

            r = f1.retrieve_from_cache()
            assert not r, r
            assert self.retrieved == [f1], self.retrieved
            assert built_it is None, built_it
        finally:
            SCons.CacheDir.CacheRetrieve = save_CacheRetrieve

    def test_CacheRetrieveSilent(self):
        """Test the CacheRetrieveSilent() function"""

        save_CacheRetrieveSilent = SCons.CacheDir.CacheRetrieveSilent

        SCons.CacheDir.cache_show = 1

        f2 = self.File("cd.f2", 'f2_bsig')
        try:
            SCons.CacheDir.CacheRetrieveSilent = self.retrieve_succeed
            self.retrieved = []
            built_it = None

            r = f2.retrieve_from_cache()
            assert r == 1, r
            assert self.retrieved == [f2], self.retrieved
            assert built_it is None, built_it

            SCons.CacheDir.CacheRetrieveSilent = self.retrieve_fail
            self.retrieved = []
            built_it = None

            r = f2.retrieve_from_cache()
            assert r is False, r
            assert self.retrieved == [f2], self.retrieved
            assert built_it is None, built_it
        finally:
            SCons.CacheDir.CacheRetrieveSilent = save_CacheRetrieveSilent

    def test_CachePush(self):
        """Test the CachePush() function"""

        save_CachePush = SCons.CacheDir.CachePush

        SCons.CacheDir.CachePush = self.push

        try:
            self.pushed = []

            cd_f3 = self.test.workpath("cd.f3")
            f3 = self.File(cd_f3)
            f3.push_to_cache()
            assert self.pushed == [], self.pushed
            self.test.write(cd_f3, "cd.f3\n")
            f3.push_to_cache()
            assert self.pushed == [f3], self.pushed

            self.pushed = []

            cd_f4 = self.test.workpath("cd.f4")
            f4 = self.File(cd_f4)
            f4.visited()
            assert self.pushed == [], self.pushed
            self.test.write(cd_f4, "cd.f4\n")
            f4.clear()
            f4.visited()
            assert self.pushed == [], self.pushed
            SCons.CacheDir.cache_force = 1
            f4.clear()
            f4.visited()
            assert self.pushed == [f4], self.pushed
        finally:
            SCons.CacheDir.CachePush = save_CachePush

    def test_warning(self):
        """Test raising a warning if we can't copy a file to cache."""

        test = TestCmd(workdir='')

        save_copy2 = shutil.copy2
        def copy2(src, dst):
            raise OSError
        shutil.copy2 = copy2
        save_mkdir = os.mkdir
        def mkdir(dir, mode=0):
            pass
        os.mkdir = mkdir
        old_warn_exceptions = SCons.Warnings.warningAsException(1)
        SCons.Warnings.enableWarningClass(SCons.Warnings.CacheWriteErrorWarning)

        try:
            cd_f7 = self.test.workpath("cd.f7")
            self.test.write(cd_f7, "cd.f7\n")
            f7 = self.File(cd_f7, 'f7_bsig')

            warn_caught = 0
            try:
                f7.push_to_cache()
            except SCons.Errors.BuildError as e:
                assert e.exc_info[0] == SCons.Warnings.CacheWriteErrorWarning
                warn_caught = 1
            assert warn_caught
        finally:
            shutil.copy2 = save_copy2
            os.mkdir = save_mkdir
            SCons.Warnings.warningAsException(old_warn_exceptions)
            SCons.Warnings.suppressWarningClass(SCons.Warnings.CacheWriteErrorWarning)

    def test_no_strfunction(self):
        """Test handling no strfunction() for an action."""

        save_CacheRetrieveSilent = SCons.CacheDir.CacheRetrieveSilent

        f8 = self.File("cd.f8", 'f8_bsig')
        try:
            SCons.CacheDir.CacheRetrieveSilent = self.retrieve_succeed
            self.retrieved = []
            built_it = None

            r = f8.retrieve_from_cache()
            assert r == 1, r
            assert self.retrieved == [f8], self.retrieved
            assert built_it is None, built_it

            SCons.CacheDir.CacheRetrieveSilent = self.retrieve_fail
            self.retrieved = []
            built_it = None

            r = f8.retrieve_from_cache()
            assert r is False, r
            assert self.retrieved == [f8], self.retrieved
            assert built_it is None, built_it
        finally:
            SCons.CacheDir.CacheRetrieveSilent = save_CacheRetrieveSilent

if __name__ == "__main__":
    unittest.main()
# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/Conftest.py

# MIT License
#
# Copyright The SCons Foundation
# Copyright (c) 2003 Stichting NLnet Labs
# Copyright (c) 2001, 2002, 2003 Steven Knight
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

r"""Autoconf-like configuration support

The purpose of this module is to define how a check is to be performed.

A context class is used that defines functions for carrying out the tests,
logging and messages.  The following methods and members must be present:

context.Display(msg)
    Function called to print messages that are normally displayed
    for the user.  Newlines are explicitly used.  The text should
    also be written to the logfile!

context.Log(msg)
    Function called to write to a log file.

context.BuildProg(text, ext)
    Function called to build a program, using "ext" for the file
    extension.  Must return an empty string for success, an error
    message for failure.  For reliable test results building should
    be done just like an actual program would be build, using the
    same command and arguments (including configure results so far).

context.CompileProg(text, ext)
    Function called to compile a program, using "ext" for the file
    extension.  Must return an empty string for success, an error
    message for failure.  For reliable test results compiling should be
    done just like an actual source file would be compiled, using the
    same command and arguments (including configure results so far).

context.AppendLIBS(lib_name_list)
    Append "lib_name_list" to the value of LIBS.  "lib_namelist" is
    a list of strings.  Return the value of LIBS before changing it
    (any type can be used, it is passed to SetLIBS() later.)

context.PrependLIBS(lib_name_list)
    Prepend "lib_name_list" to the value of LIBS.  "lib_namelist" is
    a list of strings.  Return the value of LIBS before changing it
    (any type can be used, it is passed to SetLIBS() later.)

context.SetLIBS(value)
    Set LIBS to "value".  The type of "value" is what AppendLIBS()
    returned.  Return the value of LIBS before changing it (any type
    can be used, it is passed to SetLIBS() later.)

context.headerfilename
    Name of file to append configure results to, usually "confdefs.h".
    The file must not exist or be empty when starting.  Empty or None
    to skip this (some tests will not work!).

context.config_h  (may be missing).
    If present, must be a string, which will be filled with the
    contents of a config_h file.

context.vardict
    Dictionary holding variables used for the tests and stores results
    from the tests, used for the build commands.  Normally contains
    "CC", "LIBS", "CPPFLAGS", etc.

context.havedict
    Dictionary holding results from the tests that are to be used
    inside a program.  Names often start with "HAVE\_".  These are zero
    (feature not present) or one (feature present).  Other variables
    may have any value, e.g., "PERLVERSION" can be a number and
    "SYSTEMNAME" a string.
"""

import re

#
# PUBLIC VARIABLES
#

LogInputFiles = 1    # Set that to log the input files in case of a failed test
LogErrorMessages = 1 # Set that to log Conftest-generated error messages

#
# PUBLIC FUNCTIONS
#

# Generic remarks:
# - When a language is specified which is not supported the test fails.  The
#   message is a bit different, because not all the arguments for the normal
#   message are available yet (chicken-egg problem).


def CheckBuilder(context, text = None, language = None):
    """
    Configure check to see if the compiler works.
    Note that this uses the current value of compiler and linker flags, make
    sure $CFLAGS, $CPPFLAGS and $LIBS are set correctly.
    "language" should be "C" or "C++" and is used to select the compiler.
    Default is "C".
    "text" may be used to specify the code to be build.
    Returns an empty string for success, an error message for failure.
    """
    lang, suffix, msg = _lang2suffix(language)
    if msg:
        context.Display("%s\n" % msg)
        return msg

    if not text:
        text = """
int main(void) {
    return 0;
}
"""

    context.Display("Checking if building a %s file works... " % lang)
    ret = context.BuildProg(text, suffix)
    _YesNoResult(context, ret, None, text)
    return ret

def CheckCC(context):
    """
    Configure check for a working C compiler.

    This checks whether the C compiler, as defined in the $CC construction
    variable, can compile a C source file. It uses the current $CCCOM value
    too, so that it can test against non working flags.

    """
    context.Display("Checking whether the C compiler works... ")
    text = """
int main(void)
{
    return 0;
}
"""
    ret = _check_empty_program(context, 'CC', text, 'C')
    _YesNoResult(context, ret, None, text)
    return ret

def CheckSHCC(context):
    """
    Configure check for a working shared C compiler.

    This checks whether the C compiler, as defined in the $SHCC construction
    variable, can compile a C source file. It uses the current $SHCCCOM value
    too, so that it can test against non working flags.

    """
    context.Display("Checking whether the (shared) C compiler works... ")
    text = """
int foo(void)
{
    return 0;
}
"""
    ret = _check_empty_program(context, 'SHCC', text, 'C', use_shared = True)
    _YesNoResult(context, ret, None, text)
    return ret

def CheckCXX(context):
    """
    Configure check for a working CXX compiler.

    This checks whether the CXX compiler, as defined in the $CXX construction
    variable, can compile a CXX source file. It uses the current $CXXCOM value
    too, so that it can test against non working flags.

    """
    context.Display("Checking whether the C++ compiler works... ")
    text = """
int main(void)
{
    return 0;
}
"""
    ret = _check_empty_program(context, 'CXX', text, 'C++')
    _YesNoResult(context, ret, None, text)
    return ret

def CheckSHCXX(context):
    """
    Configure check for a working shared CXX compiler.

    This checks whether the CXX compiler, as defined in the $SHCXX construction
    variable, can compile a CXX source file. It uses the current $SHCXXCOM value
    too, so that it can test against non working flags.

    """
    context.Display("Checking whether the (shared) C++ compiler works... ")
    text = """
int main(void)
{
    return 0;
}
"""
    ret = _check_empty_program(context, 'SHCXX', text, 'C++', use_shared = True)
    _YesNoResult(context, ret, None, text)
    return ret

def _check_empty_program(context, comp, text, language, use_shared = False):
    """Return 0 on success, 1 otherwise."""
    if comp not in context.env or not context.env[comp]:
        # The compiler construction variable is not set or empty
        return 1

    lang, suffix, msg = _lang2suffix(language)
    if msg:
        return 1

    if use_shared:
        return context.CompileSharedObject(text, suffix)
    else:
        return context.CompileProg(text, suffix)


def CheckFunc(context, function_name, header = None, language = None):
    """
    Configure check for a function "function_name".
    "language" should be "C" or "C++" and is used to select the compiler.
    Default is "C".
    Optional "header" can be defined to define a function prototype, include a
    header file or anything else that comes before main().
    Sets HAVE_function_name in context.havedict according to the result.
    Note that this uses the current value of compiler and linker flags, make
    sure $CFLAGS, $CPPFLAGS and $LIBS are set correctly.
    Returns an empty string for success, an error message for failure.
    """

    # Remarks from autoconf:
    # - Don't include <ctype.h> because on OSF/1 3.0 it includes <sys/types.h>
    #   which includes <sys/select.h> which contains a prototype for select.
    #   Similarly for bzero.
    # - assert.h is included to define __stub macros and hopefully few
    #   prototypes, which can conflict with char $1(); below.
    # - Override any gcc2 internal prototype to avoid an error.
    # - We use char for the function declaration because int might match the
    #   return type of a gcc2 builtin and then its argument prototype would
    #   still apply.
    # - The GNU C library defines this for functions which it implements to
    #   always fail with ENOSYS.  Some functions are actually named something
    #   starting with __ and the normal name is an alias.

    if context.headerfilename:
        includetext = '#include "%s"' % context.headerfilename
    else:
        includetext = ''
    if not header:
        header = """
#ifdef __cplusplus
extern "C"
#endif
char %s();""" % function_name

    lang, suffix, msg = _lang2suffix(language)
    if msg:
        context.Display("Cannot check for %s(): %s\n" % (function_name, msg))
        return msg

    text = """
%(include)s
#include <assert.h>
%(hdr)s

#if _MSC_VER && !__INTEL_COMPILER
    #pragma function(%(name)s)
#endif

int main(void) {
#if defined (__stub_%(name)s) || defined (__stub___%(name)s)
  fail fail fail
#else
  %(name)s();
#endif

  return 0;
}
""" % { 'name': function_name,
        'include': includetext,
        'hdr': header }

    context.Display("Checking for %s function %s()... " % (lang, function_name))
    ret = context.BuildProg(text, suffix)
    _YesNoResult(context, ret, "HAVE_" + function_name, text,
                 "Define to 1 if the system has the function `%s'." %\
                 function_name)
    return ret


def CheckHeader(context, header_name, header=None, language=None,
                include_quotes=None):
    """
    Configure check for a C or C++ header file "header_name".
    Optional "header" can be defined to do something before including the
    header file (unusual, supported for consistency).
    "language" should be "C" or "C++" and is used to select the compiler.
    Default is "C".
    Sets HAVE_header_name in context.havedict according to the result.
    Note that this uses the current value of compiler and linker flags, make
    sure $CFLAGS and $CPPFLAGS are set correctly.
    Returns an empty string for success, an error message for failure.
    """
    # Why compile the program instead of just running the preprocessor?
    # It is possible that the header file exists, but actually using it may
    # fail (e.g., because it depends on other header files).  Thus this test is
    # more strict.  It may require using the "header" argument.
    #
    # Use <> by default, because the check is normally used for system header
    # files.  SCons passes '""' to overrule this.

    # Include "confdefs.h" first, so that the header can use HAVE_HEADER_H.
    if context.headerfilename:
        includetext = '#include "%s"\n' % context.headerfilename
    else:
        includetext = ''
    if not header:
        header = ""

    lang, suffix, msg = _lang2suffix(language)
    if msg:
        context.Display("Cannot check for header file %s: %s\n"
                                                          % (header_name, msg))
        return msg

    if not include_quotes:
        include_quotes = "<>"

    text = "%s%s\n#include %s%s%s\n\n" % (includetext, header,
                             include_quotes[0], header_name, include_quotes[1])

    context.Display("Checking for %s header file %s... " % (lang, header_name))
    ret = context.CompileProg(text, suffix)
    _YesNoResult(context, ret, "HAVE_" + header_name, text,
                 "Define to 1 if you have the <%s> header file." % header_name)
    return ret


def CheckType(context, type_name, fallback = None,
                                               header = None, language = None):
    """
    Configure check for a C or C++ type "type_name".
    Optional "header" can be defined to include a header file.
    "language" should be "C" or "C++" and is used to select the compiler.
    Default is "C".
    Sets HAVE_type_name in context.havedict according to the result.
    Note that this uses the current value of compiler and linker flags, make
    sure $CFLAGS, $CPPFLAGS and $LIBS are set correctly.
    Returns an empty string for success, an error message for failure.
    """

    # Include "confdefs.h" first, so that the header can use HAVE_HEADER_H.
    if context.headerfilename:
        includetext = '#include "%s"' % context.headerfilename
    else:
        includetext = ''
    if not header:
        header = ""

    lang, suffix, msg = _lang2suffix(language)
    if msg:
        context.Display("Cannot check for %s type: %s\n" % (type_name, msg))
        return msg

    # Remarks from autoconf about this test:
    # - Grepping for the type in include files is not reliable (grep isn't
    #   portable anyway).
    # - Using "TYPE my_var;" doesn't work for const qualified types in C++.
    #   Adding an initializer is not valid for some C++ classes.
    # - Using the type as parameter to a function either fails for K&$ C or for
    #   C++.
    # - Using "TYPE *my_var;" is valid in C for some types that are not
    #   declared (struct something).
    # - Using "sizeof(TYPE)" is valid when TYPE is actually a variable.
    # - Using the previous two together works reliably.
    text = """
%(include)s
%(header)s

int main(void) {
  if ((%(name)s *) 0)
    return 0;
  if (sizeof (%(name)s))
    return 0;
}
""" % { 'include': includetext,
        'header': header,
        'name': type_name }

    context.Display("Checking for %s type %s... " % (lang, type_name))
    ret = context.BuildProg(text, suffix)
    _YesNoResult(context, ret, "HAVE_" + type_name, text,
                 "Define to 1 if the system has the type `%s'." % type_name)
    if ret and fallback and context.headerfilename:
        f = open(context.headerfilename, "a")
        f.write("typedef %s %s;\n" % (fallback, type_name))
        f.close()

    return ret

def CheckTypeSize(context, type_name, header = None, language = None, expect = None):
    """This check can be used to get the size of a given type, or to check whether
    the type is of expected size.

    Arguments:
        - type : str
            the type to check
        - includes : sequence
            list of headers to include in the test code before testing the type
        - language : str
            'C' or 'C++'
        - expect : int
            if given, will test wether the type has the given number of bytes.
            If not given, will automatically find the size.

        Returns:
            status : int
                0 if the check failed, or the found size of the type if the check succeeded."""

    # Include "confdefs.h" first, so that the header can use HAVE_HEADER_H.
    if context.headerfilename:
        includetext = '#include "%s"' % context.headerfilename
    else:
        includetext = ''

    if not header:
        header = ""

    lang, suffix, msg = _lang2suffix(language)
    if msg:
        context.Display("Cannot check for %s type: %s\n" % (type_name, msg))
        return msg

    src = includetext + header
    if expect is not None:
        # Only check if the given size is the right one
        context.Display('Checking %s is %d bytes... ' % (type_name, expect))

        # test code taken from autoconf: this is a pretty clever hack to find that
        # a type is of a given size using only compilation. This speeds things up
        # quite a bit compared to straightforward code using TryRun
        src = src + r"""
typedef %s scons_check_type;

int main(void)
{
    static int test_array[1 - 2 * !(((long int) (sizeof(scons_check_type))) == %d)];
    test_array[0] = 0;

    return 0;
}
"""

        st = context.CompileProg(src % (type_name, expect), suffix)
        if not st:
            context.Display("yes\n")
            _Have(context, "SIZEOF_%s" % type_name, expect,
                  "The size of `%s', as computed by sizeof." % type_name)
            return expect
        else:
            context.Display("no\n")
            _LogFailed(context, src, st)
            return 0
    else:
        # Only check if the given size is the right one
        context.Message('Checking size of %s ... ' % type_name)

        # We have to be careful with the program we wish to test here since
        # compilation will be attempted using the current environment's flags.
        # So make sure that the program will compile without any warning. For
        # example using: 'int main(int argc, char** argv)' will fail with the
        # '-Wall -Werror' flags since the variables argc and argv would not be
        # used in the program...
        #
        src = src + """
#include <stdlib.h>
#include <stdio.h>
int main(void) {
    printf("%d", (int)sizeof(""" + type_name + """));
    return 0;
}
    """
        st, out = context.RunProg(src, suffix)
        try:
            size = int(out)
        except ValueError:
            # If cannot convert output of test prog to an integer (the size),
            # something went wront, so just fail
            st = 1
            size = 0

        if not st:
            context.Display("yes\n")
            _Have(context, "SIZEOF_%s" % type_name, size,
                  "The size of `%s', as computed by sizeof." % type_name)
            return size
        else:
            context.Display("no\n")
            _LogFailed(context, src, st)
            return 0

    return 0

def CheckDeclaration(context, symbol, includes = None, language = None):
    """Checks whether symbol is declared.

    Use the same test as autoconf, that is test whether the symbol is defined
    as a macro or can be used as an r-value.

    Arguments:
        symbol : str
            the symbol to check
        includes : str
            Optional "header" can be defined to include a header file.
        language : str
            only C and C++ supported.

    Returns:
        status : bool
            True if the check failed, False if succeeded."""

    # Include "confdefs.h" first, so that the header can use HAVE_HEADER_H.
    if context.headerfilename:
        includetext = '#include "%s"' % context.headerfilename
    else:
        includetext = ''

    if not includes:
        includes = ""

    lang, suffix, msg = _lang2suffix(language)
    if msg:
        context.Display("Cannot check for declaration %s: %s\n" % (symbol, msg))
        return msg

    src = includetext + includes
    context.Display('Checking whether %s is declared... ' % symbol)

    src = src + r"""
int main(void)
{
#ifndef %s
    (void) %s;
#endif
    ;
    return 0;
}
""" % (symbol, symbol)

    st = context.CompileProg(src, suffix)
    _YesNoResult(context, st, "HAVE_DECL_" + symbol, src,
                 "Set to 1 if %s is defined." % symbol)
    return st


def CheckMember(context, aggregate_member, header = None, language = None):
    """
    Configure check for a C or C++ member "aggregate_member".
    Optional "header" can be defined to include a header file.
    "language" should be "C" or "C++" and is used to select the compiler.
    Default is "C".
    Note that this uses the current value of compiler and linker flags, make
    sure $CFLAGS, $CPPFLAGS and $LIBS are set correctly.

    Arguments:
        aggregate_member : str
            the member to check. For example, 'struct tm.tm_gmtoff'.
        includes : str
            Optional "header" can be defined to include a header file.
        language : str
            only C and C++ supported.

    Returns the status (0 or False = Passed, True/non-zero = Failed).
    """

    lang, suffix, msg = _lang2suffix(language)
    if msg:
        context.Display("Cannot check for member %s: %s\n" % (aggregate_member, msg))
        return True
    context.Display("Checking for %s member %s... " % (lang, aggregate_member))
    fields = aggregate_member.split('.')
    if len(fields) != 2:
        msg = "shall contain just one dot, for example 'struct tm.tm_gmtoff'"
        context.Display("Cannot check for member %s: %s\n" % (aggregate_member, msg))
        return True
    aggregate, member = fields[0], fields[1]

    # Include "confdefs.h" first, so that the header can use HAVE_HEADER_H.
    if context.headerfilename:
        includetext = '#include "%s"' % context.headerfilename
    else:
        includetext = ''
    if not header:
        header = ''
    text = '''
%(include)s
%(header)s

int main(void) {
  if (sizeof ((%(aggregate)s *) 0)->%(member)s)
    return 0;
}''' % {'include': includetext,
        'header': header,
        'aggregate': aggregate,
        'member': member}

    ret = context.BuildProg(text, suffix)
    _YesNoResult(context, ret, "HAVE_" + aggregate_member, text,
                 "Define to 1 if the system has the member `%s`." % aggregate_member)
    return ret

def CheckLib(context, libs, func_name = None, header = None,
             extra_libs = None, call = None, language = None, autoadd = 1,
             append = True):
    """
    Configure check for a C or C++ libraries "libs".  Searches through
    the list of libraries, until one is found where the test succeeds.
    Tests if "func_name" or "call" exists in the library.  Note: if it exists
    in another library the test succeeds anyway!
    Optional "header" can be defined to include a header file.  If not given a
    default prototype for "func_name" is added.
    Optional "extra_libs" is a list of library names to be added after
    "lib_name" in the build command.  To be used for libraries that "lib_name"
    depends on.
    Optional "call" replaces the call to "func_name" in the test code.  It must
    consist of complete C statements, including a trailing ";".
    Both "func_name" and "call" arguments are optional, and in that case, just
    linking against the libs is tested.
    "language" should be "C" or "C++" and is used to select the compiler.
    Default is "C".
    Note that this uses the current value of compiler and linker flags, make
    sure $CFLAGS, $CPPFLAGS and $LIBS are set correctly.
    Returns an empty string for success, an error message for failure.
    """
    # Include "confdefs.h" first, so that the header can use HAVE_HEADER_H.
    if context.headerfilename:
        includetext = '#include "%s"' % context.headerfilename
    else:
        includetext = ''
    if not header:
        header = ""

    text = """
%s
%s""" % (includetext, header)

    # Add a function declaration if needed.
    if func_name and func_name != "main":
        if not header:
            text = text + """
#ifdef __cplusplus
extern "C"
#endif
char %s();
""" % func_name

        # The actual test code.
        if not call:
            call = "%s();" % func_name

    # if no function to test, leave main() blank
    text = text + """
int
main() {
  %s
return 0;
}
""" % (call or "")

    if call:
        i = call.find("\n")
        if i > 0:
            calltext = call[:i] + ".."
        elif call[-1] == ';':
            calltext = call[:-1]
        else:
            calltext = call

    for lib_name in libs:

        lang, suffix, msg = _lang2suffix(language)
        if msg:
            context.Display("Cannot check for library %s: %s\n" % (lib_name, msg))
            return msg

        # if a function was specified to run in main(), say it
        if call:
                context.Display("Checking for %s in %s library %s... "
                                % (calltext, lang, lib_name))
        # otherwise, just say the name of library and language
        else:
                context.Display("Checking for %s library %s... "
                                % (lang, lib_name))

        if lib_name:
            l = [ lib_name ]
            if extra_libs:
                l.extend(extra_libs)
            if append:
                oldLIBS = context.AppendLIBS(l)
            else:
                oldLIBS = context.PrependLIBS(l)
            sym = "HAVE_LIB" + lib_name
        else:
            oldLIBS = -1
            sym = None

        ret = context.BuildProg(text, suffix)

        _YesNoResult(context, ret, sym, text,
                     "Define to 1 if you have the `%s' library." % lib_name)
        if oldLIBS != -1 and (ret or not autoadd):
            context.SetLIBS(oldLIBS)

        if not ret:
            return ret

    return ret

def CheckProg(context, prog_name):
    """
    Configure check for a specific program.

    Check whether program prog_name exists in path.  If it is found,
    returns the path for it, otherwise returns None.
    """
    context.Display("Checking whether %s program exists..." % prog_name)
    path = context.env.WhereIs(prog_name)
    if path:
        context.Display(path + "\n")
    else:
        context.Display("no\n")
    return path


#
# END OF PUBLIC FUNCTIONS
#

def _YesNoResult(context, ret, key, text, comment = None):
    r"""
    Handle the result of a test with a "yes" or "no" result.

    :Parameters:
      - `ret` is the return value: empty if OK, error message when not.
      - `key` is the name of the symbol to be defined (HAVE_foo).
      - `text` is the source code of the program used for testing.
      - `comment` is the C comment to add above the line defining the symbol (the comment is automatically put inside a /\* \*/). If None, no comment is added.
    """
    if key:
        _Have(context, key, not ret, comment)
    if ret:
        context.Display("no\n")
        _LogFailed(context, text, ret)
    else:
        context.Display("yes\n")


def _Have(context, key, have, comment = None):
    r"""
    Store result of a test in context.havedict and context.headerfilename.

    :Parameters:
      - `key` - is a "HAVE_abc" name.  It is turned into all CAPITALS and non-alphanumerics are replaced by an underscore.
      - `have`   - value as it should appear in the header file, include quotes when desired and escape special characters!
      - `comment` is the C comment to add above the line defining the symbol (the comment is automatically put inside a /\* \*/). If None, no comment is added.


    The value of "have" can be:
      - 1      - Feature is defined, add "#define key".
      - 0      - Feature is not defined, add "/\* #undef key \*/". Adding "undef" is what autoconf does.  Not useful for the compiler, but it shows that the test was done.
      - number - Feature is defined to this number "#define key have". Doesn't work for 0 or 1, use a string then.
      - string - Feature is defined to this string "#define key have".


    """
    key_up = key.upper()
    key_up = re.sub('[^A-Z0-9_]', '_', key_up)
    context.havedict[key_up] = have
    if have == 1:
        line = "#define %s 1\n" % key_up
    elif have == 0:
        line = "/* #undef %s */\n" % key_up
    elif isinstance(have, int):
        line = "#define %s %d\n" % (key_up, have)
    else:
        line = "#define %s %s\n" % (key_up, str(have))

    if comment is not None:
        lines = "\n/* %s */\n" % comment + line
    else:
        lines = "\n" + line

    if context.headerfilename:
        f = open(context.headerfilename, "a")
        f.write(lines)
        f.close()
    elif hasattr(context,'config_h'):
        context.config_h = context.config_h + lines


def _LogFailed(context, text, msg):
    """
    Write to the log about a failed program.
    Add line numbers, so that error messages can be understood.
    """
    if LogInputFiles:
        context.Log("Failed program was:\n")
        lines = text.split('\n')
        if len(lines) and lines[-1] == '':
            lines = lines[:-1]              # remove trailing empty line
        n = 1
        for line in lines:
            context.Log("%d: %s\n" % (n, line))
            n = n + 1
    if LogErrorMessages:
        context.Log("Error message: %s\n" % msg)


def _lang2suffix(lang):
    """
    Convert a language name to a suffix.
    When "lang" is empty or None C is assumed.
    Returns a tuple (lang, suffix, None) when it works.
    For an unrecognized language returns (None, None, msg).

    Where:
      - lang   = the unified language name
      - suffix = the suffix, including the leading dot
      - msg    = an error message
    """
    if not lang or lang in ["C", "c"]:
        return ("C", ".c", None)
    if lang in ["c++", "C++", "cpp", "CXX", "cxx"]:
        return ("C++", ".cpp", None)

    return None, None, "Unsupported language: %s" % lang


# vim: set sw=4 et sts=4 tw=79 fo+=l:

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/Debug.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Code for debugging SCons internal things.

Shouldn't be needed by most users. Quick shortcuts:

from SCons.Debug import caller_trace
caller_trace()
"""

import atexit
import os
import sys
import time
import weakref
import inspect

# Global variable that gets set to 'True' by the Main script,
# when the creation of class instances should get tracked.
track_instances = False
# List of currently tracked classes
tracked_classes = {}

def logInstanceCreation(instance, name=None):
    if name is None:
        name = instance.__class__.__name__
    if name not in tracked_classes:
        tracked_classes[name] = []
    if hasattr(instance, '__dict__'):
        tracked_classes[name].append(weakref.ref(instance))
    else:
        # weakref doesn't seem to work when the instance
        # contains only slots...
        tracked_classes[name].append(instance)

def string_to_classes(s):
    if s == '*':
        return sorted(tracked_classes.keys())
    else:
        return s.split()

def fetchLoggedInstances(classes="*"):
    classnames = string_to_classes(classes)
    return [(cn, len(tracked_classes[cn])) for cn in classnames]

def countLoggedInstances(classes, file=sys.stdout):
    for classname in string_to_classes(classes):
        file.write("%s: %d\n" % (classname, len(tracked_classes[classname])))

def listLoggedInstances(classes, file=sys.stdout):
    for classname in string_to_classes(classes):
        file.write('\n%s:\n' % classname)
        for ref in tracked_classes[classname]:
            if inspect.isclass(ref):
                obj = ref()
            else:
                obj = ref
            if obj is not None:
                file.write('    %s\n' % repr(obj))

def dumpLoggedInstances(classes, file=sys.stdout):
    for classname in string_to_classes(classes):
        file.write('\n%s:\n' % classname)
        for ref in tracked_classes[classname]:
            obj = ref()
            if obj is not None:
                file.write('    %s:\n' % obj)
                for key, value in obj.__dict__.items():
                    file.write('        %20s : %s\n' % (key, value))


if sys.platform[:5] == "linux":
    # Linux doesn't actually support memory usage stats from getrusage().
    def memory():
        with open('/proc/self/stat') as f:
            mstr = f.read()
        mstr = mstr.split()[22]
        return int(mstr)
elif sys.platform[:6] == 'darwin':
    #TODO really get memory stats for OS X
    def memory():
        return 0
elif sys.platform == 'win32':
    from SCons.compat.win32 import get_peak_memory_usage
    memory = get_peak_memory_usage
else:
    try:
        import resource
    except ImportError:
        def memory():
            return 0
    else:
        def memory():
            res = resource.getrusage(resource.RUSAGE_SELF)
            return res[4]


def caller_stack():
    """return caller's stack"""
    import traceback
    tb = traceback.extract_stack()
    # strip itself and the caller from the output
    tb = tb[:-2]
    result = []
    for back in tb:
        # (filename, line number, function name, text)
        key = back[:3]
        result.append('%s:%d(%s)' % func_shorten(key))
    return result

caller_bases = {}
caller_dicts = {}

def caller_trace(back=0):
    """
    Trace caller stack and save info into global dicts, which
    are printed automatically at the end of SCons execution.
    """
    global caller_bases, caller_dicts
    import traceback
    tb = traceback.extract_stack(limit=3+back)
    tb.reverse()
    callee = tb[1][:3]
    caller_bases[callee] = caller_bases.get(callee, 0) + 1
    for caller in tb[2:]:
        caller = callee + caller[:3]
        try:
            entry = caller_dicts[callee]
        except KeyError:
            caller_dicts[callee] = entry = {}
        entry[caller] = entry.get(caller, 0) + 1
        callee = caller

# print a single caller and its callers, if any
def _dump_one_caller(key, file, level=0):
    leader = '      '*level
    for v,c in sorted([(-v,c) for c,v in caller_dicts[key].items()]):
        file.write("%s  %6d %s:%d(%s)\n" % ((leader,-v) + func_shorten(c[-3:])))
        if c in caller_dicts:
            _dump_one_caller(c, file, level+1)

# print each call tree
def dump_caller_counts(file=sys.stdout):
    for k in sorted(caller_bases.keys()):
        file.write("Callers of %s:%d(%s), %d calls:\n"
                    % (func_shorten(k) + (caller_bases[k],)))
        _dump_one_caller(k, file)

shorten_list = [
    ( '/scons/SCons/',          1),
    ( '/src/engine/SCons/',     1),
    ( '/usr/lib/python',        0),
]

if os.sep != '/':
    shorten_list = [(t[0].replace('/', os.sep), t[1]) for t in shorten_list]

def func_shorten(func_tuple):
    f = func_tuple[0]
    for t in shorten_list:
        i = f.find(t[0])
        if i >= 0:
            if t[1]:
                i = i + len(t[0])
            return (f[i:],)+func_tuple[1:]
    return func_tuple


TraceFP = {}
if sys.platform == 'win32':
    TraceDefault = 'con'
else:
    TraceDefault = '/dev/tty'
TimeStampDefault = False
StartTime = time.perf_counter()
PreviousTime = StartTime

def Trace(msg, tracefile=None, mode='w', tstamp=False):
    """Write a trace message.

    Write messages when debugging which do not interfere with stdout.
    Useful in tests, which monitor stdout and would break with
    unexpected output. Trace messages can go to the console (which is
    opened as a file), or to a disk file; the tracefile argument persists
    across calls unless overridden.

    Args:
        tracefile: file to write trace message to. If omitted,
          write to the previous trace file (default: console).
        mode: file open mode (default: 'w')
        tstamp: write relative timestamps with trace. Outputs time since
          scons was started, and time since last trace (default: False)

    """
    global TraceDefault
    global TimeStampDefault
    global PreviousTime

    def trace_cleanup(traceFP):
        traceFP.close()

    if tracefile is None:
        tracefile = TraceDefault
    else:
        TraceDefault = tracefile
    if not tstamp:
        tstamp = TimeStampDefault
    else:
        TimeStampDefault = tstamp
    try:
        fp = TraceFP[tracefile]
    except KeyError:
        try:
            fp = TraceFP[tracefile] = open(tracefile, mode)
            atexit.register(trace_cleanup, fp)
        except TypeError:
            # Assume we were passed an open file pointer.
            fp = tracefile
    if tstamp:
        now = time.perf_counter()
        fp.write('%8.4f %8.4f:  ' % (now - StartTime, now - PreviousTime))
        PreviousTime = now
    fp.write(msg)
    fp.flush()

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/Defaults.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#

"""Builders and other things for the local site.

Here's where we'll duplicate the functionality of autoconf until we
move it into the installation procedure or use something like qmconf.

The code that reads the registry to find MSVC components was borrowed
from distutils.msvccompiler.
"""

import os
import shutil
import stat
import sys
import time

import SCons.Action
import SCons.Builder
import SCons.CacheDir
import SCons.Environment
import SCons.Errors
import SCons.PathList
import SCons.Scanner.Dir
import SCons.Subst
import SCons.Tool
import SCons.Util

# A placeholder for a default Environment (for fetching source files
# from source code management systems and the like).  This must be
# initialized later, after the top-level directory is set by the calling
# interface.
_default_env = None


# Lazily instantiate the default environment so the overhead of creating
# it doesn't apply when it's not needed.
def _fetch_DefaultEnvironment(*args, **kw):
    """Returns the already-created default construction environment."""
    global _default_env
    return _default_env


def DefaultEnvironment(*args, **kw):
    """
    Initial public entry point for creating the default construction
    Environment.

    After creating the environment, we overwrite our name
    (DefaultEnvironment) with the _fetch_DefaultEnvironment() function,
    which more efficiently returns the initialized default construction
    environment without checking for its existence.

    (This function still exists with its _default_check because someone
    else (*cough* Script/__init__.py *cough*) may keep a reference
    to this function.  So we can't use the fully functional idiom of
    having the name originally be a something that *only* creates the
    construction environment and then overwrites the name.)
    """
    global _default_env
    if not _default_env:
        _default_env = SCons.Environment.Environment(*args, **kw)
        _default_env.Decider('content')
        global DefaultEnvironment
        DefaultEnvironment = _fetch_DefaultEnvironment
        _default_env._CacheDir_path = None
    return _default_env


# Emitters for setting the shared attribute on object files,
# and an action for checking that all of the source files
# going into a shared library are, in fact, shared.
def StaticObjectEmitter(target, source, env):
    for tgt in target:
        tgt.attributes.shared = None
    return target, source


def SharedObjectEmitter(target, source, env):
    for tgt in target:
        tgt.attributes.shared = 1
    return target, source


def SharedFlagChecker(source, target, env):
    same = env.subst('$STATIC_AND_SHARED_OBJECTS_ARE_THE_SAME')
    if same == '0' or same == '' or same == 'False':
        for src in source:
            try:
                shared = src.attributes.shared
            except AttributeError:
                shared = None
            if not shared:
                raise SCons.Errors.UserError(
                    "Source file: %s is static and is not compatible with shared target: %s" % (src, target[0]))


SharedCheck = SCons.Action.Action(SharedFlagChecker, None)

# Some people were using these variable name before we made
# SourceFileScanner part of the public interface.  Don't break their
# SConscript files until we've given them some fair warning and a
# transition period.
CScan = SCons.Tool.CScanner
DScan = SCons.Tool.DScanner
LaTeXScan = SCons.Tool.LaTeXScanner
ObjSourceScan = SCons.Tool.SourceFileScanner
ProgScan = SCons.Tool.ProgramScanner

# These aren't really tool scanners, so they don't quite belong with
# the rest of those in Tool/__init__.py, but I'm not sure where else
# they should go.  Leave them here for now.

DirScanner = SCons.Scanner.Dir.DirScanner()
DirEntryScanner = SCons.Scanner.Dir.DirEntryScanner()

# Actions for common languages.
CAction = SCons.Action.Action("$CCCOM", "$CCCOMSTR")
ShCAction = SCons.Action.Action("$SHCCCOM", "$SHCCCOMSTR")
CXXAction = SCons.Action.Action("$CXXCOM", "$CXXCOMSTR")
ShCXXAction = SCons.Action.Action("$SHCXXCOM", "$SHCXXCOMSTR")

DAction = SCons.Action.Action("$DCOM", "$DCOMSTR")
ShDAction = SCons.Action.Action("$SHDCOM", "$SHDCOMSTR")

ASAction = SCons.Action.Action("$ASCOM", "$ASCOMSTR")
ASPPAction = SCons.Action.Action("$ASPPCOM", "$ASPPCOMSTR")

LinkAction = SCons.Action.Action("$LINKCOM", "$LINKCOMSTR")
ShLinkAction = SCons.Action.Action("$SHLINKCOM", "$SHLINKCOMSTR")

LdModuleLinkAction = SCons.Action.Action("$LDMODULECOM", "$LDMODULECOMSTR")

# Common tasks that we allow users to perform in platform-independent
# ways by creating ActionFactory instances.
ActionFactory = SCons.Action.ActionFactory


def get_paths_str(dest) -> str:
    """Generates a string from *dest* for use in a strfunction.

    If *dest* is a list, manually converts each elem to a string.
    """
    def quote(arg):
        return f'"{arg}"'

    if SCons.Util.is_List(dest):
        elem_strs = [quote(d) for d in dest]
        return f'[{", ".join(elem_strs)}]'
    else:
        return quote(dest)


permission_dic = {
    'u': {
        'r': stat.S_IRUSR,
        'w': stat.S_IWUSR,
        'x': stat.S_IXUSR
    },
    'g': {
        'r': stat.S_IRGRP,
        'w': stat.S_IWGRP,
        'x': stat.S_IXGRP
    },
    'o': {
        'r': stat.S_IROTH,
        'w': stat.S_IWOTH,
        'x': stat.S_IXOTH
    }
}


def chmod_func(dest, mode) -> None:
    """Implementation of the Chmod action function.

    *mode* can be either an integer (normally expressed in octal mode,
    as in 0o755) or a string following the syntax of the POSIX chmod
    command (for example "ugo+w"). The latter must be converted, since
    the underlying Python only takes the numeric form.
    """
    from string import digits
    SCons.Node.FS.invalidate_node_memos(dest)
    if not SCons.Util.is_List(dest):
        dest = [dest]
    if SCons.Util.is_String(mode) and 0 not in [i in digits for i in mode]:
        mode = int(mode, 8)
    if not SCons.Util.is_String(mode):
        for element in dest:
            os.chmod(str(element), mode)
    else:
        mode = str(mode)
        for operation in mode.split(","):
            if "=" in operation:
                operator = "="
            elif "+" in operation:
                operator = "+"
            elif "-" in operation:
                operator = "-"
            else:
                raise SyntaxError("Could not find +, - or =")
            operation_list = operation.split(operator)
            if len(operation_list) != 2:
                raise SyntaxError("More than one operator found")
            user = operation_list[0].strip().replace("a", "ugo")
            permission = operation_list[1].strip()
            new_perm = 0
            for u in user:
                for p in permission:
                    try:
                        new_perm = new_perm | permission_dic[u][p]
                    except KeyError:
                        raise SyntaxError("Unrecognized user or permission format")
            for element in dest:
                curr_perm = os.stat(str(element)).st_mode
                if operator == "=":
                    os.chmod(str(element), new_perm)
                elif operator == "+":
                    os.chmod(str(element), curr_perm | new_perm)
                elif operator == "-":
                    os.chmod(str(element), curr_perm & ~new_perm)


def chmod_strfunc(dest, mode) -> str:
    """strfunction for the Chmod action function."""
    if not SCons.Util.is_String(mode):
        return f'Chmod({get_paths_str(dest)}, {mode:#o})'
    else:
        return f'Chmod({get_paths_str(dest)}, "{mode}")'



Chmod = ActionFactory(chmod_func, chmod_strfunc)



def copy_func(dest, src, symlinks=True) -> int:
    """Implementation of the Copy action function.

    Copies *src* to *dest*.  If *src* is a list, *dest* must be
    a directory, or not exist (will be created).

    Since Python :mod:`shutil` methods, which know nothing about
    SCons Nodes, will be called to perform the actual copying,
    args are converted to strings first.

    If *symlinks* evaluates true, then a symbolic link will be
    shallow copied and recreated as a symbolic link; otherwise, copying
    a symbolic link will be equivalent to copying the symbolic link's
    final target regardless of symbolic link depth.
    """

    dest = str(dest)
    src = [str(n) for n in src] if SCons.Util.is_List(src) else str(src)

    SCons.Node.FS.invalidate_node_memos(dest)
    if SCons.Util.is_List(src):
        # this fails only if dest exists and is not a dir
        try:
            os.makedirs(dest, exist_ok=True)
        except FileExistsError:
            raise SCons.Errors.BuildError(
                errstr=(
                    'Error: Copy() called with a list of sources, '
                    'which requires target to be a directory, '
                    f'but "{dest}" is not a directory.'
                )
            )
        for file in src:
            shutil.copy2(file, dest)
        return 0

    elif os.path.islink(src):
        if symlinks:
            os.symlink(os.readlink(src), dest)
            return 0

        return copy_func(dest, os.path.realpath(src))

    elif os.path.isfile(src):
        shutil.copy2(src, dest)
        return 0

    else:
        shutil.copytree(src, dest, symlinks)
        return 0


def copy_strfunc(dest, src, symlinks=True) -> str:
    """strfunction for the Copy action function."""
    return f'Copy({get_paths_str(dest)}, {get_paths_str(src)})'


Copy = ActionFactory(copy_func, copy_strfunc)


def delete_func(dest, must_exist=False) -> None:
    """Implementation of the Delete action function.

    Lets the Python :func:`os.unlink` raise an error if *dest* does not exist,
    unless *must_exist* evaluates false (the default).
    """
    SCons.Node.FS.invalidate_node_memos(dest)
    if not SCons.Util.is_List(dest):
        dest = [dest]
    for entry in dest:
        entry = str(entry)
        # os.path.exists returns False with broken links that exist
        entry_exists = os.path.exists(entry) or os.path.islink(entry)
        if not entry_exists and not must_exist:
            continue
        # os.path.isdir returns True when entry is a link to a dir
        if os.path.isdir(entry) and not os.path.islink(entry):
            shutil.rmtree(entry, True)
            continue
        os.unlink(entry)


def delete_strfunc(dest, must_exist=False) -> str:
    """strfunction for the Delete action function."""
    return f'Delete({get_paths_str(dest)})'


Delete = ActionFactory(delete_func, delete_strfunc)


def mkdir_func(dest) -> None:
    """Implementation of the Mkdir action function."""
    SCons.Node.FS.invalidate_node_memos(dest)
    if not SCons.Util.is_List(dest):
        dest = [dest]
    for entry in dest:
        os.makedirs(str(entry), exist_ok=True)


Mkdir = ActionFactory(mkdir_func, lambda _dir: f'Mkdir({get_paths_str(_dir)})')


def move_func(dest, src) -> None:
    """Implementation of the Move action function."""
    SCons.Node.FS.invalidate_node_memos(dest)
    SCons.Node.FS.invalidate_node_memos(src)
    shutil.move(src, dest)


Move = ActionFactory(
    move_func, lambda dest, src: f'Move("{dest}", "{src}")', convert=str
)


def touch_func(dest) -> None:
    """Implementation of the Touch action function."""
    SCons.Node.FS.invalidate_node_memos(dest)
    if not SCons.Util.is_List(dest):
        dest = [dest]
    for file in dest:
        file = str(file)
        mtime = int(time.time())
        if os.path.exists(file):
            atime = os.path.getatime(file)
        else:
            with open(file, 'w'):
                atime = mtime
        os.utime(file, (atime, mtime))


Touch = ActionFactory(touch_func, lambda file: f'Touch({get_paths_str(file)})')


# Internal utility functions

# pylint: disable-msg=too-many-arguments
def _concat(prefix, items_iter, suffix, env, f=lambda x: x, target=None, source=None, affect_signature=True):
    """
    Creates a new list from 'items_iter' by first interpolating each element
    in the list using the 'env' dictionary and then calling f on the
    list, and finally calling _concat_ixes to concatenate 'prefix' and
    'suffix' onto each element of the list.
    """

    if not items_iter:
        return items_iter

    l = f(SCons.PathList.PathList(items_iter).subst_path(env, target, source))
    if l is not None:
        items_iter = l

    if not affect_signature:
        value = ['$(']
    else:
        value = []
    value += _concat_ixes(prefix, items_iter, suffix, env)

    if not affect_signature:
        value += ["$)"]

    return value
# pylint: enable-msg=too-many-arguments


def _concat_ixes(prefix, items_iter, suffix, env):
    """
    Creates a new list from 'items_iter' by concatenating the 'prefix' and
    'suffix' arguments onto each element of the list.  A trailing space
    on 'prefix' or leading space on 'suffix' will cause them to be put
    into separate list elements rather than being concatenated.
    """

    result = []

    # ensure that prefix and suffix are strings
    prefix = str(env.subst(prefix, SCons.Subst.SUBST_RAW))
    suffix = str(env.subst(suffix, SCons.Subst.SUBST_RAW))

    for x in SCons.Util.flatten(items_iter):
        if isinstance(x, SCons.Node.FS.File):
            result.append(x)
            continue
        x = str(x)
        if x:

            if prefix:
                if prefix[-1] == ' ':
                    result.append(prefix[:-1])
                elif x[:len(prefix)] != prefix:
                    x = prefix + x

            result.append(x)

            if suffix:
                if suffix[0] == ' ':
                    result.append(suffix[1:])
                elif x[-len(suffix):] != suffix:
                    result[-1] = result[-1] + suffix

    return result


def _stripixes(prefix, itms, suffix, stripprefixes, stripsuffixes, env, c=None):
    """
    This is a wrapper around _concat()/_concat_ixes() that checks for
    the existence of prefixes or suffixes on list items and strips them
    where it finds them.  This is used by tools (like the GNU linker)
    that need to turn something like 'libfoo.a' into '-lfoo'.
    """

    if not itms:
        return itms

    if not callable(c):
        env_c = env['_concat']
        if env_c != _concat and callable(env_c):
            # There's a custom _concat() method in the construction
            # environment, and we've allowed people to set that in
            # the past (see test/custom-concat.py), so preserve the
            # backwards compatibility.
            c = env_c
        else:
            c = _concat_ixes

    stripprefixes = list(map(env.subst, SCons.Util.flatten(stripprefixes)))
    stripsuffixes = list(map(env.subst, SCons.Util.flatten(stripsuffixes)))

    stripped = []
    for l in SCons.PathList.PathList(itms).subst_path(env, None, None):
        if isinstance(l, SCons.Node.FS.File):
            stripped.append(l)
            continue

        if not SCons.Util.is_String(l):
            l = str(l)

        for stripprefix in stripprefixes:
            lsp = len(stripprefix)
            if l[:lsp] == stripprefix:
                l = l[lsp:]
                # Do not strip more than one prefix
                break

        for stripsuffix in stripsuffixes:
            lss = len(stripsuffix)
            if l[-lss:] == stripsuffix:
                l = l[:-lss]
                # Do not strip more than one suffix
                break

        stripped.append(l)

    return c(prefix, stripped, suffix, env)


def processDefines(defs):
    """process defines, resolving strings, lists, dictionaries, into a list of
    strings
    """
    if SCons.Util.is_List(defs):
        l = []
        for d in defs:
            if d is None:
                continue
            elif SCons.Util.is_List(d) or isinstance(d, tuple):
                if len(d) >= 2:
                    l.append(str(d[0]) + '=' + str(d[1]))
                else:
                    l.append(str(d[0]))
            elif SCons.Util.is_Dict(d):
                for macro, value in d.items():
                    if value is not None:
                        l.append(str(macro) + '=' + str(value))
                    else:
                        l.append(str(macro))
            elif SCons.Util.is_String(d):
                l.append(str(d))
            else:
                raise SCons.Errors.UserError("DEFINE %s is not a list, dict, string or None." % repr(d))
    elif SCons.Util.is_Dict(defs):
        # The items in a dictionary are stored in random order, but
        # if the order of the command-line options changes from
        # invocation to invocation, then the signature of the command
        # line will change and we'll get random unnecessary rebuilds.
        # Consequently, we have to sort the keys to ensure a
        # consistent order...
        l = []
        for k, v in sorted(defs.items()):
            if v is None:
                l.append(str(k))
            else:
                l.append(str(k) + '=' + str(v))
    else:
        l = [str(defs)]
    return l


def _defines(prefix, defs, suffix, env, target=None, source=None, c=_concat_ixes):
    """A wrapper around _concat_ixes that turns a list or string
    into a list of C preprocessor command-line definitions.
    """

    return c(prefix, env.subst_list(processDefines(defs), target=target, source=source), suffix, env)


class NullCmdGenerator:
    """This is a callable class that can be used in place of other
    command generators if you don't want them to do anything.

    The __call__ method for this class simply returns the thing
    you instantiated it with.

    Example usage:
    env["DO_NOTHING"] = NullCmdGenerator
    env["LINKCOM"] = "${DO_NOTHING('$LINK $SOURCES $TARGET')}"
    """

    def __init__(self, cmd):
        self.cmd = cmd

    def __call__(self, target, source, env, for_signature=None):
        return self.cmd


class Variable_Method_Caller:
    """A class for finding a construction variable on the stack and
    calling one of its methods.

    We use this to support "construction variables" in our string
    eval()s that actually stand in for methods--specifically, use
    of "RDirs" in call to _concat that should actually execute the
    "TARGET.RDirs" method.  (We used to support this by creating a little
    "build dictionary" that mapped RDirs to the method, but this got in
    the way of Memoizing construction environments, because we had to
    create new environment objects to hold the variables.)
    """

    def __init__(self, variable, method):
        self.variable = variable
        self.method = method

    def __call__(self, *args, **kw):
        try:
            1 // 0
        except ZeroDivisionError:
            # Don't start iterating with the current stack-frame to
            # prevent creating reference cycles (f_back is safe).
            frame = sys.exc_info()[2].tb_frame.f_back
        variable = self.variable
        while frame:
            if variable in frame.f_locals:
                v = frame.f_locals[variable]
                if v:
                    method = getattr(v, self.method)
                    return method(*args, **kw)
            frame = frame.f_back
        return None


def __libversionflags(env, version_var, flags_var):
    """
    if version_var is not empty, returns env[flags_var], otherwise returns None
    :param env:
    :param version_var:
    :param flags_var:
    :return:
    """
    try:
        if env.subst('$' + version_var):
            return env[flags_var]
    except KeyError:
        pass
    return None


def __lib_either_version_flag(env, version_var1, version_var2, flags_var):
    """
    if $version_var1 or $version_var2 is not empty, returns env[flags_var], otherwise returns None
    :param env:
    :param version_var1:
    :param version_var2:
    :param flags_var:
    :return:
    """
    try:
        if env.subst('$' + version_var1) or env.subst('$' + version_var2):
            return env[flags_var]
    except KeyError:
        pass
    return None


ConstructionEnvironment = {
    'BUILDERS': {},
    'SCANNERS': [SCons.Tool.SourceFileScanner],
    'CONFIGUREDIR': '#/.sconf_temp',
    'CONFIGURELOG': '#/config.log',
    'CPPSUFFIXES': SCons.Tool.CSuffixes,
    'DSUFFIXES': SCons.Tool.DSuffixes,
    'ENV': {},
    'IDLSUFFIXES': SCons.Tool.IDLSuffixes,
    '_concat': _concat,
    '_defines': _defines,
    '_stripixes': _stripixes,
    '_LIBFLAGS': '${_concat(LIBLINKPREFIX, LIBS, LIBLINKSUFFIX, __env__)}',

    '_LIBDIRFLAGS': '${_concat(LIBDIRPREFIX, LIBPATH, LIBDIRSUFFIX, __env__, RDirs, TARGET, SOURCE, affect_signature=False)}',
    '_CPPINCFLAGS': '${_concat(INCPREFIX, CPPPATH, INCSUFFIX, __env__, RDirs, TARGET, SOURCE, affect_signature=False)}',

    '_CPPDEFFLAGS': '${_defines(CPPDEFPREFIX, CPPDEFINES, CPPDEFSUFFIX, __env__, TARGET, SOURCE)}',

    '__libversionflags': __libversionflags,
    '__SHLIBVERSIONFLAGS': '${__libversionflags(__env__,"SHLIBVERSION","_SHLIBVERSIONFLAGS")}',
    '__LDMODULEVERSIONFLAGS': '${__libversionflags(__env__,"LDMODULEVERSION","_LDMODULEVERSIONFLAGS")}',
    '__DSHLIBVERSIONFLAGS': '${__libversionflags(__env__,"DSHLIBVERSION","_DSHLIBVERSIONFLAGS")}',
    '__lib_either_version_flag': __lib_either_version_flag,

    'TEMPFILE': NullCmdGenerator,
    'TEMPFILEARGJOIN': ' ',
    'TEMPFILEARGESCFUNC': SCons.Subst.quote_spaces,
    'Dir': Variable_Method_Caller('TARGET', 'Dir'),
    'Dirs': Variable_Method_Caller('TARGET', 'Dirs'),
    'File': Variable_Method_Caller('TARGET', 'File'),
    'RDirs': Variable_Method_Caller('TARGET', 'RDirs'),
}

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/DefaultsTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import os
import unittest
import collections

import TestCmd

from SCons.Defaults import mkdir_func, _defines


class DummyEnvironment(collections.UserDict):
    def __init__(self, **kwargs):
        super().__init__()
        self.data.update(kwargs)

    def subst(self, str_subst, target=None, source=None, conv=None):
        if str_subst[0] == '$':
            return self.data[str_subst[1:]]
        return str_subst

    def subst_list(self, str_subst, target=None, source=None, conv=None):
        if str_subst[0] == '$':
            return [self.data[str_subst[1:]]]
        return [[str_subst]]


class DefaultsTestCase(unittest.TestCase):
    def test_mkdir_func0(self):
        test = TestCmd.TestCmd(workdir='')
        test.subdir('sub')
        subdir2 = test.workpath('sub', 'dir1', 'dir2')
        # Simple smoke test
        mkdir_func(subdir2)
        mkdir_func(subdir2)  # 2nd time should be OK too

    def test_mkdir_func1(self):
        test = TestCmd.TestCmd(workdir='')
        test.subdir('sub')
        subdir1 = test.workpath('sub', 'dir1')
        subdir2 = test.workpath('sub', 'dir1', 'dir2')
        # No error if asked to create existing dir
        os.makedirs(subdir2)
        mkdir_func(subdir2)
        mkdir_func(subdir1)

    def test_mkdir_func2(self):
        test = TestCmd.TestCmd(workdir='')
        test.subdir('sub')
        subdir1 = test.workpath('sub', 'dir1')
        subdir2 = test.workpath('sub', 'dir1', 'dir2')
        file = test.workpath('sub', 'dir1', 'dir2', 'file')

        # make sure it does error if asked to create a dir
        # where there's already a file
        os.makedirs(subdir2)
        test.write(file, "test\n")
        try:
            mkdir_func(file)
        except OSError as e:
            pass
        else:
            self.fail("expected OSError")

    def test__defines_no_target_or_source_arg(self):
        """
        Verify that _defines() function can handle either or neither source or
        target being specified
        """
        env = DummyEnvironment()

        # Neither source or target specified
        x = _defines('-D', ['A', 'B', 'C'], 'XYZ', env)
        self.assertEqual(x, ['-DAXYZ', '-DBXYZ', '-DCXYZ'])

        # only source specified
        y = _defines('-D', ['AA', 'BA', 'CA'], 'XYZA', env, 'XYZ')
        self.assertEqual(y, ['-DAAXYZA', '-DBAXYZA', '-DCAXYZA'])

        # source and target specified
        z = _defines('-D', ['AAB', 'BAB', 'CAB'], 'XYZAB', env, 'XYZ', 'abc')
        self.assertEqual(z,['-DAABXYZAB', '-DBABXYZAB', '-DCABXYZAB'])



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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/Environment.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Base class for construction Environments.

These are the primary objects used to communicate dependency and
construction information to the build engine.

Keyword arguments supplied when the construction Environment is created
are construction variables used to initialize the Environment.
"""

import copy
import os
import sys
import re
import shlex
from collections import UserDict

import SCons.Action
import SCons.Builder
import SCons.Debug
from SCons.Debug import logInstanceCreation
import SCons.Defaults
from SCons.Errors import UserError, BuildError
import SCons.Memoize
import SCons.Node
import SCons.Node.Alias
import SCons.Node.FS
import SCons.Node.Python
import SCons.Platform
import SCons.SConf
import SCons.SConsign
import SCons.Subst
import SCons.Tool
import SCons.Warnings
from SCons.Util import (
    AppendPath,
    CLVar,
    LogicalLines,
    MethodWrapper,
    PrependPath,
    Split,
    WhereIs,
    flatten,
    is_Dict,
    is_List,
    is_Sequence,
    is_String,
    is_Tuple,
    semi_deepcopy,
    semi_deepcopy_dict,
    to_String_for_subst,
    uniquer_hashables,
)

class _Null:
    pass

_null = _Null

_warn_copy_deprecated = True
_warn_source_signatures_deprecated = True
_warn_target_signatures_deprecated = True

CleanTargets = {}
CalculatorArgs = {}

def alias_builder(env, target, source):
    pass

AliasBuilder = SCons.Builder.Builder(
    action=alias_builder,
    target_factory=SCons.Node.Alias.default_ans.Alias,
    source_factory=SCons.Node.FS.Entry,
    multi=True,
    is_explicit=None,
    name='AliasBuilder',
)

def apply_tools(env, tools, toolpath):
    # Store the toolpath in the Environment.
    # This is expected to work even if no tools are given, so do this first.
    if toolpath is not None:
        env['toolpath'] = toolpath
    if not tools:
        return

    # Filter out null tools from the list.
    for tool in [_f for _f in tools if _f]:
        if is_List(tool) or is_Tuple(tool):
            # toolargs should be a dict of kw args
            toolname, toolargs, *rest = tool
            _ = env.Tool(toolname, **toolargs)
        else:
            _ = env.Tool(tool)

# These names are (or will be) controlled by SCons; users should never
# set or override them.  The warning can optionally be turned off,
# but scons will still ignore the illegal variable names even if it's off.
reserved_construction_var_names = [
    'CHANGED_SOURCES',
    'CHANGED_TARGETS',
    'SOURCE',
    'SOURCES',
    'TARGET',
    'TARGETS',
    'UNCHANGED_SOURCES',
    'UNCHANGED_TARGETS',
]

future_reserved_construction_var_names = [
    #'HOST_OS',
    #'HOST_ARCH',
    #'HOST_CPU',
]

def copy_non_reserved_keywords(dict):
    result = semi_deepcopy(dict)
    for k in result.copy().keys():
        if k in reserved_construction_var_names:
            msg = "Ignoring attempt to set reserved variable `$%s'"
            SCons.Warnings.warn(SCons.Warnings.ReservedVariableWarning, msg % k)
            del result[k]
    return result

def _set_reserved(env, key, value):
    msg = "Ignoring attempt to set reserved variable `$%s'"
    SCons.Warnings.warn(SCons.Warnings.ReservedVariableWarning, msg % key)

def _set_future_reserved(env, key, value):
    env._dict[key] = value
    msg = "`$%s' will be reserved in a future release and setting it will become ignored"
    SCons.Warnings.warn(SCons.Warnings.FutureReservedVariableWarning, msg % key)

def _set_BUILDERS(env, key, value):
    try:
        bd = env._dict[key]
        for k in bd.copy().keys():
            del bd[k]
    except KeyError:
        bd = BuilderDict(bd, env)
        env._dict[key] = bd
    for k, v in value.items():
        if not SCons.Builder.is_a_Builder(v):
            raise UserError('%s is not a Builder.' % repr(v))
    bd.update(value)

def _del_SCANNERS(env, key):
    del env._dict[key]
    env.scanner_map_delete()

def _set_SCANNERS(env, key, value):
    env._dict[key] = value
    env.scanner_map_delete()

def _delete_duplicates(l, keep_last):
    """Delete duplicates from a sequence, keeping the first or last."""
    seen=set()
    result=[]
    if keep_last:           # reverse in & out, then keep first
        l.reverse()
    for i in l:
        try:
            if i not in seen:
                result.append(i)
                seen.add(i)
        except TypeError:
            # probably unhashable.  Just keep it.
            result.append(i)
    if keep_last:
        result.reverse()
    return result



# The following is partly based on code in a comment added by Peter
# Shannon at the following page (there called the "transplant" class):
#
# ASPN : Python Cookbook : Dynamically added methods to a class
# https://code.activestate.com/recipes/81732/
#
# We had independently been using the idiom as BuilderWrapper, but
# factoring out the common parts into this base class, and making
# BuilderWrapper a subclass that overrides __call__() to enforce specific
# Builder calling conventions, simplified some of our higher-layer code.
#
# Note: MethodWrapper moved to SCons.Util as it was needed there
# and otherwise we had a circular import problem.

class BuilderWrapper(MethodWrapper):
    """
    A MethodWrapper subclass that that associates an environment with
    a Builder.

    This mainly exists to wrap the __call__() function so that all calls
    to Builders can have their argument lists massaged in the same way
    (treat a lone argument as the source, treat two arguments as target
    then source, make sure both target and source are lists) without
    having to have cut-and-paste code to do it.

    As a bit of obsessive backwards compatibility, we also intercept
    attempts to get or set the "env" or "builder" attributes, which were
    the names we used before we put the common functionality into the
    MethodWrapper base class.  We'll keep this around for a while in case
    people shipped Tool modules that reached into the wrapper (like the
    Tool/qt.py module does, or did).  There shouldn't be a lot attribute
    fetching or setting on these, so a little extra work shouldn't hurt.
    """
    def __call__(self, target=None, source=_null, *args, **kw):
        if source is _null:
            source = target
            target = None
        if target is not None and not is_List(target):
            target = [target]
        if source is not None and not is_List(source):
            source = [source]
        return super().__call__(target, source, *args, **kw)

    def __repr__(self):
        return '<BuilderWrapper %s>' % repr(self.name)

    def __str__(self):
        return self.__repr__()

    def __getattr__(self, name):
        if name == 'env':
            return self.object
        elif name == 'builder':
            return self.method
        else:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        if name == 'env':
            self.object = value
        elif name == 'builder':
            self.method = value
        else:
            self.__dict__[name] = value

    # This allows a Builder to be executed directly
    # through the Environment to which it's attached.
    # In practice, we shouldn't need this, because
    # builders actually get executed through a Node.
    # But we do have a unit test for this, and can't
    # yet rule out that it would be useful in the
    # future, so leave it for now.
    #def execute(self, **kw):
    #    kw['env'] = self.env
    #    self.builder.execute(**kw)

class BuilderDict(UserDict):
    """This is a dictionary-like class used by an Environment to hold
    the Builders.  We need to do this because every time someone changes
    the Builders in the Environment's BUILDERS dictionary, we must
    update the Environment's attributes."""
    def __init__(self, mapping, env):
        # Set self.env before calling the superclass initialization,
        # because it will end up calling our other methods, which will
        # need to point the values in this dictionary to self.env.
        self.env = env
        super().__init__(mapping)

    def __semi_deepcopy__(self):
        # These cannot be copied since they would both modify the same builder object, and indeed
        # just copying would modify the original builder
        raise TypeError( 'cannot semi_deepcopy a BuilderDict' )

    def __setitem__(self, item, val):
        try:
            method = getattr(self.env, item).method
        except AttributeError:
            pass
        else:
            self.env.RemoveMethod(method)
        super().__setitem__(item, val)
        BuilderWrapper(self.env, val, item)

    def __delitem__(self, item):
        super().__delitem__(item)
        delattr(self.env, item)

    def update(self, mapping):
        for i, v in mapping.items():
            self.__setitem__(i, v)



_is_valid_var = re.compile(r'[_a-zA-Z]\w*$')

def is_valid_construction_var(varstr):
    """Return if the specified string is a legitimate construction
    variable.
    """
    return _is_valid_var.match(varstr)



class SubstitutionEnvironment:
    """Base class for different flavors of construction environments.

    This class contains a minimal set of methods that handle construction
    variable expansion and conversion of strings to Nodes, which may or
    may not be actually useful as a stand-alone class.  Which methods
    ended up in this class is pretty arbitrary right now.  They're
    basically the ones which we've empirically determined are common to
    the different construction environment subclasses, and most of the
    others that use or touch the underlying dictionary of construction
    variables.

    Eventually, this class should contain all the methods that we
    determine are necessary for a "minimal" interface to the build engine.
    A full "native Python" SCons environment has gotten pretty heavyweight
    with all of the methods and Tools and construction variables we've
    jammed in there, so it would be nice to have a lighter weight
    alternative for interfaces that don't need all of the bells and
    whistles.  (At some point, we'll also probably rename this class
    "Base," since that more reflects what we want this class to become,
    but because we've released comments that tell people to subclass
    Environment.Base to create their own flavors of construction
    environment, we'll save that for a future refactoring when this
    class actually becomes useful.)
    """

    def __init__(self, **kw):
        """Initialization of an underlying SubstitutionEnvironment class.
        """
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Environment.SubstitutionEnvironment')
        self.fs = SCons.Node.FS.get_default_fs()
        self.ans = SCons.Node.Alias.default_ans
        self.lookup_list = SCons.Node.arg2nodes_lookups
        self._dict = kw.copy()
        self._init_special()
        self.added_methods = []
        #self._memo = {}

    def _init_special(self):
        """Initial the dispatch tables for special handling of
        special construction variables."""
        self._special_del = {}
        self._special_del['SCANNERS'] = _del_SCANNERS

        self._special_set = {}
        for key in reserved_construction_var_names:
            self._special_set[key] = _set_reserved
        for key in future_reserved_construction_var_names:
            self._special_set[key] = _set_future_reserved
        self._special_set['BUILDERS'] = _set_BUILDERS
        self._special_set['SCANNERS'] = _set_SCANNERS

        # Freeze the keys of self._special_set in a list for use by
        # methods that need to check.
        self._special_set_keys = list(self._special_set.keys())

    def __eq__(self, other):
        return self._dict == other._dict

    def __delitem__(self, key):
        special = self._special_del.get(key)
        if special:
            special(self, key)
        else:
            del self._dict[key]

    def __getitem__(self, key):
        return self._dict[key]

    def __setitem__(self, key, value):
        # This is heavily used.  This implementation is the best we have
        # according to the timings in bench/env.__setitem__.py.
        #
        # The "key in self._special_set_keys" test here seems to perform
        # pretty well for the number of keys we have.  A hard-coded
        # list worked a little better in Python 2.5, but that has the
        # disadvantage of maybe getting out of sync if we ever add more
        # variable names.
        # So right now it seems like a good trade-off, but feel free to
        # revisit this with bench/env.__setitem__.py as needed (and
        # as newer versions of Python come out).
        if key in self._special_set_keys:
            self._special_set[key](self, key, value)
        else:
            # If we already have the entry, then it's obviously a valid
            # key and we don't need to check.  If we do check, using a
            # global, pre-compiled regular expression directly is more
            # efficient than calling another function or a method.
            if key not in self._dict and not _is_valid_var.match(key):
                raise UserError("Illegal construction variable `%s'" % key)
            self._dict[key] = value

    def get(self, key, default=None):
        """Emulates the get() method of dictionaries."""
        return self._dict.get(key, default)

    def __contains__(self, key):
        return key in self._dict

    def keys(self):
        """Emulates the keys() method of dictionaries."""
        return self._dict.keys()

    def values(self):
        """Emulates the values() method of dictionaries."""
        return self._dict.values()

    def items(self):
        """Emulates the items() method of dictionaries."""
        return self._dict.items()

    def setdefault(self, key, default=None):
        """Emulates the setdefault() method of dictionaries."""
        return self._dict.setdefault(key, default)

    def arg2nodes(self, args, node_factory=_null, lookup_list=_null, **kw):
        if node_factory is _null:
            node_factory = self.fs.File
        if lookup_list is _null:
            lookup_list = self.lookup_list

        if not args:
            return []

        args = flatten(args)

        nodes = []
        for v in args:
            if is_String(v):
                n = None
                for l in lookup_list:
                    n = l(v)
                    if n is not None:
                        break
                if n is not None:
                    if is_String(n):
                        # n = self.subst(n, raw=1, **kw)
                        kw['raw'] = 1
                        n = self.subst(n, **kw)
                        if node_factory:
                            n = node_factory(n)
                    if is_List(n):
                        nodes.extend(n)
                    else:
                        nodes.append(n)
                elif node_factory:
                    # v = node_factory(self.subst(v, raw=1, **kw))
                    kw['raw'] = 1
                    v = node_factory(self.subst(v, **kw))
                    if is_List(v):
                        nodes.extend(v)
                    else:
                        nodes.append(v)
            else:
                nodes.append(v)

        return nodes

    def gvars(self):
        return self._dict

    def lvars(self):
        return {}

    def subst(self, string, raw=0, target=None, source=None, conv=None, executor=None):
        """Recursively interpolates construction variables from the
        Environment into the specified string, returning the expanded
        result.  Construction variables are specified by a $ prefix
        in the string and begin with an initial underscore or
        alphabetic character followed by any number of underscores
        or alphanumeric characters.  The construction variable names
        may be surrounded by curly braces to separate the name from
        trailing characters.
        """
        gvars = self.gvars()
        lvars = self.lvars()
        lvars['__env__'] = self
        if executor:
            lvars.update(executor.get_lvars())
        return SCons.Subst.scons_subst(string, self, raw, target, source, gvars, lvars, conv)

    def subst_kw(self, kw, raw=0, target=None, source=None):
        nkw = {}
        for k, v in kw.items():
            k = self.subst(k, raw, target, source)
            if is_String(v):
                v = self.subst(v, raw, target, source)
            nkw[k] = v
        return nkw

    def subst_list(self, string, raw=0, target=None, source=None, conv=None, executor=None):
        """Calls through to SCons.Subst.scons_subst_list().  See
        the documentation for that function."""
        gvars = self.gvars()
        lvars = self.lvars()
        lvars['__env__'] = self
        if executor:
            lvars.update(executor.get_lvars())
        return SCons.Subst.scons_subst_list(string, self, raw, target, source, gvars, lvars, conv)

    def subst_path(self, path, target=None, source=None):
        """Substitute a path list, turning EntryProxies into Nodes
        and leaving Nodes (and other objects) as-is."""

        if not is_List(path):
            path = [path]

        def s(obj):
            """This is the "string conversion" routine that we have our
            substitutions use to return Nodes, not strings.  This relies
            on the fact that an EntryProxy object has a get() method that
            returns the underlying Node that it wraps, which is a bit of
            architectural dependence that we might need to break or modify
            in the future in response to additional requirements."""
            try:
                get = obj.get
            except AttributeError:
                obj = to_String_for_subst(obj)
            else:
                obj = get()
            return obj

        r = []
        for p in path:
            if is_String(p):
                p = self.subst(p, target=target, source=source, conv=s)
                if is_List(p):
                    if len(p) == 1:
                        p = p[0]
                    else:
                        # We have an object plus a string, or multiple
                        # objects that we need to smush together.  No choice
                        # but to make them into a string.
                        p = ''.join(map(to_String_for_subst, p))
            else:
                p = s(p)
            r.append(p)
        return r

    subst_target_source = subst


    def backtick(self, command) -> str:
        """Emulate command substitution.

        Provides behavior conceptually like POSIX Shell notation
        for running a command in backquotes (backticks) by running
        ``command`` and returning the resulting output string.

        This is not really a public API any longer, it is provided for the
        use of :meth:`ParseFlags` (which supports it using a syntax of
        !command) and :meth:`ParseConfig`.

        Raises:
            OSError: if the external command returned non-zero exit status.
        """

        import subprocess

        # common arguments
        kw = {
            "stdin": "devnull",
            "stdout": subprocess.PIPE,
            "stderr": subprocess.PIPE,
            "universal_newlines": True,
        }
        # if the command is a list, assume it's been quoted
        # othewise force a shell
        if not is_List(command):
            kw["shell"] = True
        # run constructed command
        p = SCons.Action._subproc(self, command, **kw)
        out, err = p.communicate()
        status = p.wait()
        if err:
            sys.stderr.write("" + err)
        if status:
            raise OSError("'%s' exited %d" % (command, status))
        return out


    def AddMethod(self, function, name=None):
        """
        Adds the specified function as a method of this construction
        environment with the specified name.  If the name is omitted,
        the default name is the name of the function itself.
        """
        method = MethodWrapper(self, function, name)
        self.added_methods.append(method)

    def RemoveMethod(self, function):
        """
        Removes the specified function's MethodWrapper from the
        added_methods list, so we don't re-bind it when making a clone.
        """
        self.added_methods = [dm for dm in self.added_methods if dm.method is not function]

    def Override(self, overrides):
        """
        Produce a modified environment whose variables are overridden by
        the overrides dictionaries.  "overrides" is a dictionary that
        will override the variables of this environment.

        This function is much more efficient than Clone() or creating
        a new Environment because it doesn't copy the construction
        environment dictionary, it just wraps the underlying construction
        environment, and doesn't even create a wrapper object if there
        are no overrides.
        """
        if not overrides: return self
        o = copy_non_reserved_keywords(overrides)
        if not o: return self
        overrides = {}
        merges = None
        for key, value in o.items():
            if key == 'parse_flags':
                merges = value
            else:
                overrides[key] = SCons.Subst.scons_subst_once(value, self, key)
        env = OverrideEnvironment(self, overrides)
        if merges:
            env.MergeFlags(merges)
        return env

    def ParseFlags(self, *flags) -> dict:
        """Return a dict of parsed flags.

        Parse ``flags`` and return a dict with the flags distributed into
        the appropriate construction variable names.  The flags are treated
        as a typical set of command-line flags for a GNU-style toolchain,
        such as might have been generated by one of the {foo}-config scripts,
        and used to populate the entries based on knowledge embedded in
        this method - the choices are not expected to be portable to other
        toolchains.

        If one of the ``flags`` strings begins with a bang (exclamation mark),
        it is assumed to be a command and the rest of the string is executed;
        the result of that evaluation is then added to the dict.
        """
        mapping = {
            'ASFLAGS'       : CLVar(''),
            'CFLAGS'        : CLVar(''),
            'CCFLAGS'       : CLVar(''),
            'CXXFLAGS'      : CLVar(''),
            'CPPDEFINES'    : [],
            'CPPFLAGS'      : CLVar(''),
            'CPPPATH'       : [],
            'FRAMEWORKPATH' : CLVar(''),
            'FRAMEWORKS'    : CLVar(''),
            'LIBPATH'       : [],
            'LIBS'          : [],
            'LINKFLAGS'     : CLVar(''),
            'RPATH'         : [],
        }

        def do_parse(arg):
            # if arg is a sequence, recurse with each element
            if not arg:
                return

            if not is_String(arg):
                for t in arg: do_parse(t)
                return

            # if arg is a command, execute it
            if arg[0] == '!':
                arg = self.backtick(arg[1:])

            # utility function to deal with -D option
            def append_define(name, mapping=mapping):
                t = name.split('=')
                if len(t) == 1:
                    mapping['CPPDEFINES'].append(name)
                else:
                    mapping['CPPDEFINES'].append([t[0], '='.join(t[1:])])

            # Loop through the flags and add them to the appropriate option.
            # This tries to strike a balance between checking for all possible
            # flags and keeping the logic to a finite size, so it doesn't
            # check for some that don't occur often.  It particular, if the
            # flag is not known to occur in a config script and there's a way
            # of passing the flag to the right place (by wrapping it in a -W
            # flag, for example) we don't check for it.  Note that most
            # preprocessor options are not handled, since unhandled options
            # are placed in CCFLAGS, so unless the preprocessor is invoked
            # separately, these flags will still get to the preprocessor.
            # Other options not currently handled:
            #  -iqoutedir      (preprocessor search path)
            #  -u symbol       (linker undefined symbol)
            #  -s              (linker strip files)
            #  -static*        (linker static binding)
            #  -shared*        (linker dynamic binding)
            #  -symbolic       (linker global binding)
            #  -R dir          (deprecated linker rpath)
            # IBM compilers may also accept -qframeworkdir=foo

            params = shlex.split(arg)
            append_next_arg_to = None   # for multi-word args
            for arg in params:
                if append_next_arg_to:
                    if append_next_arg_to == 'CPPDEFINES':
                        append_define(arg)
                    elif append_next_arg_to == '-include':
                        t = ('-include', self.fs.File(arg))
                        mapping['CCFLAGS'].append(t)
                    elif append_next_arg_to == '-imacros':
                        t = ('-imacros', self.fs.File(arg))
                        mapping['CCFLAGS'].append(t)
                    elif append_next_arg_to == '-isysroot':
                        t = ('-isysroot', arg)
                        mapping['CCFLAGS'].append(t)
                        mapping['LINKFLAGS'].append(t)
                    elif append_next_arg_to == '-isystem':
                        t = ('-isystem', arg)
                        mapping['CCFLAGS'].append(t)
                    elif append_next_arg_to == '-iquote':
                        t = ('-iquote', arg)
                        mapping['CCFLAGS'].append(t)
                    elif append_next_arg_to == '-idirafter':
                        t = ('-idirafter', arg)
                        mapping['CCFLAGS'].append(t)
                    elif append_next_arg_to == '-arch':
                        t = ('-arch', arg)
                        mapping['CCFLAGS'].append(t)
                        mapping['LINKFLAGS'].append(t)
                    elif append_next_arg_to == '--param':
                        t = ('--param', arg)
                        mapping['CCFLAGS'].append(t)
                    else:
                        mapping[append_next_arg_to].append(arg)
                    append_next_arg_to = None
                elif not arg[0] in ['-', '+']:
                    mapping['LIBS'].append(self.fs.File(arg))
                elif arg == '-dylib_file':
                    mapping['LINKFLAGS'].append(arg)
                    append_next_arg_to = 'LINKFLAGS'
                elif arg[:2] == '-L':
                    if arg[2:]:
                        mapping['LIBPATH'].append(arg[2:])
                    else:
                        append_next_arg_to = 'LIBPATH'
                elif arg[:2] == '-l':
                    if arg[2:]:
                        mapping['LIBS'].append(arg[2:])
                    else:
                        append_next_arg_to = 'LIBS'
                elif arg[:2] == '-I':
                    if arg[2:]:
                        mapping['CPPPATH'].append(arg[2:])
                    else:
                        append_next_arg_to = 'CPPPATH'
                elif arg[:4] == '-Wa,':
                    mapping['ASFLAGS'].append(arg[4:])
                    mapping['CCFLAGS'].append(arg)
                elif arg[:4] == '-Wl,':
                    if arg[:11] == '-Wl,-rpath=':
                        mapping['RPATH'].append(arg[11:])
                    elif arg[:7] == '-Wl,-R,':
                        mapping['RPATH'].append(arg[7:])
                    elif arg[:6] == '-Wl,-R':
                        mapping['RPATH'].append(arg[6:])
                    else:
                        mapping['LINKFLAGS'].append(arg)
                elif arg[:4] == '-Wp,':
                    mapping['CPPFLAGS'].append(arg)
                elif arg[:2] == '-D':
                    if arg[2:]:
                        append_define(arg[2:])
                    else:
                        append_next_arg_to = 'CPPDEFINES'
                elif arg == '-framework':
                    append_next_arg_to = 'FRAMEWORKS'
                elif arg[:14] == '-frameworkdir=':
                    mapping['FRAMEWORKPATH'].append(arg[14:])
                elif arg[:2] == '-F':
                    if arg[2:]:
                        mapping['FRAMEWORKPATH'].append(arg[2:])
                    else:
                        append_next_arg_to = 'FRAMEWORKPATH'
                elif arg in (
                    '-mno-cygwin',
                    '-pthread',
                    '-openmp',
                    '-fmerge-all-constants',
                    '-fopenmp',
                ) or arg.startswith('-fsanitize'):
                    mapping['CCFLAGS'].append(arg)
                    mapping['LINKFLAGS'].append(arg)
                elif arg == '-mwindows':
                    mapping['LINKFLAGS'].append(arg)
                elif arg[:5] == '-std=':
                    if '++' in arg[5:]:
                        key = 'CXXFLAGS'
                    else:
                        key = 'CFLAGS'
                    mapping[key].append(arg)
                elif arg[0] == '+':
                    mapping['CCFLAGS'].append(arg)
                    mapping['LINKFLAGS'].append(arg)
                elif arg in [
                    '-include',
                    '-imacros',
                    '-isysroot',
                    '-isystem',
                    '-iquote',
                    '-idirafter',
                    '-arch',
                    '--param',
                ]:
                    append_next_arg_to = arg
                else:
                    mapping['CCFLAGS'].append(arg)

        for arg in flags:
            do_parse(arg)
        return mapping

    def MergeFlags(self, args, unique=True) -> None:
        """Merge flags into construction variables.

        Merges the flags from ``args`` into this construction environent.
        If ``args`` is not a dict, it is first converted to one with
        flags distributed into appropriate construction variables.
        See :meth:`ParseFlags`.

        Args:
            args: flags to merge
            unique: merge flags rather than appending (default: True).
                When merging, path variables are retained from the front,
                other construction variables from the end.
        """
        if not is_Dict(args):
            args = self.ParseFlags(args)

        if not unique:
            self.Append(**args)
            return

        for key, value in args.items():
            if not value:
                continue
            value = Split(value)
            try:
                orig = self[key]
            except KeyError:
                orig = value
            else:
                if not orig:
                    orig = value
                elif value:
                    # Add orig and value.  The logic here was lifted from
                    # part of env.Append() (see there for a lot of comments
                    # about the order in which things are tried) and is
                    # used mainly to handle coercion of strings to CLVar to
                    # "do the right thing" given (e.g.) an original CCFLAGS
                    # string variable like '-pipe -Wall'.
                    try:
                        orig = orig + value
                    except (KeyError, TypeError):
                        try:
                            add_to_orig = orig.append
                        except AttributeError:
                            value.insert(0, orig)
                            orig = value
                        else:
                            add_to_orig(value)
            t = []
            if key[-4:] == 'PATH':
                ### keep left-most occurence
                for v in orig:
                    if v not in t:
                        t.append(v)
            else:
                ### keep right-most occurence
                for v in orig[::-1]:
                    if v not in t:
                        t.insert(0, v)
            self[key] = t


def default_decide_source(dependency, target, prev_ni, repo_node=None):
    f = SCons.Defaults.DefaultEnvironment().decide_source
    return f(dependency, target, prev_ni, repo_node)


def default_decide_target(dependency, target, prev_ni, repo_node=None):
    f = SCons.Defaults.DefaultEnvironment().decide_target
    return f(dependency, target, prev_ni, repo_node)


def default_copy_from_cache(env, src, dst):
    return SCons.CacheDir.CacheDir.copy_from_cache(env, src, dst)


def default_copy_to_cache(env, src, dst):
    return SCons.CacheDir.CacheDir.copy_to_cache(env, src, dst)


class Base(SubstitutionEnvironment):
    """Base class for "real" construction Environments.

    These are the primary objects used to communicate dependency
    and construction information to the build engine.

    Keyword arguments supplied when the construction Environment
    is created are construction variables used to initialize the
    Environment.
    """

    #######################################################################
    # This is THE class for interacting with the SCons build engine,
    # and it contains a lot of stuff, so we're going to try to keep this
    # a little organized by grouping the methods.
    #######################################################################

    #######################################################################
    # Methods that make an Environment act like a dictionary.  These have
    # the expected standard names for Python mapping objects.  Note that
    # we don't actually make an Environment a subclass of UserDict for
    # performance reasons.  Note also that we only supply methods for
    # dictionary functionality that we actually need and use.
    #######################################################################

    def __init__(
        self,
        platform=None,
        tools=None,
        toolpath=None,
        variables=None,
        parse_flags=None,
        **kw
    ):
        """Initialization of a basic SCons construction environment.

        Sets up special construction variables like BUILDER,
        PLATFORM, etc., and searches for and applies available Tools.

        Note that we do *not* call the underlying base class
        (SubsitutionEnvironment) initialization, because we need to
        initialize things in a very specific order that doesn't work
        with the much simpler base class initialization.
        """
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Environment.Base')
        self._memo = {}
        self.fs = SCons.Node.FS.get_default_fs()
        self.ans = SCons.Node.Alias.default_ans
        self.lookup_list = SCons.Node.arg2nodes_lookups
        self._dict = semi_deepcopy(SCons.Defaults.ConstructionEnvironment)
        self._init_special()
        self.added_methods = []

        # We don't use AddMethod, or define these as methods in this
        # class, because we *don't* want these functions to be bound
        # methods.  They need to operate independently so that the
        # settings will work properly regardless of whether a given
        # target ends up being built with a Base environment or an
        # OverrideEnvironment or what have you.
        self.decide_target = default_decide_target
        self.decide_source = default_decide_source

        self.cache_timestamp_newer = False

        self._dict['BUILDERS'] = BuilderDict(self._dict['BUILDERS'], self)

        if platform is None:
            platform = self._dict.get('PLATFORM', None)
            if platform is None:
                platform = SCons.Platform.Platform()
        if is_String(platform):
            platform = SCons.Platform.Platform(platform)
        self._dict['PLATFORM'] = str(platform)
        platform(self)

        # these should be set by the platform, backstop just in case
        self._dict['HOST_OS'] = self._dict.get('HOST_OS', None)
        self._dict['HOST_ARCH'] = self._dict.get('HOST_ARCH', None)

        # these are not currently set by the platform, give them a default
        self._dict['TARGET_OS'] = self._dict.get('TARGET_OS', None)
        self._dict['TARGET_ARCH'] = self._dict.get('TARGET_ARCH', None)

        # Apply the passed-in and customizable variables to the
        # environment before calling the tools, because they may use
        # some of them during initialization.
        if 'options' in kw:
            # Backwards compatibility:  they may stll be using the
            # old "options" keyword.
            variables = kw['options']
            del kw['options']
        self.Replace(**kw)
        keys = list(kw.keys())
        if variables:
            keys = keys + list(variables.keys())
            variables.Update(self)

        save = {}
        for k in keys:
            try:
                save[k] = self._dict[k]
            except KeyError:
                # No value may have been set if they tried to pass in a
                # reserved variable name like TARGETS.
                pass

        SCons.Tool.Initializers(self)

        if tools is None:
            tools = self._dict.get('TOOLS', None)
            if tools is None:
                tools = ['default']
        apply_tools(self, tools, toolpath)

        # Now restore the passed-in and customized variables
        # to the environment, since the values the user set explicitly
        # should override any values set by the tools.
        for key, val in save.items():
            self._dict[key] = val

        # Finally, apply any flags to be merged in
        if parse_flags:
            self.MergeFlags(parse_flags)

    #######################################################################
    # Utility methods that are primarily for internal use by SCons.
    # These begin with lower-case letters.
    #######################################################################

    def get_builder(self, name):
        """Fetch the builder with the specified name from the environment.
        """
        try:
            return self._dict['BUILDERS'][name]
        except KeyError:
            return None

    def validate_CacheDir_class(self, custom_class=None):
        """Validate the passed custom CacheDir class, or if no args are passed,
        validate the custom CacheDir class from the environment.
        """

        if custom_class is None:
            custom_class = self.get("CACHEDIR_CLASS", SCons.CacheDir.CacheDir)
        if not issubclass(custom_class, SCons.CacheDir.CacheDir):
            raise UserError("Custom CACHEDIR_CLASS %s not derived from CacheDir" % str(custom_class))
        return custom_class

    def get_CacheDir(self):
        try:
            path = self._CacheDir_path
        except AttributeError:
            path = SCons.Defaults.DefaultEnvironment()._CacheDir_path

        cachedir_class = self.validate_CacheDir_class()
        try:
            if (path == self._last_CacheDir_path
                    # this checks if the cachedir class type has changed from what the
                    # instantiated cache dir type is. If the are exactly the same we
                    # can just keep using the existing one, otherwise the user is requesting
                    # something new, so we will re-instantiate below.
                    and type(self._last_CacheDir) is cachedir_class):
                return self._last_CacheDir
        except AttributeError:
            pass

        cd = cachedir_class(path)
        self._last_CacheDir_path = path
        self._last_CacheDir = cd
        return cd

    def get_factory(self, factory, default='File'):
        """Return a factory function for creating Nodes for this
        construction environment.
        """
        name = default
        try:
            is_node = issubclass(factory, SCons.Node.FS.Base)
        except TypeError:
            # The specified factory isn't a Node itself--it's
            # most likely None, or possibly a callable.
            pass
        else:
            if is_node:
                # The specified factory is a Node (sub)class.  Try to
                # return the FS method that corresponds to the Node's
                # name--that is, we return self.fs.Dir if they want a Dir,
                # self.fs.File for a File, etc.
                try: name = factory.__name__
                except AttributeError: pass
                else: factory = None
        if not factory:
            # They passed us None, or we picked up a name from a specified
            # class, so return the FS method.  (Note that we *don't*
            # use our own self.{Dir,File} methods because that would
            # cause env.subst() to be called twice on the file name,
            # interfering with files that have $$ in them.)
            factory = getattr(self.fs, name)
        return factory

    @SCons.Memoize.CountMethodCall
    def _gsm(self):
        try:
            return self._memo['_gsm']
        except KeyError:
            pass

        result = {}

        try:
            scanners = self._dict['SCANNERS']
        except KeyError:
            pass
        else:
            # Reverse the scanner list so that, if multiple scanners
            # claim they can scan the same suffix, earlier scanners
            # in the list will overwrite later scanners, so that
            # the result looks like a "first match" to the user.
            if not is_List(scanners):
                scanners = [scanners]
            else:
                scanners = scanners[:] # copy so reverse() doesn't mod original
            scanners.reverse()
            for scanner in scanners:
                for k in scanner.get_skeys(self):
                    if k and self['PLATFORM'] == 'win32':
                        k = k.lower()
                    result[k] = scanner

        self._memo['_gsm'] = result

        return result

    def get_scanner(self, skey):
        """Find the appropriate scanner given a key (usually a file suffix).
        """
        if skey and self['PLATFORM'] == 'win32':
            skey = skey.lower()
        return self._gsm().get(skey)

    def scanner_map_delete(self, kw=None):
        """Delete the cached scanner map (if we need to).
        """
        try:
            del self._memo['_gsm']
        except KeyError:
            pass

    def _update(self, other):
        """Private method to update an environment's consvar dict directly.

        Bypasses the normal checks that occur when users try to set items.
        """
        self._dict.update(other)

    def _update_onlynew(self, other):
        """Private method to add new items to an environment's consvar dict.

        Only adds items from `other` whose keys do not already appear in
        the existing dict; values from `other` are not used for replacement.
        Bypasses the normal checks that occur when users try to set items.
        """
        for k, v in other.items():
            if k not in self._dict:
                self._dict[k] = v


    def get_src_sig_type(self):
        try:
            return self.src_sig_type
        except AttributeError:
            t = SCons.Defaults.DefaultEnvironment().src_sig_type
            self.src_sig_type = t
            return t

    def get_tgt_sig_type(self):
        try:
            return self.tgt_sig_type
        except AttributeError:
            t = SCons.Defaults.DefaultEnvironment().tgt_sig_type
            self.tgt_sig_type = t
            return t

    #######################################################################
    # Public methods for manipulating an Environment.  These begin with
    # upper-case letters.  The essential characteristic of methods in
    # this section is that they do *not* have corresponding same-named
    # global functions.  For example, a stand-alone Append() function
    # makes no sense, because Append() is all about appending values to
    # an Environment's construction variables.
    #######################################################################

    def Append(self, **kw):
        """Append values to construction variables in an Environment.

        The variable is created if it is not already present.
        """

        kw = copy_non_reserved_keywords(kw)
        for key, val in kw.items():
            try:
                if key == 'CPPDEFINES' and is_String(self._dict[key]):
                    self._dict[key] = [self._dict[key]]
                orig = self._dict[key]
            except KeyError:
                # No existing var in the environment, so set to the new value.
                if key == 'CPPDEFINES' and is_String(val):
                    self._dict[key] = [val]
                else:
                    self._dict[key] = val
                continue

            try:
                # Check if the original looks like a dict: has .update?
                update_dict = orig.update
            except AttributeError:
                try:
                    # Just try to add them together.  This will work
                    # in most cases, when the original and new values
                    # are compatible types.
                    self._dict[key] = orig + val
                except (KeyError, TypeError):
                    try:
                        # Check if the original is a list: has .append?
                        add_to_orig = orig.append
                    except AttributeError:
                        # The original isn't a list, but the new
                        # value is (by process of elimination),
                        # so insert the original in the new value
                        # (if there's one to insert) and replace
                        # the variable with it.
                        if orig:
                            val.insert(0, orig)
                        self._dict[key] = val
                    else:
                        # The original is a list, so append the new
                        # value to it (if there's a value to append).
                        if val:
                            add_to_orig(val)
                continue

            # The original looks like a dictionary, so update it
            # based on what we think the value looks like.
            # We can't just try adding the value because
            # dictionaries don't have __add__() methods, and
            # things like UserList will incorrectly coerce the
            # original dict to a list (which we don't want).
            if is_List(val):
                if key == 'CPPDEFINES':
                    tmp = []
                    for (k, v) in orig.items():
                        if v is not None:
                            tmp.append((k, v))
                        else:
                            tmp.append((k,))
                    orig = tmp
                    orig += val
                    self._dict[key] = orig
                else:
                    for v in val:
                        orig[v] = None
            else:
                try:
                    update_dict(val)
                except (AttributeError, TypeError, ValueError):
                    if is_Dict(val):
                        for k, v in val.items():
                            orig[k] = v
                    else:
                        orig[val] = None

        self.scanner_map_delete(kw)

    def _canonicalize(self, path):
        """Allow Dirs and strings beginning with # for top-relative.

        Note this uses the current env's fs (in self).
        """
        if not is_String(path):  # typically a Dir
            path = str(path)
        if path and path[0] == '#':
            path = str(self.fs.Dir(path))
        return path

    def AppendENVPath(self, name, newpath, envname='ENV',
                      sep=os.pathsep, delete_existing=False):
        """Append path elements to the path *name* in the *envname*
        dictionary for this environment.  Will only add any particular
        path once, and will normpath and normcase all paths to help
        assure this.  This can also handle the case where the env
        variable is a list instead of a string.

        If *delete_existing* is False, a *newpath* element already in the path
        will not be moved to the end (it will be left where it is).
        """

        orig = ''
        if envname in self._dict and name in self._dict[envname]:
            orig = self._dict[envname][name]

        nv = AppendPath(orig, newpath, sep, delete_existing, canonicalize=self._canonicalize)

        if envname not in self._dict:
            self._dict[envname] = {}

        self._dict[envname][name] = nv

    def AppendUnique(self, delete_existing=False, **kw):
        """Append values to existing construction variables
        in an Environment, if they're not already there.
        If delete_existing is True, removes existing values first, so
        values move to end.
        """
        kw = copy_non_reserved_keywords(kw)
        for key, val in kw.items():
            if is_List(val):
                val = _delete_duplicates(val, delete_existing)
            if key not in self._dict or self._dict[key] in ('', None):
                self._dict[key] = val
            elif is_Dict(self._dict[key]) and is_Dict(val):
                self._dict[key].update(val)
            elif is_List(val):
                dk = self._dict[key]
                if key == 'CPPDEFINES':
                    tmp = []
                    for i in val:
                        if is_List(i):
                            if len(i) >= 2:
                                tmp.append((i[0], i[1]))
                            else:
                                tmp.append((i[0],))
                        elif is_Tuple(i):
                            tmp.append(i)
                        else:
                            tmp.append((i,))
                    val = tmp
                    # Construct a list of (key, value) tuples.
                    if is_Dict(dk):
                        tmp = []
                        for (k, v) in dk.items():
                            if v is not None:
                                tmp.append((k, v))
                            else:
                                tmp.append((k,))
                        dk = tmp
                    elif is_String(dk):
                        dk = [(dk,)]
                    else:
                        tmp = []
                        for i in dk:
                            if is_List(i):
                                if len(i) >= 2:
                                    tmp.append((i[0], i[1]))
                                else:
                                    tmp.append((i[0],))
                            elif is_Tuple(i):
                                tmp.append(i)
                            else:
                                tmp.append((i,))
                        dk = tmp
                else:
                    if not is_List(dk):
                        dk = [dk]
                if delete_existing:
                    dk = [x for x in dk if x not in val]
                else:
                    val = [x for x in val if x not in dk]
                self._dict[key] = dk + val
            else:
                dk = self._dict[key]
                if is_List(dk):
                    if key == 'CPPDEFINES':
                        tmp = []
                        for i in dk:
                            if is_List(i):
                                if len(i) >= 2:
                                    tmp.append((i[0], i[1]))
                                else:
                                    tmp.append((i[0],))
                            elif is_Tuple(i):
                                tmp.append(i)
                            else:
                                tmp.append((i,))
                        dk = tmp
                        # Construct a list of (key, value) tuples.
                        if is_Dict(val):
                            tmp = []
                            for (k, v) in val.items():
                                if v is not None:
                                    tmp.append((k, v))
                                else:
                                    tmp.append((k,))
                            val = tmp
                        elif is_String(val):
                            val = [(val,)]
                        if delete_existing:
                            dk = list(filter(lambda x, val=val: x not in val, dk))
                            self._dict[key] = dk + val
                        else:
                            dk = [x for x in dk if x not in val]
                            self._dict[key] = dk + val
                    else:
                        # By elimination, val is not a list.  Since dk is a
                        # list, wrap val in a list first.
                        if delete_existing:
                            dk = list(filter(lambda x, val=val: x not in val, dk))
                            self._dict[key] = dk + [val]
                        else:
                            if val not in dk:
                                self._dict[key] = dk + [val]
                else:
                    if key == 'CPPDEFINES':
                        if is_String(dk):
                            dk = [dk]
                        elif is_Dict(dk):
                            tmp = []
                            for (k, v) in dk.items():
                                if v is not None:
                                    tmp.append((k, v))
                                else:
                                    tmp.append((k,))
                            dk = tmp
                        if is_String(val):
                            if val in dk:
                                val = []
                            else:
                                val = [val]
                        elif is_Dict(val):
                            tmp = []
                            for i,j in val.items():
                                if j is not None:
                                    tmp.append((i,j))
                                else:
                                    tmp.append(i)
                            val = tmp
                    if delete_existing:
                        dk = [x for x in dk if x not in val]
                    self._dict[key] = dk + val
        self.scanner_map_delete(kw)

    def Clone(self, tools=[], toolpath=None, parse_flags = None, **kw):
        """Return a copy of a construction Environment.

        The copy is like a Python "deep copy"--that is, independent
        copies are made recursively of each objects--except that
        a reference is copied when an object is not deep-copyable
        (like a function).  There are no references to any mutable
        objects in the original Environment.
        """

        builders = self._dict.get('BUILDERS', {})

        clone = copy.copy(self)
        # BUILDERS is not safe to do a simple copy
        clone._dict = semi_deepcopy_dict(self._dict, ['BUILDERS'])
        clone._dict['BUILDERS'] = BuilderDict(builders, clone)

        # Check the methods added via AddMethod() and re-bind them to
        # the cloned environment.  Only do this if the attribute hasn't
        # been overwritten by the user explicitly and still points to
        # the added method.
        clone.added_methods = []
        for mw in self.added_methods:
            if mw == getattr(self, mw.name):
                clone.added_methods.append(mw.clone(clone))

        clone._memo = {}

        # Apply passed-in variables before the tools
        # so the tools can use the new variables
        kw = copy_non_reserved_keywords(kw)
        new = {}
        for key, value in kw.items():
            new[key] = SCons.Subst.scons_subst_once(value, self, key)
        clone.Replace(**new)

        apply_tools(clone, tools, toolpath)

        # apply them again in case the tools overwrote them
        clone.Replace(**new)

        # Finally, apply any flags to be merged in
        if parse_flags:
            clone.MergeFlags(parse_flags)

        if SCons.Debug.track_instances: logInstanceCreation(self, 'Environment.EnvironmentClone')
        return clone

    def _changed_build(self, dependency, target, prev_ni, repo_node=None):
        if dependency.changed_state(target, prev_ni, repo_node):
            return 1
        return self.decide_source(dependency, target, prev_ni, repo_node)

    def _changed_content(self, dependency, target, prev_ni, repo_node=None):
        return dependency.changed_content(target, prev_ni, repo_node)

    def _changed_source(self, dependency, target, prev_ni, repo_node=None):
        target_env = dependency.get_build_env()
        type = target_env.get_tgt_sig_type()
        if type == 'source':
            return target_env.decide_source(dependency, target, prev_ni, repo_node)
        else:
            return target_env.decide_target(dependency, target, prev_ni, repo_node)

    def _changed_timestamp_then_content(self, dependency, target, prev_ni, repo_node=None):
        return dependency.changed_timestamp_then_content(target, prev_ni, repo_node)

    def _changed_timestamp_newer(self, dependency, target, prev_ni, repo_node=None):
        return dependency.changed_timestamp_newer(target, prev_ni, repo_node)

    def _changed_timestamp_match(self, dependency, target, prev_ni, repo_node=None):
        return dependency.changed_timestamp_match(target, prev_ni, repo_node)

    def Decider(self, function):
        self.cache_timestamp_newer = False
        if function in ('MD5', 'content'):
            # TODO: Handle if user requests MD5 and not content with deprecation notice
            function = self._changed_content
        elif function in ('MD5-timestamp', 'content-timestamp'):
            function = self._changed_timestamp_then_content
        elif function in ('timestamp-newer', 'make'):
            function = self._changed_timestamp_newer
            self.cache_timestamp_newer = True
        elif function == 'timestamp-match':
            function = self._changed_timestamp_match
        elif not callable(function):
            raise UserError("Unknown Decider value %s" % repr(function))

        # We don't use AddMethod because we don't want to turn the
        # function, which only expects three arguments, into a bound
        # method, which would add self as an initial, fourth argument.
        self.decide_target = function
        self.decide_source = function


    def Detect(self, progs):
        """Return the first available program from one or more possibilities.

        Args:
            progs (str or list): one or more command names to check for

        """
        if not is_List(progs):
            progs = [progs]
        for prog in progs:
            path = self.WhereIs(prog)
            if path: return prog
        return None


    def Dictionary(self, *args):
        r"""Return construction variables from an environment.

        Args:
          \*args (optional): variable names to look up

        Returns:
          If `args` omitted, the dictionary of all construction variables.
          If one arg, the corresponding value is returned.
          If more than one arg, a list of values is returned.

        Raises:
          KeyError: if any of `args` is not in the construction environment.

        """
        if not args:
            return self._dict
        dlist = [self._dict[x] for x in args]
        if len(dlist) == 1:
            dlist = dlist[0]
        return dlist


    def Dump(self, key=None, format='pretty'):
        """ Return construction variables serialized to a string.

        Args:
          key (optional): if None, format the whole dict of variables.
            Else format the value of `key` (Default value = None)
          format (str, optional): specify the format to serialize to.
            `"pretty"` generates a pretty-printed string,
            `"json"` a JSON-formatted string.
            (Default value = `"pretty"`)

        """
        if key:
            cvars = self.Dictionary(key)
        else:
            cvars = self.Dictionary()

        fmt = format.lower()

        if fmt == 'pretty':
            import pprint
            pp = pprint.PrettyPrinter(indent=2)

            # TODO: pprint doesn't do a nice job on path-style values
            # if the paths contain spaces (i.e. Windows), because the
            # algorithm tries to break lines on spaces, while breaking
            # on the path-separator would be more "natural". Is there
            # a better way to format those?
            return pp.pformat(cvars)

        elif fmt == 'json':
            import json
            def non_serializable(obj):
                return str(type(obj).__qualname__)
            return json.dumps(cvars, indent=4, default=non_serializable)
        else:
            raise ValueError("Unsupported serialization format: %s." % fmt)


    def FindIxes(self, paths, prefix, suffix):
        """Search a list of paths for something that matches the prefix and suffix.

        Args:
          paths: the list of paths or nodes.
          prefix: construction variable for the prefix.
          suffix: construction variable for the suffix.

        Returns: the matched path or None

        """

        suffix = self.subst('$'+suffix)
        prefix = self.subst('$'+prefix)

        for path in paths:
            name = os.path.basename(str(path))
            if name[:len(prefix)] == prefix and name[-len(suffix):] == suffix:
                return path


    def ParseConfig(self, command, function=None, unique=True):
        """Parse the result of running a command to update construction vars.

        Use ``function`` to parse the output of running ``command``
        in order to modify the current environment.

        Args:
            command: a string or a list of strings representing a command
              and its arguments.
            function: called to process the result of ``command``, which will
              be passed as ``args``.  If ``function`` is omitted or ``None``,
              :meth:`MergeFlags` is used. Takes 3 args ``(env, args, unique)``
            unique: whether no duplicate values are allowed (default true)
        """
        if function is None:

            def parse_conf(env, cmd, unique=unique):
                return env.MergeFlags(cmd, unique)

            function = parse_conf
        if is_List(command):
            command = ' '.join(command)
        command = self.subst(command)
        return function(self, self.backtick(command), unique)


    def ParseDepends(self, filename, must_exist=None, only_one=False):
        """
        Parse a mkdep-style file for explicit dependencies.  This is
        completely abusable, and should be unnecessary in the "normal"
        case of proper SCons configuration, but it may help make
        the transition from a Make hierarchy easier for some people
        to swallow.  It can also be genuinely useful when using a tool
        that can write a .d file, but for which writing a scanner would
        be too complicated.
        """
        filename = self.subst(filename)
        try:
            with open(filename, 'r') as fp:
                lines = LogicalLines(fp).readlines()
        except IOError:
            if must_exist:
                raise
            return
        lines = [l for l in lines if l[0] != '#']
        tdlist = []
        for line in lines:
            try:
                target, depends = line.split(':', 1)
            except (AttributeError, ValueError):
                # Throws AttributeError if line isn't a string.  Can throw
                # ValueError if line doesn't split into two or more elements.
                pass
            else:
                tdlist.append((target.split(), depends.split()))
        if only_one:
            targets = []
            for td in tdlist:
                targets.extend(td[0])
            if len(targets) > 1:
                raise UserError(
                            "More than one dependency target found in `%s':  %s"
                                            % (filename, targets))
        for target, depends in tdlist:
            self.Depends(target, depends)

    def Platform(self, platform):
        platform = self.subst(platform)
        return SCons.Platform.Platform(platform)(self)

    def Prepend(self, **kw):
        """Prepend values to construction variables in an Environment.

        The variable is created if it is not already present.
        """

        kw = copy_non_reserved_keywords(kw)
        for key, val in kw.items():
            try:
                orig = self._dict[key]
            except KeyError:
                # No existing var in the environment so set to the new value.
                self._dict[key] = val
                continue

            try:
                # Check if the original looks like a dict: has .update?
                update_dict = orig.update
            except AttributeError:
                try:
                    # Just try to add them together.  This will work
                    # in most cases, when the original and new values
                    # are compatible types.
                    self._dict[key] = val + orig
                except (KeyError, TypeError):
                    try:
                        # Check if the added value is a list: has .append?
                        add_to_val = val.append
                    except AttributeError:
                        # The added value isn't a list, but the
                        # original is (by process of elimination),
                        # so insert the the new value in the original
                        # (if there's one to insert).
                        if val:
                            orig.insert(0, val)
                    else:
                        # The added value is a list, so append
                        # the original to it (if there's a value
                        # to append) and replace the original.
                        if orig:
                            add_to_val(orig)
                        self._dict[key] = val
                continue

            # The original looks like a dictionary, so update it
            # based on what we think the value looks like.
            # We can't just try adding the value because
            # dictionaries don't have __add__() methods, and
            # things like UserList will incorrectly coerce the
            # original dict to a list (which we don't want).
            if is_List(val):
                for v in val:
                    orig[v] = None
            else:
                try:
                    update_dict(val)
                except (AttributeError, TypeError, ValueError):
                    if is_Dict(val):
                        for k, v in val.items():
                            orig[k] = v
                    else:
                        orig[val] = None

        self.scanner_map_delete(kw)

    def PrependENVPath(self, name, newpath, envname='ENV',
                       sep=os.pathsep, delete_existing=True):
        """Prepend path elements to the path *name* in the *envname*
        dictionary for this environment.  Will only add any particular
        path once, and will normpath and normcase all paths to help
        assure this.  This can also handle the case where the env
        variable is a list instead of a string.

        If *delete_existing* is False, a *newpath* component already in the path
        will not be moved to the front (it will be left where it is).
        """

        orig = ''
        if envname in self._dict and name in self._dict[envname]:
            orig = self._dict[envname][name]

        nv = PrependPath(orig, newpath, sep, delete_existing,
                                    canonicalize=self._canonicalize)

        if envname not in self._dict:
            self._dict[envname] = {}

        self._dict[envname][name] = nv

    def PrependUnique(self, delete_existing=False, **kw):
        """Prepend values to existing construction variables
        in an Environment, if they're not already there.
        If delete_existing is True, removes existing values first, so
        values move to front.
        """
        kw = copy_non_reserved_keywords(kw)
        for key, val in kw.items():
            if is_List(val):
                val = _delete_duplicates(val, not delete_existing)
            if key not in self._dict or self._dict[key] in ('', None):
                self._dict[key] = val
            elif is_Dict(self._dict[key]) and is_Dict(val):
                self._dict[key].update(val)
            elif is_List(val):
                dk = self._dict[key]
                if not is_List(dk):
                    dk = [dk]
                if delete_existing:
                    dk = [x for x in dk if x not in val]
                else:
                    val = [x for x in val if x not in dk]
                self._dict[key] = val + dk
            else:
                dk = self._dict[key]
                if is_List(dk):
                    # By elimination, val is not a list.  Since dk is a
                    # list, wrap val in a list first.
                    if delete_existing:
                        dk = [x for x in dk if x not in val]
                        self._dict[key] = [val] + dk
                    else:
                        if val not in dk:
                            self._dict[key] = [val] + dk
                else:
                    if delete_existing:
                        dk = [x for x in dk if x not in val]
                    self._dict[key] = val + dk
        self.scanner_map_delete(kw)

    def Replace(self, **kw):
        """Replace existing construction variables in an Environment
        with new construction variables and/or values.
        """
        try:
            kwbd = kw['BUILDERS']
        except KeyError:
            pass
        else:
            kwbd = BuilderDict(kwbd,self)
            del kw['BUILDERS']
            self.__setitem__('BUILDERS', kwbd)
        kw = copy_non_reserved_keywords(kw)
        self._update(semi_deepcopy(kw))
        self.scanner_map_delete(kw)

    def ReplaceIxes(self, path, old_prefix, old_suffix, new_prefix, new_suffix):
        """
        Replace old_prefix with new_prefix and old_suffix with new_suffix.

        env - Environment used to interpolate variables.
        path - the path that will be modified.
        old_prefix - construction variable for the old prefix.
        old_suffix - construction variable for the old suffix.
        new_prefix - construction variable for the new prefix.
        new_suffix - construction variable for the new suffix.
        """
        old_prefix = self.subst('$'+old_prefix)
        old_suffix = self.subst('$'+old_suffix)

        new_prefix = self.subst('$'+new_prefix)
        new_suffix = self.subst('$'+new_suffix)

        dir,name = os.path.split(str(path))
        if name[:len(old_prefix)] == old_prefix:
            name = name[len(old_prefix):]
        if name[-len(old_suffix):] == old_suffix:
            name = name[:-len(old_suffix)]
        return os.path.join(dir, new_prefix+name+new_suffix)

    def SetDefault(self, **kw):
        for k in list(kw.keys()):
            if k in self._dict:
                del kw[k]
        self.Replace(**kw)

    def _find_toolpath_dir(self, tp):
        return self.fs.Dir(self.subst(tp)).srcnode().get_abspath()

    def Tool(self, tool, toolpath=None, **kwargs) -> SCons.Tool.Tool:
        """Find and run tool module *tool*.

        .. versionchanged:: 4.2
           returns the tool module rather than ``None``.
        """
        if is_String(tool):
            tool = self.subst(tool)
            if toolpath is None:
                toolpath = self.get('toolpath', [])
            toolpath = list(map(self._find_toolpath_dir, toolpath))
            tool = SCons.Tool.Tool(tool, toolpath, **kwargs)
        tool(self)
        return tool

    def WhereIs(self, prog, path=None, pathext=None, reject=None):
        """Find prog in the path. """
        if not prog:  # nothing to search for, just give up
            return None
        if path is None:
            try:
                path = self['ENV']['PATH']
            except KeyError:
                pass
        elif is_String(path):
            path = self.subst(path)
        if pathext is None:
            try:
                pathext = self['ENV']['PATHEXT']
            except KeyError:
                pass
        elif is_String(pathext):
            pathext = self.subst(pathext)
        prog = CLVar(self.subst(prog))  # support "program --with-args"
        path = WhereIs(prog[0], path, pathext, reject)
        if path:
            return path
        return None

    #######################################################################
    # Public methods for doing real "SCons stuff" (manipulating
    # dependencies, setting attributes on targets, etc.).  These begin
    # with upper-case letters.  The essential characteristic of methods
    # in this section is that they all *should* have corresponding
    # same-named global functions.
    #######################################################################

    def Action(self, *args, **kw):
        def subst_string(a, self=self):
            if is_String(a):
                a = self.subst(a)
            return a
        nargs = list(map(subst_string, args))
        nkw = self.subst_kw(kw)
        return SCons.Action.Action(*nargs, **nkw)

    def AddPreAction(self, files, action):
        nodes = self.arg2nodes(files, self.fs.Entry)
        action = SCons.Action.Action(action)
        uniq = {}
        for executor in [n.get_executor() for n in nodes]:
            uniq[executor] = 1
        for executor in uniq.keys():
            executor.add_pre_action(action)
        return nodes

    def AddPostAction(self, files, action):
        nodes = self.arg2nodes(files, self.fs.Entry)
        action = SCons.Action.Action(action)
        uniq = {}
        for executor in [n.get_executor() for n in nodes]:
            uniq[executor] = 1
        for executor in uniq.keys():
            executor.add_post_action(action)
        return nodes

    def Alias(self, target, source=[], action=None, **kw):
        tlist = self.arg2nodes(target, self.ans.Alias)
        if not is_List(source):
            source = [source]
        source = [_f for _f in source if _f]

        if not action:
            if not source:
                # There are no source files and no action, so just
                # return a target list of classic Alias Nodes, without
                # any builder.  The externally visible effect is that
                # this will make the wrapping Script.BuildTask class
                # say that there's "Nothing to be done" for this Alias,
                # instead of that it's "up to date."
                return tlist

            # No action, but there are sources.  Re-call all the target
            # builders to add the sources to each target.
            result = []
            for t in tlist:
                bld = t.get_builder(AliasBuilder)
                result.extend(bld(self, t, source))
            return result

        nkw = self.subst_kw(kw)
        nkw.update({
            'action'            : SCons.Action.Action(action),
            'source_factory'    : self.fs.Entry,
            'multi'             : 1,
            'is_explicit'       : None,
        })
        bld = SCons.Builder.Builder(**nkw)

        # Apply the Builder separately to each target so that the Aliases
        # stay separate.  If we did one "normal" Builder call with the
        # whole target list, then all of the target Aliases would be
        # associated under a single Executor.
        result = []
        for t in tlist:
            # Calling the convert() method will cause a new Executor to be
            # created from scratch, so we have to explicitly initialize
            # it with the target's existing sources, plus our new ones,
            # so nothing gets lost.
            b = t.get_builder()
            if b is None or b is AliasBuilder:
                b = bld
            else:
                nkw['action'] = b.action + action
                b = SCons.Builder.Builder(**nkw)
            t.convert()
            result.extend(b(self, t, t.sources + source))
        return result

    def AlwaysBuild(self, *targets):
        tlist = []
        for t in targets:
            tlist.extend(self.arg2nodes(t, self.fs.Entry))
        for t in tlist:
            t.set_always_build()
        return tlist

    def Builder(self, **kw):
        nkw = self.subst_kw(kw)
        return SCons.Builder.Builder(**nkw)

    def CacheDir(self, path, custom_class=None):
        if path is not None:
            path = self.subst(path)
        self._CacheDir_path = path

        if custom_class:
            self['CACHEDIR_CLASS'] = self.validate_CacheDir_class(custom_class)

        if SCons.Action.execute_actions:
            # Only initialize the CacheDir if  -n/-no_exec was NOT specified.
            # Now initialized the CacheDir and prevent a race condition which can
            # happen when there's no existing cache dir and you are building with
            # multiple threads, but initializing it before the task walk starts
            self.get_CacheDir()

    def Clean(self, targets, files):
        global CleanTargets
        tlist = self.arg2nodes(targets, self.fs.Entry)
        flist = self.arg2nodes(files, self.fs.Entry)
        for t in tlist:
            try:
                CleanTargets[t].extend(flist)
            except KeyError:
                CleanTargets[t] = flist

    def Configure(self, *args, **kw):
        nargs = [self]
        if args:
            nargs = nargs + self.subst_list(args)[0]
        nkw = self.subst_kw(kw)
        nkw['_depth'] = kw.get('_depth', 0) + 1
        try:
            nkw['custom_tests'] = self.subst_kw(nkw['custom_tests'])
        except KeyError:
            pass
        return SCons.SConf.SConf(*nargs, **nkw)

    def Command(self, target, source, action, **kw):
        """Builds the supplied target files from the supplied
        source files using the supplied action.  Action may
        be any type that the Builder constructor will accept
        for an action."""
        bkw = {
            'action': action,
            'target_factory': self.fs.Entry,
            'source_factory': self.fs.Entry,
        }
        # source scanner
        try:
            bkw['source_scanner'] = kw['source_scanner']
        except KeyError:
            pass
        else:
            del kw['source_scanner']

        # target scanner
        try:
            bkw['target_scanner'] = kw['target_scanner']
        except KeyError:
            pass
        else:
            del kw['target_scanner']

        # source factory
        try:
            bkw['source_factory'] = kw['source_factory']
        except KeyError:
            pass
        else:
            del kw['source_factory']

        # target factory
        try:
            bkw['target_factory'] = kw['target_factory']
        except KeyError:
            pass
        else:
            del kw['target_factory']

        bld = SCons.Builder.Builder(**bkw)
        return bld(self, target, source, **kw)

    def Depends(self, target, dependency):
        """Explicity specify that 'target's depend on 'dependency'."""
        tlist = self.arg2nodes(target, self.fs.Entry)
        dlist = self.arg2nodes(dependency, self.fs.Entry)
        for t in tlist:
            t.add_dependency(dlist)
        return tlist

    def Dir(self, name, *args, **kw):
        """
        """
        s = self.subst(name)
        if is_Sequence(s):
            result=[]
            for e in s:
                result.append(self.fs.Dir(e, *args, **kw))
            return result
        return self.fs.Dir(s, *args, **kw)

    def PyPackageDir(self, modulename):
        s = self.subst(modulename)
        if is_Sequence(s):
            result=[]
            for e in s:
                result.append(self.fs.PyPackageDir(e))
            return result
        return self.fs.PyPackageDir(s)

    def NoClean(self, *targets):
        """Tags a target so that it will not be cleaned by -c"""
        tlist = []
        for t in targets:
            tlist.extend(self.arg2nodes(t, self.fs.Entry))
        for t in tlist:
            t.set_noclean()
        return tlist

    def NoCache(self, *targets):
        """Tags a target so that it will not be cached"""
        tlist = []
        for t in targets:
            tlist.extend(self.arg2nodes(t, self.fs.Entry))
        for t in tlist:
            t.set_nocache()
        return tlist

    def Entry(self, name, *args, **kw):
        """
        """
        s = self.subst(name)
        if is_Sequence(s):
            result=[]
            for e in s:
                result.append(self.fs.Entry(e, *args, **kw))
            return result
        return self.fs.Entry(s, *args, **kw)

    def Environment(self, **kw):
        return SCons.Environment.Environment(**self.subst_kw(kw))

    def Execute(self, action, *args, **kw):
        """Directly execute an action through an Environment
        """
        action = self.Action(action, *args, **kw)
        result = action([], [], self)
        if isinstance(result, BuildError):
            errstr = result.errstr
            if result.filename:
                errstr = result.filename + ': ' + errstr
            sys.stderr.write("scons: *** %s\n" % errstr)
            return result.status
        else:
            return result

    def File(self, name, *args, **kw):
        """
        """
        s = self.subst(name)
        if is_Sequence(s):
            result=[]
            for e in s:
                result.append(self.fs.File(e, *args, **kw))
            return result
        return self.fs.File(s, *args, **kw)

    def FindFile(self, file, dirs):
        file = self.subst(file)
        nodes = self.arg2nodes(dirs, self.fs.Dir)
        return SCons.Node.FS.find_file(file, tuple(nodes))

    def Flatten(self, sequence):
        return flatten(sequence)

    def GetBuildPath(self, files):
        result = list(map(str, self.arg2nodes(files, self.fs.Entry)))
        if is_List(files):
            return result
        else:
            return result[0]

    def Glob(self, pattern, ondisk=True, source=False, strings=False, exclude=None):
        return self.fs.Glob(self.subst(pattern), ondisk, source, strings, exclude)

    def Ignore(self, target, dependency):
        """Ignore a dependency."""
        tlist = self.arg2nodes(target, self.fs.Entry)
        dlist = self.arg2nodes(dependency, self.fs.Entry)
        for t in tlist:
            t.add_ignore(dlist)
        return tlist

    def Literal(self, string):
        return SCons.Subst.Literal(string)

    def Local(self, *targets):
        ret = []
        for targ in targets:
            if isinstance(targ, SCons.Node.Node):
                targ.set_local()
                ret.append(targ)
            else:
                for t in self.arg2nodes(targ, self.fs.Entry):
                   t.set_local()
                   ret.append(t)
        return ret

    def Precious(self, *targets):
        tlist = []
        for t in targets:
            tlist.extend(self.arg2nodes(t, self.fs.Entry))
        for t in tlist:
            t.set_precious()
        return tlist

    def Pseudo(self, *targets):
        tlist = []
        for t in targets:
            tlist.extend(self.arg2nodes(t, self.fs.Entry))
        for t in tlist:
            t.set_pseudo()
        return tlist

    def Repository(self, *dirs, **kw):
        dirs = self.arg2nodes(list(dirs), self.fs.Dir)
        self.fs.Repository(*dirs, **kw)

    def Requires(self, target, prerequisite):
        """Specify that 'prerequisite' must be built before 'target',
        (but 'target' does not actually depend on 'prerequisite'
        and need not be rebuilt if it changes)."""
        tlist = self.arg2nodes(target, self.fs.Entry)
        plist = self.arg2nodes(prerequisite, self.fs.Entry)
        for t in tlist:
            t.add_prerequisite(plist)
        return tlist

    def Scanner(self, *args, **kw):
        nargs = []
        for arg in args:
            if is_String(arg):
                arg = self.subst(arg)
            nargs.append(arg)
        nkw = self.subst_kw(kw)
        return SCons.Scanner.ScannerBase(*nargs, **nkw)

    def SConsignFile(self, name=SCons.SConsign.current_sconsign_filename(), dbm_module=None):
        if name is not None:
            name = self.subst(name)
            if not os.path.isabs(name):
                name = os.path.join(str(self.fs.SConstruct_dir), name)
        if name:
            name = os.path.normpath(name)
            sconsign_dir = os.path.dirname(name)
            if sconsign_dir and not os.path.exists(sconsign_dir):
                self.Execute(SCons.Defaults.Mkdir(sconsign_dir))
        SCons.SConsign.File(name, dbm_module)

    def SideEffect(self, side_effect, target):
        """Tell scons that side_effects are built as side
        effects of building targets."""
        side_effects = self.arg2nodes(side_effect, self.fs.Entry)
        targets = self.arg2nodes(target, self.fs.Entry)

        added_side_effects = []
        for side_effect in side_effects:
            if side_effect.multiple_side_effect_has_builder():
                raise UserError("Multiple ways to build the same target were specified for: %s" % str(side_effect))
            side_effect.add_source(targets)
            side_effect.side_effect = 1
            self.Precious(side_effect)
            added = False
            for target in targets:
                if side_effect not in target.side_effects:
                    target.side_effects.append(side_effect)
                    added = True
            if added:
                added_side_effects.append(side_effect)
        return added_side_effects

    def Split(self, arg):
        """This function converts a string or list into a list of strings
        or Nodes.  This makes things easier for users by allowing files to
        be specified as a white-space separated list to be split.

        The input rules are:
            - A single string containing names separated by spaces. These will be
              split apart at the spaces.
            - A single Node instance
            - A list containing either strings or Node instances. Any strings
              in the list are not split at spaces.

        In all cases, the function returns a list of Nodes and strings."""

        if is_List(arg):
            return list(map(self.subst, arg))
        elif is_String(arg):
            return self.subst(arg).split()
        else:
            return [self.subst(arg)]

    def Value(self, value, built_value=None, name=None):
        """Return a Value (Python expression) node.

        .. versionchanged:: 4.0
           the *name* parameter was added.
        """
        return SCons.Node.Python.ValueWithMemo(value, built_value, name)

    def VariantDir(self, variant_dir, src_dir, duplicate=1):
        variant_dir = self.arg2nodes(variant_dir, self.fs.Dir)[0]
        src_dir = self.arg2nodes(src_dir, self.fs.Dir)[0]
        self.fs.VariantDir(variant_dir, src_dir, duplicate)

    def FindSourceFiles(self, node='.') -> list:
        """Return a list of all source files."""
        node = self.arg2nodes(node, self.fs.Entry)[0]

        sources = []
        def build_source(ss):
            for s in ss:
                if isinstance(s, SCons.Node.FS.Dir):
                    build_source(s.all_children())
                elif s.has_builder():
                    build_source(s.sources)
                elif isinstance(s.disambiguate(), SCons.Node.FS.File):
                    sources.append(s)
        build_source(node.all_children())

        def final_source(node):
            while node != node.srcnode():
              node = node.srcnode()
            return node
        sources = list(map(final_source, sources))
        # remove duplicates
        return list(set(sources))

    def FindInstalledFiles(self):
        """ returns the list of all targets of the Install and InstallAs Builder.
        """
        from SCons.Tool import install
        if install._UNIQUE_INSTALLED_FILES is None:
            install._UNIQUE_INSTALLED_FILES = uniquer_hashables(install._INSTALLED_FILES)
        return install._UNIQUE_INSTALLED_FILES


class OverrideEnvironment(Base):
    """A proxy that overrides variables in a wrapped construction
    environment by returning values from an overrides dictionary in
    preference to values from the underlying subject environment.

    This is a lightweight (I hope) proxy that passes through most use of
    attributes to the underlying Environment.Base class, but has just
    enough additional methods defined to act like a real construction
    environment with overridden values.  It can wrap either a Base
    construction environment, or another OverrideEnvironment, which
    can in turn nest arbitrary OverrideEnvironments...

    Note that we do *not* call the underlying base class
    (SubsitutionEnvironment) initialization, because we get most of those
    from proxying the attributes of the subject construction environment.
    But because we subclass SubstitutionEnvironment, this class also
    has inherited arg2nodes() and subst*() methods; those methods can't
    be proxied because they need *this* object's methods to fetch the
    values from the overrides dictionary.
    """

    def __init__(self, subject, overrides=None):
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Environment.OverrideEnvironment')
        self.__dict__['__subject'] = subject
        if overrides is None:
            self.__dict__['overrides'] = {}
        else:
            self.__dict__['overrides'] = overrides

    # Methods that make this class act like a proxy.
    def __getattr__(self, name):
        attr = getattr(self.__dict__['__subject'], name)
        # Here we check if attr is one of the Wrapper classes. For
        # example when a pseudo-builder is being called from an
        # OverrideEnvironment.
        #
        # These wrappers when they're constructed capture the
        # Environment they are being constructed with and so will not
        # have access to overrided values. So we rebuild them with the
        # OverrideEnvironment so they have access to overrided values.
        if isinstance(attr, MethodWrapper):
            return attr.clone(self)
        else:
            return attr

    def __setattr__(self, name, value):
        setattr(self.__dict__['__subject'], name, value)

    # Methods that make this class act like a dictionary.
    def __getitem__(self, key):
        try:
            return self.__dict__['overrides'][key]
        except KeyError:
            return self.__dict__['__subject'].__getitem__(key)

    def __setitem__(self, key, value):
        if not is_valid_construction_var(key):
            raise UserError("Illegal construction variable `%s'" % key)
        self.__dict__['overrides'][key] = value

    def __delitem__(self, key):
        try:
            del self.__dict__['overrides'][key]
        except KeyError:
            deleted = 0
        else:
            deleted = 1
        try:
            result = self.__dict__['__subject'].__delitem__(key)
        except KeyError:
            if not deleted:
                raise
            result = None
        return result

    def get(self, key, default=None):
        """Emulates the get() method of dictionaries."""
        try:
            return self.__dict__['overrides'][key]
        except KeyError:
            return self.__dict__['__subject'].get(key, default)

    def __contains__(self, key):
        if key in self.__dict__['overrides']:
            return True
        return key in self.__dict__['__subject']

    def Dictionary(self, *args):
        d = self.__dict__['__subject'].Dictionary().copy()
        d.update(self.__dict__['overrides'])
        if not args:
            return d
        dlist = [d[x] for x in args]
        if len(dlist) == 1:
            dlist = dlist[0]
        return dlist

    def items(self):
        """Emulates the items() method of dictionaries."""
        return self.Dictionary().items()

    def keys(self):
        """Emulates the keys() method of dictionaries."""
        return self.Dictionary().keys()

    def values(self):
        """Emulates the values() method of dictionaries."""
        return self.Dictionary().values()

    def setdefault(self, key, default=None):
        """Emulates the setdefault() method of dictionaries."""
        try:
            return self.__getitem__(key)
        except KeyError:
            self.__dict__['overrides'][key] = default
            return default

    # Overridden private construction environment methods.
    def _update(self, other):
        self.__dict__['overrides'].update(other)

    def _update_onlynew(self, other):
        """Update a dict with new keys.

        Unlike the .update method, if the key is already present,
        it is not replaced.
        """
        for k, v in other.items():
            if k not in self.__dict__['overrides']:
                self.__dict__['overrides'][k] = v

    def gvars(self):
        return self.__dict__['__subject'].gvars()

    def lvars(self):
        lvars = self.__dict__['__subject'].lvars()
        lvars.update(self.__dict__['overrides'])
        return lvars

    # Overridden public construction environment methods.
    def Replace(self, **kw):
        kw = copy_non_reserved_keywords(kw)
        self.__dict__['overrides'].update(semi_deepcopy(kw))


# The entry point that will be used by the external world
# to refer to a construction environment.  This allows the wrapper
# interface to extend a construction environment for its own purposes
# by subclassing SCons.Environment.Base and then assigning the
# class to SCons.Environment.Environment.

Environment = Base


def NoSubstitutionProxy(subject):
    """
    An entry point for returning a proxy subclass instance that overrides
    the subst*() methods so they don't actually perform construction
    variable substitution.  This is specifically intended to be the shim
    layer in between global function calls (which don't want construction
    variable substitution) and the DefaultEnvironment() (which would
    substitute variables if left to its own devices).

    We have to wrap this in a function that allows us to delay definition of
    the class until it's necessary, so that when it subclasses Environment
    it will pick up whatever Environment subclass the wrapper interface
    might have assigned to SCons.Environment.Environment.
    """
    class _NoSubstitutionProxy(Environment):
        def __init__(self, subject):
            self.__dict__['__subject'] = subject

        def __getattr__(self, name):
            return getattr(self.__dict__['__subject'], name)

        def __setattr__(self, name, value):
            return setattr(self.__dict__['__subject'], name, value)

        def executor_to_lvars(self, kwdict):
            if 'executor' in kwdict:
                kwdict['lvars'] = kwdict['executor'].get_lvars()
                del kwdict['executor']
            else:
                kwdict['lvars'] = {}

        def raw_to_mode(self, mapping):
            try:
                raw = mapping['raw']
            except KeyError:
                pass
            else:
                del mapping['raw']
                mapping['mode'] = raw

        def subst(self, string, *args, **kwargs):
            return string

        def subst_kw(self, kw, *args, **kwargs):
            return kw

        def subst_list(self, string, *args, **kwargs):
            nargs = (string, self,) + args
            nkw = kwargs.copy()
            nkw['gvars'] = {}
            self.executor_to_lvars(nkw)
            self.raw_to_mode(nkw)
            return SCons.Subst.scons_subst_list(*nargs, **nkw)

        def subst_target_source(self, string, *args, **kwargs):
            nargs = (string, self,) + args
            nkw = kwargs.copy()
            nkw['gvars'] = {}
            self.executor_to_lvars(nkw)
            self.raw_to_mode(nkw)
            return SCons.Subst.scons_subst(*nargs, **nkw)

    return _NoSubstitutionProxy(subject)

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/EnvironmentTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import SCons.compat

import copy
import io
import os
import sys
import unittest
from collections import UserDict as UD, UserList as UL

import TestCmd

import SCons.Warnings
from SCons.Environment import (
    Environment,
    NoSubstitutionProxy,
    OverrideEnvironment,
    SubstitutionEnvironment,
    is_valid_construction_var,
)
from SCons.Util import CLVar
from SCons.SConsign import current_sconsign_filename


def diff_env(env1, env2):
    s1 = "env1 = {\n"
    s2 = "env2 = {\n"
    d = {}
    for k in list(env1._dict.keys()) + list(env2._dict.keys()):
        d[k] = None
    for k in sorted(d.keys()):
        if k in env1:
           if k in env2:
               if env1[k] != env2[k]:
                   s1 = s1 + "    " + repr(k) + " : " + repr(env1[k]) + "\n"
                   s2 = s2 + "    " + repr(k) + " : " + repr(env2[k]) + "\n"
           else:
               s1 = s1 + "    " + repr(k) + " : " + repr(env1[k]) + "\n"
        elif k in env2:
           s2 = s2 + "    " + repr(k) + " : " + repr(env2[k]) + "\n"
    s1 = s1 + "}\n"
    s2 = s2 + "}\n"
    return s1 + s2

def diff_dict(d1, d2):
    s1 = "d1 = {\n"
    s2 = "d2 = {\n"
    d = {}
    for k in list(d1.keys()) + list(d2.keys()):
        d[k] = None
    for k in sorted(d.keys()):
        if k in d1:
           if k in d2:
               if d1[k] != d2[k]:
                   s1 = s1 + "    " + repr(k) + " : " + repr(d1[k]) + "\n"
                   s2 = s2 + "    " + repr(k) + " : " + repr(d2[k]) + "\n"
           else:
               s1 = s1 + "    " + repr(k) + " : " + repr(d1[k]) + "\n"
        elif k in d2:
           s2 = s2 + "    " + repr(k) + " : " + repr(d2[k]) + "\n"
    s1 = s1 + "}\n"
    s2 = s2 + "}\n"
    return s1 + s2

called_it = {}
built_it = {}

class Builder(SCons.Builder.BuilderBase):
    """A dummy Builder class for testing purposes.  "Building"
    a target is simply setting a value in the dictionary.
    """
    def __init__(self, name = None):
        self.name = name

    def __call__(self, env, target=None, source=None, **kw):
        global called_it
        called_it['target'] = target
        called_it['source'] = source
        called_it.update(kw)

    def execute(self, target = None, **kw):
        global built_it
        built_it[target] = 1



scanned_it = {}

class Scanner:
    """A dummy Scanner class for testing purposes.  "Scanning"
    a target is simply setting a value in the dictionary.
    """
    def __init__(self, name, skeys=[]):
        self.name = name
        self.skeys = skeys

    def __call__(self, filename):
        global scanned_it
        scanned_it[filename] = 1

    def __eq__(self, other):
        try:
            return self.__dict__ == other.__dict__
        except AttributeError:
            return False

    def get_skeys(self, env):
        return self.skeys

    def __str__(self):
        return self.name


class DummyNode:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return self.name
    def rfile(self):
        return self
    def get_subst_proxy(self):
        return self

def test_tool( env ):
    env['_F77INCFLAGS'] = '${_concat(INCPREFIX, F77PATH, INCSUFFIX, __env__, RDirs, TARGET, SOURCE, affect_signature=False)}'

class TestEnvironmentFixture:
    def TestEnvironment(self, *args, **kw):
        if not kw or 'tools' not in kw:
            kw['tools'] = [test_tool]
        default_keys = { 'CC' : 'cc',
                         'CCFLAGS' : '-DNDEBUG',
                         'ENV' : { 'TMP' : '/tmp' } }
        for key, value in default_keys.items():
            if key not in kw:
                kw[key] = value
        if 'BUILDERS' not in kw:
            static_obj = SCons.Builder.Builder(action = {},
                                               emitter = {},
                                               suffix = '.o',
                                               single_source = 1)
            kw['BUILDERS'] = {'Object' : static_obj}
            static_obj.add_action('.cpp', 'fake action')

        env = Environment(*args, **kw)
        return env

class SubstitutionTestCase(unittest.TestCase):

    def test___init__(self):
        """Test initializing a SubstitutionEnvironment."""
        env = SubstitutionEnvironment()
        assert '__env__' not in env

    def test___cmp__(self):
        """Test comparing SubstitutionEnvironments."""
        env1 = SubstitutionEnvironment(XXX = 'x')
        env2 = SubstitutionEnvironment(XXX = 'x')
        env3 = SubstitutionEnvironment(XXX = 'xxx')
        env4 = SubstitutionEnvironment(XXX = 'x', YYY = 'x')

        assert env1 == env2
        assert env1 != env3
        assert env1 != env4

    def test___delitem__(self):
        """Test deleting a variable from a SubstitutionEnvironment."""
        env1 = SubstitutionEnvironment(XXX = 'x', YYY = 'y')
        env2 = SubstitutionEnvironment(XXX = 'x')
        del env1['YYY']
        assert env1 == env2

    def test___getitem__(self):
        """Test fetching a variable from a SubstitutionEnvironment."""
        env = SubstitutionEnvironment(XXX = 'x')
        assert env['XXX'] == 'x', env['XXX']

    def test___setitem__(self):
        """Test setting a variable in a SubstitutionEnvironment."""
        env1 = SubstitutionEnvironment(XXX = 'x')
        env2 = SubstitutionEnvironment(XXX = 'x', YYY = 'y')
        env1['YYY'] = 'y'
        assert env1 == env2

    def test_get(self):
        """Test the SubstitutionEnvironment get() method."""
        env = SubstitutionEnvironment(XXX = 'x')
        assert env.get('XXX') == 'x', env.get('XXX')
        assert env.get('YYY') is None, env.get('YYY')

    def test_contains(self):
        """Test the SubstitutionEnvironment __contains__() method."""
        env = SubstitutionEnvironment(XXX = 'x')
        assert 'XXX' in env
        assert 'YYY' not in env

    def test_keys(self):
        """Test the SubstitutionEnvironment keys() method."""
        testdata = {'XXX': 'x', 'YYY': 'y'}
        env = SubstitutionEnvironment(**testdata)
        keys = list(env.keys())
        assert len(keys) == 2, keys
        for k in testdata.keys():
            assert k in keys, keys

    def test_values(self):
        """Test the SubstitutionEnvironment values() method."""
        testdata = {'XXX': 'x', 'YYY': 'y'}
        env = SubstitutionEnvironment(**testdata)
        values = list(env.values())
        assert len(values) == 2, values
        for v in testdata.values():
            assert v in values, values

    def test_items(self):
        """Test the SubstitutionEnvironment items() method."""
        testdata = {'XXX': 'x', 'YYY': 'y'}
        env = SubstitutionEnvironment(**testdata)
        items = list(env.items())
        assert len(items) == 2, items
        for k, v in testdata.items():
            assert (k, v) in items, items

    def test_setdefault(self):
        """Test the SubstitutionEnvironment setdefault() method."""
        env = SubstitutionEnvironment(XXX = 'x')
        assert env.setdefault('XXX', 'z') == 'x', env['XXX']
        assert env.setdefault('YYY', 'y') == 'y', env['YYY']
        assert 'YYY' in env

    def test_arg2nodes(self):
        """Test the arg2nodes method."""
        env = SubstitutionEnvironment()
        dict = {}
        class X(SCons.Node.Node):
            pass
        def Factory(name, directory = None, create = 1, dict=dict, X=X):
            if name not in dict:
                dict[name] = X()
                dict[name].name = name
            return dict[name]

        nodes = env.arg2nodes("Util.py UtilTests.py", Factory)
        assert len(nodes) == 1, nodes
        assert isinstance(nodes[0], X)
        assert nodes[0].name == "Util.py UtilTests.py", nodes[0].name

        nodes = env.arg2nodes(["Util.py", "UtilTests.py"], Factory)
        assert len(nodes) == 2, nodes
        assert isinstance(nodes[0], X)
        assert isinstance(nodes[1], X)
        assert nodes[0].name == "Util.py", nodes[0].name
        assert nodes[1].name == "UtilTests.py", nodes[1].name

        n1 = Factory("Util.py")
        nodes = env.arg2nodes([n1, "UtilTests.py"], Factory)
        assert len(nodes) == 2, nodes
        assert isinstance(nodes[0], X)
        assert isinstance(nodes[1], X)
        assert nodes[0].name == "Util.py", nodes[0].name
        assert nodes[1].name == "UtilTests.py", nodes[1].name

        class SConsNode(SCons.Node.Node):
            pass
        nodes = env.arg2nodes(SConsNode())
        assert len(nodes) == 1, nodes
        assert isinstance(nodes[0], SConsNode), nodes[0]

        class OtherNode:
            pass
        nodes = env.arg2nodes(OtherNode())
        assert len(nodes) == 1, nodes
        assert isinstance(nodes[0], OtherNode), nodes[0]

        def lookup_a(str, F=Factory):
            if str[0] == 'a':
                n = F(str)
                n.a = 1
                return n
            else:
                return None

        def lookup_b(str, F=Factory):
            if str[0] == 'b':
                n = F(str)
                n.b = 1
                return n
            else:
                return None

        env_ll = SubstitutionEnvironment()
        env_ll.lookup_list = [lookup_a, lookup_b]

        nodes = env_ll.arg2nodes(['aaa', 'bbb', 'ccc'], Factory)
        assert len(nodes) == 3, nodes

        assert nodes[0].name == 'aaa', nodes[0]
        assert nodes[0].a == 1, nodes[0]
        assert not hasattr(nodes[0], 'b'), nodes[0]

        assert nodes[1].name == 'bbb'
        assert not hasattr(nodes[1], 'a'), nodes[1]
        assert nodes[1].b == 1, nodes[1]

        assert nodes[2].name == 'ccc'
        assert not hasattr(nodes[2], 'a'), nodes[1]
        assert not hasattr(nodes[2], 'b'), nodes[1]

        def lookup_bbbb(str, F=Factory):
            if str == 'bbbb':
                n = F(str)
                n.bbbb = 1
                return n
            else:
                return None

        def lookup_c(str, F=Factory):
            if str[0] == 'c':
                n = F(str)
                n.c = 1
                return n
            else:
                return None

        nodes = env.arg2nodes(['bbbb', 'ccc'], Factory,
                                     [lookup_c, lookup_bbbb, lookup_b])
        assert len(nodes) == 2, nodes

        assert nodes[0].name == 'bbbb'
        assert not hasattr(nodes[0], 'a'), nodes[1]
        assert not hasattr(nodes[0], 'b'), nodes[1]
        assert nodes[0].bbbb == 1, nodes[1]
        assert not hasattr(nodes[0], 'c'), nodes[0]

        assert nodes[1].name == 'ccc'
        assert not hasattr(nodes[1], 'a'), nodes[1]
        assert not hasattr(nodes[1], 'b'), nodes[1]
        assert not hasattr(nodes[1], 'bbbb'), nodes[0]
        assert nodes[1].c == 1, nodes[1]

    def test_arg2nodes_target_source(self):
        """Test the arg2nodes method with target= and source= keywords
        """
        targets = [DummyNode('t1'), DummyNode('t2')]
        sources = [DummyNode('s1'), DummyNode('s2')]
        env = SubstitutionEnvironment()
        nodes = env.arg2nodes(['${TARGET}-a',
                               '${SOURCE}-b',
                               '${TARGETS[1]}-c',
                               '${SOURCES[1]}-d'],
                              DummyNode,
                              target=targets,
                              source=sources)
        names = [n.name for n in nodes]
        assert names == ['t1-a', 's1-b', 't2-c', 's2-d'], names

    def test_gvars(self):
        """Test the base class gvars() method"""
        env = SubstitutionEnvironment()
        gvars = env.gvars()
        assert gvars == {}, gvars

    def test_lvars(self):
        """Test the base class lvars() method"""
        env = SubstitutionEnvironment()
        lvars = env.lvars()
        assert lvars == {}, lvars

    def test_subst(self):
        """Test substituting construction variables within strings

        Check various combinations, including recursive expansion
        of variables into other variables.
        """
        env = SubstitutionEnvironment(AAA = 'a', BBB = 'b')
        mystr = env.subst("$AAA ${AAA}A $BBBB $BBB")
        assert mystr == "a aA b", mystr

        # Changed the tests below to reflect a bug fix in
        # subst()
        env = SubstitutionEnvironment(AAA = '$BBB', BBB = 'b', BBBA = 'foo')
        mystr = env.subst("$AAA ${AAA}A ${AAA}B $BBB")
        assert mystr == "b bA bB b", mystr

        env = SubstitutionEnvironment(AAA = '$BBB', BBB = '$CCC', CCC = 'c')
        mystr = env.subst("$AAA ${AAA}A ${AAA}B $BBB")
        assert mystr == "c cA cB c", mystr

        # Lists:
        env = SubstitutionEnvironment(AAA = ['a', 'aa', 'aaa'])
        mystr = env.subst("$AAA")
        assert mystr == "a aa aaa", mystr

        # Tuples:
        env = SubstitutionEnvironment(AAA = ('a', 'aa', 'aaa'))
        mystr = env.subst("$AAA")
        assert mystr == "a aa aaa", mystr

        t1 = DummyNode('t1')
        t2 = DummyNode('t2')
        s1 = DummyNode('s1')
        s2 = DummyNode('s2')

        env = SubstitutionEnvironment(AAA = 'aaa')
        s = env.subst('$AAA $TARGET $SOURCES', target=[t1, t2], source=[s1, s2])
        assert s == "aaa t1 s1 s2", s
        s = env.subst('$AAA $TARGETS $SOURCE', target=[t1, t2], source=[s1, s2])
        assert s == "aaa t1 t2 s1", s

        # Test callables in the SubstitutionEnvironment
        def foo(target, source, env, for_signature):
            assert str(target) == 't', target
            assert str(source) == 's', source
            return env["FOO"]

        env = SubstitutionEnvironment(BAR=foo, FOO='baz')
        t = DummyNode('t')
        s = DummyNode('s')

        subst = env.subst('test $BAR', target=t, source=s)
        assert subst == 'test baz', subst

        # Test not calling callables in the SubstitutionEnvironment
        if 0:
            # This will take some serious surgery to subst() and
            # subst_list(), so just leave these tests out until we can
            # do that.
            def bar(arg):
                pass

            env = SubstitutionEnvironment(BAR=bar, FOO='$BAR')

            subst = env.subst('$BAR', call=None)
            assert subst is bar, subst

            subst = env.subst('$FOO', call=None)
            assert subst is bar, subst

    def test_subst_kw(self):
        """Test substituting construction variables within dictionaries"""
        env = SubstitutionEnvironment(AAA = 'a', BBB = 'b')
        kw = env.subst_kw({'$AAA' : 'aaa', 'bbb' : '$BBB'})
        assert len(kw) == 2, kw
        assert kw['a'] == 'aaa', kw['a']
        assert kw['bbb'] == 'b', kw['bbb']

    def test_subst_list(self):
        """Test substituting construction variables in command lists
        """
        env = SubstitutionEnvironment(AAA = 'a', BBB = 'b')
        l = env.subst_list("$AAA ${AAA}A $BBBB $BBB")
        assert l == [["a", "aA", "b"]], l

        # Changed the tests below to reflect a bug fix in
        # subst()
        env = SubstitutionEnvironment(AAA = '$BBB', BBB = 'b', BBBA = 'foo')
        l = env.subst_list("$AAA ${AAA}A ${AAA}B $BBB")
        assert l == [["b", "bA", "bB", "b"]], l

        env = SubstitutionEnvironment(AAA = '$BBB', BBB = '$CCC', CCC = 'c')
        l = env.subst_list("$AAA ${AAA}A ${AAA}B $BBB")
        assert l == [["c", "cA", "cB", "c"]], l

        env = SubstitutionEnvironment(AAA = '$BBB', BBB = '$CCC', CCC = [ 'a', 'b\nc' ])
        lst = env.subst_list([ "$AAA", "B $CCC" ])
        assert lst == [[ "a", "b"], ["c", "B a", "b"], ["c"]], lst

        t1 = DummyNode('t1')
        t2 = DummyNode('t2')
        s1 = DummyNode('s1')
        s2 = DummyNode('s2')

        env = SubstitutionEnvironment(AAA = 'aaa')
        s = env.subst_list('$AAA $TARGET $SOURCES', target=[t1, t2], source=[s1, s2])
        assert s == [["aaa", "t1", "s1", "s2"]], s
        s = env.subst_list('$AAA $TARGETS $SOURCE', target=[t1, t2], source=[s1, s2])
        assert s == [["aaa", "t1", "t2", "s1"]], s

        # Test callables in the SubstitutionEnvironment
        def foo(target, source, env, for_signature):
            assert str(target) == 't', target
            assert str(source) == 's', source
            return env["FOO"]

        env = SubstitutionEnvironment(BAR=foo, FOO='baz')
        t = DummyNode('t')
        s = DummyNode('s')

        lst = env.subst_list('test $BAR', target=t, source=s)
        assert lst == [['test', 'baz']], lst

        # Test not calling callables in the SubstitutionEnvironment
        if 0:
            # This will take some serious surgery to subst() and
            # subst_list(), so just leave these tests out until we can
            # do that.
            def bar(arg):
                pass

            env = SubstitutionEnvironment(BAR=bar, FOO='$BAR')

            subst = env.subst_list('$BAR', call=None)
            assert subst is bar, subst

            subst = env.subst_list('$FOO', call=None)
            assert subst is bar, subst

    def test_subst_path(self):
        """Test substituting a path list
        """
        class MyProxy:
            def __init__(self, val):
                self.val = val
            def get(self):
                return self.val + '-proxy'

        class MyNode:
            def __init__(self, val):
                self.val = val
            def get_subst_proxy(self):
                return self
            def __str__(self):
                return self.val

        class MyObj:
            def get(self):
                return self

        env = SubstitutionEnvironment(FOO='foo',
                                      BAR='bar',
                                      LIST=['one', 'two'],
                                      PROXY=MyProxy('my1'))

        r = env.subst_path('$FOO')
        assert r == ['foo'], r

        r = env.subst_path(['$FOO', 'xxx', '$BAR'])
        assert r == ['foo', 'xxx', 'bar'], r

        r = env.subst_path(['$FOO', '$LIST', '$BAR'])
        assert list(map(str, r)) == ['foo', 'one two', 'bar'], r

        r = env.subst_path(['$FOO', '$TARGET', '$SOURCE', '$BAR'])
        assert r == ['foo', '', '', 'bar'], r

        r = env.subst_path(['$FOO', '$TARGET', '$BAR'], target=MyNode('ttt'))
        assert list(map(str, r)) == ['foo', 'ttt', 'bar'], r

        r = env.subst_path(['$FOO', '$SOURCE', '$BAR'], source=MyNode('sss'))
        assert list(map(str, r)) == ['foo', 'sss', 'bar'], r

        n = MyObj()

        r = env.subst_path(['$PROXY', MyProxy('my2'), n])
        assert r == ['my1-proxy', 'my2-proxy', n], r

        class StringableObj:
            def __init__(self, s):
                self.s = s
            def __str__(self):
                return self.s

        env = SubstitutionEnvironment(FOO=StringableObj("foo"),
                          BAR=StringableObj("bar"))

        r = env.subst_path([ "${FOO}/bar", "${BAR}/baz" ])
        assert r == [ "foo/bar", "bar/baz" ], r

        r = env.subst_path([ "bar/${FOO}", "baz/${BAR}" ])
        assert r == [ "bar/foo", "baz/bar" ], r

        r = env.subst_path([ "bar/${FOO}/bar", "baz/${BAR}/baz" ])
        assert r == [ "bar/foo/bar", "baz/bar/baz" ], r

    def test_subst_target_source(self):
        """Test the base environment subst_target_source() method"""
        env = SubstitutionEnvironment(AAA = 'a', BBB = 'b')
        mystr = env.subst_target_source("$AAA ${AAA}A $BBBB $BBB")
        assert mystr == "a aA b", mystr

    def test_backtick(self):
        """Test the backtick() method for capturing command output"""
        env = SubstitutionEnvironment()

        test = TestCmd.TestCmd(workdir = '')
        test.write('stdout.py', """\
import sys
sys.stdout.write('this came from stdout.py\\n')
sys.exit(0)
""")
        test.write('stderr.py', """\
import sys
sys.stderr.write('this came from stderr.py\\n')
sys.exit(0)
""")
        test.write('fail.py', """\
import sys
sys.exit(1)
""")
        test.write('echo.py', """\
import os, sys
sys.stdout.write(os.environ['ECHO'] + '\\n')
sys.exit(0)
""")

        save_stderr = sys.stderr

        python = '"' + sys.executable + '"'

        try:
            sys.stderr = io.StringIO()
            cmd = '%s %s' % (python, test.workpath('stdout.py'))
            output = env.backtick(cmd)
            errout = sys.stderr.getvalue()
            assert output == 'this came from stdout.py\n', output
            assert errout == '', errout

            sys.stderr = io.StringIO()
            cmd = '%s %s' % (python, test.workpath('stderr.py'))
            output = env.backtick(cmd)
            errout = sys.stderr.getvalue()
            assert output == '', output
            assert errout == 'this came from stderr.py\n', errout

            sys.stderr = io.StringIO()
            cmd = '%s %s' % (python, test.workpath('fail.py'))
            try:
                env.backtick(cmd)
            except OSError as e:
                assert str(e) == "'%s' exited 1" % cmd, str(e)
            else:
                self.fail("did not catch expected OSError")

            sys.stderr = io.StringIO()
            cmd = '%s %s' % (python, test.workpath('echo.py'))
            env['ENV'] = os.environ.copy()
            env['ENV']['ECHO'] = 'this came from ECHO'
            output = env.backtick(cmd)
            errout = sys.stderr.getvalue()
            assert output == 'this came from ECHO\n', output
            assert errout == '', errout

        finally:
            sys.stderr = save_stderr

    def test_AddMethod(self):
        """Test the AddMethod() method"""
        env = SubstitutionEnvironment(FOO = 'foo')

        def func(self):
            return 'func-' + self['FOO']

        assert not hasattr(env, 'func')
        env.AddMethod(func)
        r = env.func()
        assert r == 'func-foo', r

        assert not hasattr(env, 'bar')
        env.AddMethod(func, 'bar')
        r = env.bar()
        assert r == 'func-foo', r

        def func2(self, arg=''):
            return 'func2-' + self['FOO'] + arg

        env.AddMethod(func2)
        r = env.func2()
        assert r == 'func2-foo', r
        r = env.func2('-xxx')
        assert r == 'func2-foo-xxx', r

        env.AddMethod(func2, 'func')
        r = env.func()
        assert r == 'func2-foo', r
        r = env.func('-yyy')
        assert r == 'func2-foo-yyy', r

        # Test that clones of clones correctly re-bind added methods.
        env1 = Environment(FOO = '1')
        env1.AddMethod(func2)
        env2 = env1.Clone(FOO = '2')
        env3 = env2.Clone(FOO = '3')
        env4 = env3.Clone(FOO = '4')
        r = env1.func2()
        assert r == 'func2-1', r
        r = env2.func2()
        assert r == 'func2-2', r
        r = env3.func2()
        assert r == 'func2-3', r
        r = env4.func2()
        assert r == 'func2-4', r

        # Test that clones don't re-bind an attribute that the user set.
        env1 = Environment(FOO = '1')
        env1.AddMethod(func2)
        def replace_func2():
            return 'replace_func2'
        env1.func2 = replace_func2
        env2 = env1.Clone(FOO = '2')
        r = env2.func2()
        assert r == 'replace_func2', r

        # Test clone rebinding if using global AddMethod.
        env1 = Environment(FOO='1')
        SCons.Util.AddMethod(env1, func2)
        r = env1.func2()
        assert r == 'func2-1', r
        r = env1.func2('-xxx')
        assert r == 'func2-1-xxx', r
        env2 = env1.Clone(FOO='2')
        r = env2.func2()
        assert r == 'func2-2', r


    def test_Override(self):
        """Test overriding construction variables"""
        env = SubstitutionEnvironment(ONE=1, TWO=2, THREE=3, FOUR=4)
        assert env['ONE'] == 1, env['ONE']
        assert env['TWO'] == 2, env['TWO']
        assert env['THREE'] == 3, env['THREE']
        assert env['FOUR'] == 4, env['FOUR']

        env2 = env.Override({'TWO'   : '10',
                             'THREE' :'x $THREE y',
                             'FOUR'  : ['x', '$FOUR', 'y']})
        assert env2['ONE'] == 1, env2['ONE']
        assert env2['TWO'] == '10', env2['TWO']
        assert env2['THREE'] == 'x 3 y', env2['THREE']
        assert env2['FOUR'] == ['x', 4, 'y'], env2['FOUR']

        assert env['ONE'] == 1, env['ONE']
        assert env['TWO'] == 2, env['TWO']
        assert env['THREE'] == 3, env['THREE']
        assert env['FOUR'] == 4, env['FOUR']

        env2.Replace(ONE = "won")
        assert env2['ONE'] == "won", env2['ONE']
        assert env['ONE'] == 1, env['ONE']

    def test_ParseFlags(self):
        """Test the ParseFlags() method
        """
        env = SubstitutionEnvironment()

        empty = {
            'ASFLAGS'       : [],
            'CFLAGS'        : [],
            'CCFLAGS'       : [],
            'CXXFLAGS'      : [],
            'CPPDEFINES'    : [],
            'CPPFLAGS'      : [],
            'CPPPATH'       : [],
            'FRAMEWORKPATH' : [],
            'FRAMEWORKS'    : [],
            'LIBPATH'       : [],
            'LIBS'          : [],
            'LINKFLAGS'     : [],
            'RPATH'         : [],
        }

        d = env.ParseFlags(None)
        assert d == empty, d

        d = env.ParseFlags('')
        assert d == empty, d

        d = env.ParseFlags([])
        assert d == empty, d

        s = (
            "-I/usr/include/fum -I bar -X "
            '-I"C:\\Program Files\\ASCEND\\include" '
            "-L/usr/fax -L foo -lxxx -l yyy "
            '-L"C:\\Program Files\\ASCEND" -lascend '
            "-Wa,-as -Wl,-link "
            "-Wl,-rpath=rpath1 "
            "-Wl,-R,rpath2 "
            "-Wl,-Rrpath3 "
            "-Wp,-cpp "
            "-std=c99 "
            "-std=c++0x "
            "-framework Carbon "
            "-frameworkdir=fwd1 "
            "-Ffwd2 "
            "-F fwd3 "
            "-dylib_file foo-dylib "
            "-pthread "
            "-fmerge-all-constants "
            "-fopenmp "
            "-mno-cygwin -mwindows "
            "-arch i386 "
            "-isysroot /tmp "
            "-iquote /usr/include/foo1 "
            "-isystem /usr/include/foo2 "
            "-idirafter /usr/include/foo3 "
            "-imacros /usr/include/foo4 "
            "-include /usr/include/foo5 "
            "--param l1-cache-size=32 --param l2-cache-size=6144 "
            "+DD64 "
            "-DFOO -DBAR=value -D BAZ "
            "-fsanitize=memory "
            "-fsanitize-address-use-after-return "
        )

        d = env.ParseFlags(s)

        assert d['ASFLAGS'] == ['-as'], d['ASFLAGS']
        assert d['CFLAGS']  == ['-std=c99']
        assert d['CCFLAGS'] == ['-X', '-Wa,-as',
                                '-pthread', '-fmerge-all-constants',
                                '-fopenmp', '-mno-cygwin',
                                ('-arch', 'i386'), ('-isysroot', '/tmp'),
                                ('-iquote', '/usr/include/foo1'),
                                ('-isystem', '/usr/include/foo2'),
                                ('-idirafter', '/usr/include/foo3'),
                                ('-imacros', env.fs.File('/usr/include/foo4')),
                                ('-include', env.fs.File('/usr/include/foo5')),
                                ('--param', 'l1-cache-size=32'), ('--param', 'l2-cache-size=6144'),
                                '+DD64',
                                '-fsanitize=memory',
                                '-fsanitize-address-use-after-return'], repr(d['CCFLAGS'])
        assert d['CXXFLAGS'] == ['-std=c++0x'], repr(d['CXXFLAGS'])
        assert d['CPPDEFINES'] == ['FOO', ['BAR', 'value'], 'BAZ'], d['CPPDEFINES']
        assert d['CPPFLAGS'] == ['-Wp,-cpp'], d['CPPFLAGS']
        assert d['CPPPATH'] == ['/usr/include/fum',
                                'bar',
                                'C:\\Program Files\\ASCEND\\include'], d['CPPPATH']
        assert d['FRAMEWORKPATH'] == ['fwd1', 'fwd2', 'fwd3'], d['FRAMEWORKPATH']
        assert d['FRAMEWORKS'] == ['Carbon'], d['FRAMEWORKS']
        assert d['LIBPATH'] == ['/usr/fax',
                                'foo',
                                'C:\\Program Files\\ASCEND'], d['LIBPATH']
        LIBS = list(map(str, d['LIBS']))
        assert LIBS == ['xxx', 'yyy', 'ascend'], (d['LIBS'], LIBS)
        assert d['LINKFLAGS'] == ['-Wl,-link',
                                  '-dylib_file', 'foo-dylib',
                                  '-pthread', '-fmerge-all-constants', '-fopenmp',
                                  '-mno-cygwin', '-mwindows',
                                  ('-arch', 'i386'),
                                  ('-isysroot', '/tmp'),
                                  '+DD64',
                                  '-fsanitize=memory',
                                  '-fsanitize-address-use-after-return'], repr(d['LINKFLAGS'])
        assert d['RPATH'] == ['rpath1', 'rpath2', 'rpath3'], d['RPATH']


    def test_MergeFlags(self):
        """Test the MergeFlags() method."""

        env = SubstitutionEnvironment()
        # does not set flag if value empty
        env.MergeFlags('')
        assert 'CCFLAGS' not in env, env['CCFLAGS']
        # merges value if flag did not exist
        env.MergeFlags('-X')
        assert env['CCFLAGS'] == ['-X'], env['CCFLAGS']

        # avoid SubstitutionEnvironment for these, has no .Append method,
        # which is needed for unique=False test
        env = Environment(CCFLAGS="")
        # merge with existing but empty flag
        env.MergeFlags('-X')
        assert env['CCFLAGS'] == ['-X'], env['CCFLAGS']
        # default Unique=True enforces no dupes
        env.MergeFlags('-X')
        assert env['CCFLAGS'] == ['-X'], env['CCFLAGS']
        # Unique=False allows dupes
        env.MergeFlags('-X', unique=False)
        assert env['CCFLAGS'] == ['-X', '-X'], env['CCFLAGS']

        # merge from a dict with list values
        env = SubstitutionEnvironment(B='b')
        env.MergeFlags({'A': ['aaa'], 'B': ['bb', 'bbb']})
        assert env['A'] == ['aaa'], env['A']
        assert env['B'] == ['b', 'bb', 'bbb'], env['B']

        # issue #2961: merge from a dict with string values
        env = SubstitutionEnvironment(B='b')
        env.MergeFlags({'A': 'aaa', 'B': 'bb bbb'})
        assert env['A'] == ['aaa'], env['A']
        assert env['B'] == ['b', 'bb', 'bbb'], env['B']

        # issue #3665: if merging dict which is a compound object
        # (i.e. value can be lists, etc.), the value object should not
        # be modified. per the issue, this happened if key not in env.
        env = SubstitutionEnvironment()
        try:
            del env['CFLAGS']  # just to be sure
        except KeyError:
            pass
        flags = {'CFLAGS': ['-pipe', '-pthread', '-g']}
        import copy

        saveflags = copy.deepcopy(flags)
        env.MergeFlags(flags)
        self.assertEqual(flags, saveflags)


class BaseTestCase(unittest.TestCase,TestEnvironmentFixture):

    reserved_variables = [
        'CHANGED_SOURCES',
        'CHANGED_TARGETS',
        'SOURCE',
        'SOURCES',
        'TARGET',
        'TARGETS',
        'UNCHANGED_SOURCES',
        'UNCHANGED_TARGETS',
    ]

    def test___init__(self):
        """Test construction Environment creation

        Create two with identical arguments and check that
        they compare the same.
        """
        env1 = self.TestEnvironment(XXX = 'x', YYY = 'y')
        env2 = self.TestEnvironment(XXX = 'x', YYY = 'y')
        assert env1 == env2, diff_env(env1, env2)

        assert '__env__' not in env1
        assert '__env__' not in env2

    def test_variables(self):
        """Test that variables only get applied once."""
        class FakeOptions:
            def __init__(self, key, val):
                self.calls = 0
                self.key = key
                self.val = val
            def keys(self):
                return [self.key]
            def Update(self, env):
                env[self.key] = self.val
                self.calls = self.calls + 1

        o = FakeOptions('AAA', 'fake_opt')
        env = Environment(variables=o, AAA='keyword_arg')
        assert o.calls == 1, o.calls
        assert env['AAA'] == 'fake_opt', env['AAA']

    def test_get(self):
        """Test the get() method."""
        env = self.TestEnvironment(aaa = 'AAA')

        x = env.get('aaa')
        assert x == 'AAA', x
        x = env.get('aaa', 'XXX')
        assert x == 'AAA', x
        x = env.get('bbb')
        assert x is None, x
        x = env.get('bbb', 'XXX')
        assert x == 'XXX', x

    def test_Builder_calls(self):
        """Test Builder calls through different environments
        """
        global called_it

        b1 = Builder()
        b2 = Builder()

        env = Environment()
        env.Replace(BUILDERS = { 'builder1' : b1,
                                 'builder2' : b2 })
        called_it = {}
        env.builder1('in1')
        assert called_it['target'] is None, called_it
        assert called_it['source'] == ['in1'], called_it

        called_it = {}
        env.builder2(source = 'in2', xyzzy = 1)
        assert called_it['target'] is None, called_it
        assert called_it['source'] == ['in2'], called_it
        assert called_it['xyzzy'] == 1, called_it

        called_it = {}
        env.builder1(foo = 'bar')
        assert called_it['foo'] == 'bar', called_it
        assert called_it['target'] is None, called_it
        assert called_it['source'] is None, called_it

    def test_BuilderWrapper_attributes(self):
        """Test getting and setting of BuilderWrapper attributes."""
        b1 = Builder()
        b2 = Builder()
        e1 = Environment()
        e2 = Environment()

        e1.Replace(BUILDERS={'b': b1})
        bw = e1.b

        assert bw.env is e1
        bw.env = e2
        assert bw.env is e2

        assert bw.builder is b1
        bw.builder = b2
        assert bw.builder is b2

        self.assertRaises(AttributeError, getattr, bw, 'foobar')
        bw.foobar = 42
        assert bw.foobar == 42

    # This unit test is currently disabled because we don't think the
    # underlying method it tests (Environment.BuilderWrapper.execute())
    # is necessary, but we're leaving the code here for now in case
    # that's mistaken.
    def _DO_NOT_test_Builder_execs(self):
        """Test Builder execution through different environments

        One environment is initialized with a single
        Builder object, one with a list of a single Builder
        object, and one with a list of two Builder objects.
        """
        global built_it

        b1 = Builder()
        b2 = Builder()

        built_it = {}
        env3 = Environment()
        env3.Replace(BUILDERS = { 'builder1' : b1,
                                  'builder2' : b2 })
        env3.builder1.execute(target = 'out1')
        env3.builder2.execute(target = 'out2')
        env3.builder1.execute(target = 'out3')
        assert built_it['out1']
        assert built_it['out2']
        assert built_it['out3']

        env4 = env3.Clone()
        assert env4.builder1.env is env4, "builder1.env (%s) == env3 (%s)?" % (
env4.builder1.env, env3)
        assert env4.builder2.env is env4, "builder2.env (%s) == env3 (%s)?" % (
env4.builder1.env, env3)

        # Now test BUILDERS as a dictionary.
        built_it = {}
        env5 = self.TestEnvironment(BUILDERS={ 'foo' : b1 })
        env5['BUILDERS']['bar'] = b2
        env5.foo.execute(target='out1')
        env5.bar.execute(target='out2')
        assert built_it['out1']
        assert built_it['out2']

        built_it = {}
        env6 = Environment()
        env6['BUILDERS'] = { 'foo' : b1,
                             'bar' : b2 }
        env6.foo.execute(target='out1')
        env6.bar.execute(target='out2')
        assert built_it['out1']
        assert built_it['out2']



    def test_Scanners(self):
        """Test setting SCANNERS in various ways

        One environment is initialized with a single
        Scanner object, one with a list of a single Scanner
        object, and one with a list of two Scanner objects.
        """
        global scanned_it

        s1 = Scanner(name = 'scanner1', skeys = [".c", ".cc"])
        s2 = Scanner(name = 'scanner2', skeys = [".m4"])
        s3 = Scanner(name = 'scanner3', skeys = [".m4", ".m5"])
        s4 = Scanner(name = 'scanner4', skeys = [None])

#        XXX Tests for scanner execution through different environments,
#        XXX if we ever want to do that some day
#        scanned_it = {}
#        env1 = self.TestEnvironment(SCANNERS = s1)
#        env1.scanner1(filename = 'out1')
#        assert scanned_it['out1']
#
#        scanned_it = {}
#        env2 = self.TestEnvironment(SCANNERS = [s1])
#        env1.scanner1(filename = 'out1')
#        assert scanned_it['out1']
#
#        scanned_it = {}
#        env3 = Environment()
#        env3.Replace(SCANNERS = [s1])
#        env3.scanner1(filename = 'out1')
#        env3.scanner2(filename = 'out2')
#        env3.scanner1(filename = 'out3')
#        assert scanned_it['out1']
#        assert scanned_it['out2']
#        assert scanned_it['out3']

        suffixes = [".c", ".cc", ".cxx", ".m4", ".m5"]

        env = Environment()
        try: del env['SCANNERS']
        except KeyError: pass
        s = list(map(env.get_scanner, suffixes))
        assert s == [None, None, None, None, None], s

        env = self.TestEnvironment(SCANNERS = [])
        s = list(map(env.get_scanner, suffixes))
        assert s == [None, None, None, None, None], s

        env.Replace(SCANNERS = [s1])
        s = list(map(env.get_scanner, suffixes))
        assert s == [s1, s1, None, None, None], s

        env.Append(SCANNERS = [s2])
        s = list(map(env.get_scanner, suffixes))
        assert s == [s1, s1, None, s2, None], s

        env.AppendUnique(SCANNERS = [s3])
        s = list(map(env.get_scanner, suffixes))
        assert s == [s1, s1, None, s2, s3], s

        env = env.Clone(SCANNERS = [s2])
        s = list(map(env.get_scanner, suffixes))
        assert s == [None, None, None, s2, None], s

        env['SCANNERS'] = [s1]
        s = list(map(env.get_scanner, suffixes))
        assert s == [s1, s1, None, None, None], s

        env.PrependUnique(SCANNERS = [s2, s1])
        s = list(map(env.get_scanner, suffixes))
        assert s == [s1, s1, None, s2, None], s

        env.Prepend(SCANNERS = [s3])
        s = list(map(env.get_scanner, suffixes))
        assert s == [s1, s1, None, s3, s3], s

        # Verify behavior of case-insensitive suffix matches on Windows.
        uc_suffixes = [_.upper() for _ in suffixes]

        env = Environment(SCANNERS = [s1, s2, s3],
                          PLATFORM = 'linux')

        s = list(map(env.get_scanner, suffixes))
        assert s == [s1, s1, None, s2, s3], s

        s = list(map(env.get_scanner, uc_suffixes))
        assert s == [None, None, None, None, None], s

        env['PLATFORM'] = 'win32'

        s = list(map(env.get_scanner, uc_suffixes))
        assert s == [s1, s1, None, s2, s3], s

        # Verify behavior for a scanner returning None (on Windows
        # where we might try to perform case manipulation on None).
        env.Replace(SCANNERS = [s4])
        s = list(map(env.get_scanner, suffixes))
        assert s == [None, None, None, None, None], s

    def test_ENV(self):
        """Test setting the external ENV in Environments
        """
        env = Environment()
        assert 'ENV' in env.Dictionary()

        env = self.TestEnvironment(ENV = { 'PATH' : '/foo:/bar' })
        assert env.Dictionary('ENV')['PATH'] == '/foo:/bar'

    def test_ReservedVariables(self):
        """Test warning generation when reserved variable names are set"""

        reserved_variables = [
            'CHANGED_SOURCES',
            'CHANGED_TARGETS',
            'SOURCE',
            'SOURCES',
            'TARGET',
            'TARGETS',
            'UNCHANGED_SOURCES',
            'UNCHANGED_TARGETS',
        ]

        warning = SCons.Warnings.ReservedVariableWarning
        SCons.Warnings.enableWarningClass(warning)
        old = SCons.Warnings.warningAsException(1)

        try:
            env4 = Environment()
            for kw in self.reserved_variables:
                exc_caught = None
                try:
                    env4[kw] = 'xyzzy'
                except warning:
                    exc_caught = 1
                assert exc_caught, "Did not catch ReservedVariableWarning for `%s'" % kw
                assert kw not in env4, "`%s' variable was incorrectly set" % kw
        finally:
            SCons.Warnings.warningAsException(old)

    def test_FutureReservedVariables(self):
        """Test warning generation when future reserved variable names are set"""

        future_reserved_variables = []

        warning = SCons.Warnings.FutureReservedVariableWarning
        SCons.Warnings.enableWarningClass(warning)
        old = SCons.Warnings.warningAsException(1)

        try:
            env4 = Environment()
            for kw in future_reserved_variables:
                exc_caught = None
                try:
                    env4[kw] = 'xyzzy'
                except warning:
                    exc_caught = 1
                assert exc_caught, "Did not catch FutureReservedVariableWarning for `%s'" % kw
                assert kw in env4, "`%s' variable was not set" % kw
        finally:
            SCons.Warnings.warningAsException(old)

    def test_IllegalVariables(self):
        """Test that use of illegal variables raises an exception"""
        env = Environment()
        def test_it(var, env=env):
            exc_caught = None
            try:
                env[var] = 1
            except SCons.Errors.UserError:
                exc_caught = 1
            assert exc_caught, "did not catch UserError for '%s'" % var
        env['aaa'] = 1
        assert env['aaa'] == 1, env['aaa']
        test_it('foo/bar')
        test_it('foo.bar')
        test_it('foo-bar')

    def test_autogenerate(self):
        """Test autogenerating variables in a dictionary."""

        drive, p = os.path.splitdrive(os.getcwd())
        def normalize_path(path, drive=drive):
            if path[0] in '\\/':
                path = drive + path
            path = os.path.normpath(path)
            drive, path = os.path.splitdrive(path)
            return drive.lower() + path

        env = self.TestEnvironment(LIBS = [ 'foo', 'bar', 'baz' ],
                                   LIBLINKPREFIX = 'foo',
                                   LIBLINKSUFFIX = 'bar')

        def RDirs(pathlist, fs=env.fs):
            return fs.Dir('xx').Rfindalldirs(pathlist)

        env['RDirs'] = RDirs
        flags = env.subst_list('$_LIBFLAGS', 1)[0]
        assert flags == ['foobar', 'foobar', 'foobazbar'], flags

        blat = env.fs.Dir('blat')

        env.Replace(CPPPATH = [ 'foo', '$FOO/bar', blat ],
                    INCPREFIX = 'foo ',
                    INCSUFFIX = 'bar',
                    FOO = 'baz')
        flags = env.subst_list('$_CPPINCFLAGS', 1)[0]
        expect = [ '$(',
                   normalize_path('foo'),
                   normalize_path('xx/foobar'),
                   normalize_path('foo'),
                   normalize_path('xx/baz/bar'),
                   normalize_path('foo'),
                   normalize_path('blatbar'),
                   '$)',
        ]
        assert flags == expect, flags

        env.Replace(F77PATH = [ 'foo', '$FOO/bar', blat ],
                    INCPREFIX = 'foo ',
                    INCSUFFIX = 'bar',
                    FOO = 'baz')
        flags = env.subst_list('$_F77INCFLAGS', 1)[0]
        expect = [ '$(',
                   normalize_path('foo'),
                   normalize_path('xx/foobar'),
                   normalize_path('foo'),
                   normalize_path('xx/baz/bar'),
                   normalize_path('foo'),
                   normalize_path('blatbar'),
                   '$)',
        ]
        assert flags == expect, flags

        env.Replace(CPPPATH = '', F77PATH = '', LIBPATH = '')
        l = env.subst_list('$_CPPINCFLAGS')
        assert l == [[]], l
        l = env.subst_list('$_F77INCFLAGS')
        assert l == [[]], l
        l = env.subst_list('$_LIBDIRFLAGS')
        assert l == [[]], l

        env.fs.Repository('/rep1')
        env.fs.Repository('/rep2')
        env.Replace(CPPPATH = [ 'foo', '/__a__/b', '$FOO/bar', blat],
                    INCPREFIX = '-I ',
                    INCSUFFIX = 'XXX',
                    FOO = 'baz')
        flags = env.subst_list('$_CPPINCFLAGS', 1)[0]
        expect = [ '$(',
                   '-I', normalize_path('xx/fooXXX'),
                   '-I', normalize_path('/rep1/xx/fooXXX'),
                   '-I', normalize_path('/rep2/xx/fooXXX'),
                   '-I', normalize_path('/__a__/bXXX'),
                   '-I', normalize_path('xx/baz/barXXX'),
                   '-I', normalize_path('/rep1/xx/baz/barXXX'),
                   '-I', normalize_path('/rep2/xx/baz/barXXX'),
                   '-I', normalize_path('blatXXX'),
                   '$)'
        ]
        def normalize_if_path(arg, np=normalize_path):
            if arg not in ('$(','$)','-I'):
                return np(str(arg))
            return arg
        flags = list(map(normalize_if_path, flags))
        assert flags == expect, flags

    def test_platform(self):
        """Test specifying a platform callable when instantiating."""
        class platform:
            def __str__(self):        return "TestPlatform"
            def __call__(self, env):  env['XYZZY'] = 777

        def tool(env):
            env['SET_TOOL'] = 'initialized'
            assert env['PLATFORM'] == "TestPlatform"

        env = self.TestEnvironment(platform = platform(), tools = [tool])
        assert env['XYZZY'] == 777, env
        assert env['PLATFORM'] == "TestPlatform"
        assert env['SET_TOOL'] == "initialized"

    def test_Default_PLATFORM(self):
        """Test overriding the default PLATFORM variable"""
        class platform:
            def __str__(self):        return "DefaultTestPlatform"
            def __call__(self, env):  env['XYZZY'] = 888

        def tool(env):
            env['SET_TOOL'] = 'abcde'
            assert env['PLATFORM'] == "DefaultTestPlatform"

        import SCons.Defaults
        save = SCons.Defaults.ConstructionEnvironment.copy()
        try:
            import SCons.Defaults
            SCons.Defaults.ConstructionEnvironment.update({
                'PLATFORM' : platform(),
            })
            env = self.TestEnvironment(tools = [tool])
            assert env['XYZZY'] == 888, env
            assert env['PLATFORM'] == "DefaultTestPlatform"
            assert env['SET_TOOL'] == "abcde"
        finally:
            SCons.Defaults.ConstructionEnvironment = save

    def test_tools(self):
        """Test specifying a tool callable when instantiating."""
        def t1(env):
            env['TOOL1'] = 111
        def t2(env):
            env['TOOL2'] = 222
        def t3(env):
            env['AAA'] = env['XYZ']
        def t4(env):
            env['TOOL4'] = 444
        env = self.TestEnvironment(tools = [t1, t2, t3], XYZ = 'aaa')
        assert env['TOOL1'] == 111, env['TOOL1']
        assert env['TOOL2'] == 222, env
        assert env['AAA'] == 'aaa', env
        t4(env)
        assert env['TOOL4'] == 444, env

        test = TestCmd.TestCmd(workdir = '')
        test.write('faketool.py', """\
def generate(env, **kw):
    for k, v in kw.items():
        env[k] = v

def exists(env):
    return True
""")

        env = self.TestEnvironment(tools = [('faketool', {'a':1, 'b':2, 'c':3})],
                          toolpath = [test.workpath('')])
        assert env['a'] == 1, env['a']
        assert env['b'] == 2, env['b']
        assert env['c'] == 3, env['c']

    def test_Default_TOOLS(self):
        """Test overriding the default TOOLS variable"""
        def t5(env):
            env['TOOL5'] = 555
        def t6(env):
            env['TOOL6'] = 666
        def t7(env):
            env['BBB'] = env['XYZ']
        def t8(env):
            env['TOOL8'] = 888

        import SCons.Defaults
        save = SCons.Defaults.ConstructionEnvironment.copy()
        try:
            SCons.Defaults.ConstructionEnvironment.update({
                'TOOLS' : [t5, t6, t7],
            })
            env = Environment(XYZ = 'bbb')
            assert env['TOOL5'] == 555, env['TOOL5']
            assert env['TOOL6'] == 666, env
            assert env['BBB'] == 'bbb', env
            t8(env)
            assert env['TOOL8'] == 888, env
        finally:
            SCons.Defaults.ConstructionEnvironment = save

    def test_null_tools(self):
        """Test specifying a tool of None is OK."""
        def t1(env):
            env['TOOL1'] = 111
        def t2(env):
            env['TOOL2'] = 222
        env = self.TestEnvironment(tools = [t1, None, t2], XYZ = 'aaa')
        assert env['TOOL1'] == 111, env['TOOL1']
        assert env['TOOL2'] == 222, env
        assert env['XYZ'] == 'aaa', env
        env = self.TestEnvironment(tools = [None], XYZ = 'xyz')
        assert env['XYZ'] == 'xyz', env
        env = self.TestEnvironment(tools = [t1, '', t2], XYZ = 'ddd')
        assert env['TOOL1'] == 111, env['TOOL1']
        assert env['TOOL2'] == 222, env
        assert env['XYZ'] == 'ddd', env

    def test_default_copy_cache(self):
        copied = False

        def copy2(self, src, dst):
            nonlocal copied
            copied = True

        save_copy_from_cache = SCons.CacheDir.CacheDir.copy_from_cache
        SCons.CacheDir.CacheDir.copy_from_cache = copy2

        save_copy_to_cache = SCons.CacheDir.CacheDir.copy_to_cache
        SCons.CacheDir.CacheDir.copy_to_cache = copy2

        env = self.TestEnvironment()

        SCons.Environment.default_copy_from_cache(env, 'test.in', 'test.out')
        assert copied

        copied = False
        SCons.Environment.default_copy_to_cache(env, 'test.in', 'test.out')
        assert copied

        SCons.CacheDir.CacheDir.copy_from_cache = save_copy_from_cache
        SCons.CacheDir.CacheDir.copy_to_cache = save_copy_to_cache

    def test_concat(self):
        """Test _concat()"""
        e1 = self.TestEnvironment(PRE='pre', SUF='suf', STR='a b', LIST=['a', 'b'])
        s = e1.subst
        x = s("${_concat('', '', '', __env__)}")
        assert x == '', x
        x = s("${_concat('', [], '', __env__)}")
        assert x == '', x
        x = s("${_concat(PRE, '', SUF, __env__)}")
        assert x == '', x
        x = s("${_concat(PRE, STR, SUF, __env__)}")
        assert x == 'prea bsuf', x
        x = s("${_concat(PRE, LIST, SUF, __env__)}")
        assert x == 'preasuf prebsuf', x
        x = s("${_concat(PRE, LIST, SUF, __env__,affect_signature=False)}", raw=True)
        assert x == '$( preasuf prebsuf $)', x


    def test_concat_nested(self):
        """Test _concat() on a nested substitution strings."""
        e = self.TestEnvironment(PRE='pre', SUF='suf',
                                 L1=['a', 'b'],
                                 L2=['c', 'd'],
                                 L3=['$L2'])
        x = e.subst('$( ${_concat(PRE, L1, SUF, __env__)} $)')
        assert x == 'preasuf prebsuf', x
        e.AppendUnique(L1 = ['$L2'])
        x = e.subst('$( ${_concat(PRE, L1, SUF, __env__)} $)')
        assert x == 'preasuf prebsuf precsuf predsuf', x
        e.AppendUnique(L1 = ['$L3'])
        x = e.subst('$( ${_concat(PRE, L1, SUF, __env__)} $)')
        assert x == 'preasuf prebsuf precsuf predsuf precsuf predsuf', x

    def test_gvars(self):
        """Test the Environment gvars() method"""
        env = self.TestEnvironment(XXX = 'x', YYY = 'y', ZZZ = 'z')
        gvars = env.gvars()
        assert gvars['XXX'] == 'x', gvars['XXX']
        assert gvars['YYY'] == 'y', gvars['YYY']
        assert gvars['ZZZ'] == 'z', gvars['ZZZ']

    def test__update(self):
        """Test the _update() method"""
        env = self.TestEnvironment(X = 'x', Y = 'y', Z = 'z')
        assert env['X'] == 'x', env['X']
        assert env['Y'] == 'y', env['Y']
        assert env['Z'] == 'z', env['Z']
        env._update({'X'       : 'xxx',
                     'TARGET'  : 't',
                     'TARGETS' : 'ttt',
                     'SOURCE'  : 's',
                     'SOURCES' : 'sss',
                     'Z'       : 'zzz'})
        assert env['X'] == 'xxx', env['X']
        assert env['Y'] == 'y', env['Y']
        assert env['Z'] == 'zzz', env['Z']
        assert env['TARGET'] == 't', env['TARGET']
        assert env['TARGETS'] == 'ttt', env['TARGETS']
        assert env['SOURCE'] == 's', env['SOURCE']
        assert env['SOURCES'] == 'sss', env['SOURCES']

    def test_Append(self):
        """Test appending to construction variables in an Environment
        """

        b1 = Environment()['BUILDERS']
        b2 = Environment()['BUILDERS']
        assert b1 == b2, diff_dict(b1, b2)

        cases = [
            'a1',       'A1',           'a1A1',
            'a2',       ['A2'],         ['a2', 'A2'],
            'a3',       UL(['A3']),     UL(['a', '3', 'A3']),
            'a4',       '',             'a4',
            'a5',       [],             ['a5'],
            'a6',       UL([]),         UL(['a', '6']),
            'a7',       [''],           ['a7', ''],
            'a8',       UL(['']),       UL(['a', '8', '']),

            ['e1'],     'E1',           ['e1', 'E1'],
            ['e2'],     ['E2'],         ['e2', 'E2'],
            ['e3'],     UL(['E3']),     UL(['e3', 'E3']),
            ['e4'],     '',             ['e4'],
            ['e5'],     [],             ['e5'],
            ['e6'],     UL([]),         UL(['e6']),
            ['e7'],     [''],           ['e7', ''],
            ['e8'],     UL(['']),       UL(['e8', '']),

            UL(['i1']), 'I1',           UL(['i1', 'I', '1']),
            UL(['i2']), ['I2'],         UL(['i2', 'I2']),
            UL(['i3']), UL(['I3']),     UL(['i3', 'I3']),
            UL(['i4']), '',             UL(['i4']),
            UL(['i5']), [],             UL(['i5']),
            UL(['i6']), UL([]),         UL(['i6']),
            UL(['i7']), [''],           UL(['i7', '']),
            UL(['i8']), UL(['']),       UL(['i8', '']),

            {'d1':1},   'D1',           {'d1':1, 'D1':None},
            {'d2':1},   ['D2'],         {'d2':1, 'D2':None},
            {'d3':1},   UL(['D3']),     {'d3':1, 'D3':None},
            {'d4':1},   {'D4':1},       {'d4':1, 'D4':1},
            {'d5':1},   UD({'D5':1}),   UD({'d5':1, 'D5':1}),

            UD({'u1':1}), 'U1',         UD({'u1':1, 'U1':None}),
            UD({'u2':1}), ['U2'],       UD({'u2':1, 'U2':None}),
            UD({'u3':1}), UL(['U3']),   UD({'u3':1, 'U3':None}),
            UD({'u4':1}), {'U4':1},     UD({'u4':1, 'U4':1}),
            UD({'u5':1}), UD({'U5':1}), UD({'u5':1, 'U5':1}),

            '',         'M1',           'M1',
            '',         ['M2'],         ['M2'],
            '',         UL(['M3']),     UL(['M3']),
            '',         '',             '',
            '',         [],             [],
            '',         UL([]),         UL([]),
            '',         [''],           [''],
            '',         UL(['']),       UL(['']),

            [],         'N1',           ['N1'],
            [],         ['N2'],         ['N2'],
            [],         UL(['N3']),     UL(['N3']),
            [],         '',             [],
            [],         [],             [],
            [],         UL([]),         UL([]),
            [],         [''],           [''],
            [],         UL(['']),       UL(['']),

            UL([]),     'O1',           ['O', '1'],
            UL([]),     ['O2'],         ['O2'],
            UL([]),     UL(['O3']),     UL(['O3']),
            UL([]),     '',             UL([]),
            UL([]),     [],             UL([]),
            UL([]),     UL([]),         UL([]),
            UL([]),     [''],           UL(['']),
            UL([]),     UL(['']),       UL(['']),

            [''],       'P1',           ['', 'P1'],
            [''],       ['P2'],         ['', 'P2'],
            [''],       UL(['P3']),     UL(['', 'P3']),
            [''],       '',             [''],
            [''],       [],             [''],
            [''],       UL([]),         UL(['']),
            [''],       [''],           ['', ''],
            [''],       UL(['']),       UL(['', '']),

            UL(['']),   'Q1',           ['', 'Q', '1'],
            UL(['']),   ['Q2'],         ['', 'Q2'],
            UL(['']),   UL(['Q3']),     UL(['', 'Q3']),
            UL(['']),   '',             UL(['']),
            UL(['']),   [],             UL(['']),
            UL(['']),   UL([]),         UL(['']),
            UL(['']),   [''],           UL(['', '']),
            UL(['']),   UL(['']),       UL(['', '']),
        ]

        env = Environment()
        failed = 0
        while cases:
            input, append, expect = cases[:3]
            env['XXX'] = copy.copy(input)
            try:
                env.Append(XXX = append)
            except Exception as e:
                if failed == 0: print()
                print("    %s Append %s exception: %s" % \
                      (repr(input), repr(append), e))
                failed = failed + 1
            else:
                result = env['XXX']
                if result != expect:
                    if failed == 0: print()
                    print("    %s Append %s => %s did not match %s" % \
                          (repr(input), repr(append), repr(result), repr(expect)))
                    failed = failed + 1
            del cases[:3]
        assert failed == 0, "%d Append() cases failed" % failed

        env['UL'] = UL(['foo'])
        env.Append(UL = 'bar')
        result = env['UL']
        assert isinstance(result, UL), repr(result)
        assert result == ['foo', 'b', 'a', 'r'], result

        env['CLVar'] = CLVar(['foo'])
        env.Append(CLVar = 'bar')
        result = env['CLVar']
        assert isinstance(result, CLVar), repr(result)
        assert result == ['foo', 'bar'], result

        class C:
            def __init__(self, name):
                self.name = name
            def __str__(self):
                return self.name
            def __eq__(self, other):
                raise Exception("should not compare")

        ccc = C('ccc')

        env2 = self.TestEnvironment(CCC1 = ['c1'], CCC2 = ccc)
        env2.Append(CCC1 = ccc, CCC2 = ['c2'])
        assert env2['CCC1'][0] == 'c1', env2['CCC1']
        assert env2['CCC1'][1] is ccc, env2['CCC1']
        assert env2['CCC2'][0] is ccc, env2['CCC2']
        assert env2['CCC2'][1] == 'c2', env2['CCC2']

        env3 = self.TestEnvironment(X = {'x1' : 7})
        env3.Append(X = {'x1' : 8, 'x2' : 9}, Y = {'y1' : 10})
        assert env3['X'] == {'x1': 8, 'x2': 9}, env3['X']
        assert env3['Y'] == {'y1': 10}, env3['Y']

        z1 = Builder()
        z2 = Builder()
        env4 = self.TestEnvironment(BUILDERS = {'z1' : z1})
        env4.Append(BUILDERS = {'z2' : z2})
        assert env4['BUILDERS'] == {'z1' : z1, 'z2' : z2}, env4['BUILDERS']
        assert hasattr(env4, 'z1')
        assert hasattr(env4, 'z2')

    def test_AppendENVPath(self):
        """Test appending to an ENV path."""
        env1 = self.TestEnvironment(
            ENV={'PATH': r'C:\dir\num\one;C:\dir\num\two'},
            MYENV={'MYPATH': r'C:\mydir\num\one;C:\mydir\num\two'},
        )
        # have to include the pathsep here so that the test will work on UNIX too.
        env1.AppendENVPath('PATH', r'C:\dir\num\two', sep=';')
        env1.AppendENVPath('PATH', r'C:\dir\num\three', sep=';')
        env1.AppendENVPath('MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';')
        assert (
            env1['ENV']['PATH'] == r'C:\dir\num\one;C:\dir\num\two;C:\dir\num\three'
        ), env1['ENV']['PATH']

        env1.AppendENVPath('MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';')
        env1.AppendENVPath(
            'MYPATH', r'C:\mydir\num\one', 'MYENV', sep=';', delete_existing=1
        )
        # this should do nothing since delete_existing is 0
        assert (
            env1['MYENV']['MYPATH'] == r'C:\mydir\num\two;C:\mydir\num\three;C:\mydir\num\one'
        ), env1['MYENV']['MYPATH']

        test = TestCmd.TestCmd(workdir='')
        test.subdir('sub1', 'sub2')
        p = env1['ENV']['PATH']
        env1.AppendENVPath('PATH', '#sub1', sep=';')
        env1.AppendENVPath('PATH', env1.fs.Dir('sub2'), sep=';')
        assert env1['ENV']['PATH'] == p + ';sub1;sub2', env1['ENV']['PATH']

    def test_AppendUnique(self):
        """Test appending to unique values to construction variables

        This strips values that are already present when lists are
        involved."""
        env = self.TestEnvironment(AAA1 = 'a1',
                          AAA2 = 'a2',
                          AAA3 = 'a3',
                          AAA4 = 'a4',
                          AAA5 = 'a5',
                          BBB1 = ['b1'],
                          BBB2 = ['b2'],
                          BBB3 = ['b3'],
                          BBB4 = ['b4'],
                          BBB5 = ['b5'],
                          CCC1 = '',
                          CCC2 = '',
                          DDD1 = ['a', 'b', 'c'])
        env['LL1'] = [env.Literal('a literal'), env.Literal('b literal')]
        env['LL2'] = [env.Literal('c literal'), env.Literal('b literal')]
        env.AppendUnique(AAA1 = 'a1',
                         AAA2 = ['a2'],
                         AAA3 = ['a3', 'b', 'c', 'c', 'b', 'a3'], # ignore dups
                         AAA4 = 'a4.new',
                         AAA5 = ['a5.new'],
                         BBB1 = 'b1',
                         BBB2 = ['b2'],
                         BBB3 = ['b3', 'c', 'd', 'c', 'b3'],
                         BBB4 = 'b4.new',
                         BBB5 = ['b5.new'],
                         CCC1 = 'c1',
                         CCC2 = ['c2'],
                         DDD1 = 'b',
                         LL1  = env.Literal('a literal'),
                         LL2  = env.Literal('a literal'))

        assert env['AAA1'] == 'a1a1', env['AAA1']
        assert env['AAA2'] == ['a2'], env['AAA2']
        assert env['AAA3'] == ['a3', 'b', 'c'], env['AAA3']
        assert env['AAA4'] == 'a4a4.new', env['AAA4']
        assert env['AAA5'] == ['a5', 'a5.new'], env['AAA5']
        assert env['BBB1'] == ['b1'], env['BBB1']
        assert env['BBB2'] == ['b2'], env['BBB2']
        assert env['BBB3'] == ['b3', 'c', 'd'], env['BBB3']
        assert env['BBB4'] == ['b4', 'b4.new'], env['BBB4']
        assert env['BBB5'] == ['b5', 'b5.new'], env['BBB5']
        assert env['CCC1'] == 'c1', env['CCC1']
        assert env['CCC2'] == ['c2'], env['CCC2']
        assert env['DDD1'] == ['a', 'b', 'c'], env['DDD1']
        assert env['LL1']  == [env.Literal('a literal'), env.Literal('b literal')], env['LL1']
        assert env['LL2']  == [env.Literal('c literal'), env.Literal('b literal'), env.Literal('a literal')], [str(x) for x in env['LL2']]

        env.AppendUnique(DDD1 = 'b', delete_existing=1)
        assert env['DDD1'] == ['a', 'c', 'b'], env['DDD1'] # b moves to end
        env.AppendUnique(DDD1 = ['a','b'], delete_existing=1)
        assert env['DDD1'] == ['c', 'a', 'b'], env['DDD1'] # a & b move to end
        env.AppendUnique(DDD1 = ['e','f', 'e'], delete_existing=1)
        assert env['DDD1'] == ['c', 'a', 'b', 'f', 'e'], env['DDD1'] # add last

        env['CLVar'] = CLVar([])
        env.AppendUnique(CLVar = 'bar')
        result = env['CLVar']
        assert isinstance(result, CLVar), repr(result)
        assert result == ['bar'], result

        env['CLVar'] = CLVar(['abc'])
        env.AppendUnique(CLVar = 'bar')
        result = env['CLVar']
        assert isinstance(result, CLVar), repr(result)
        assert result == ['abc', 'bar'], result

        env['CLVar'] = CLVar(['bar'])
        env.AppendUnique(CLVar = 'bar')
        result = env['CLVar']
        assert isinstance(result, CLVar), repr(result)
        assert result == ['bar'], result

    def test_Clone(self):
        """Test construction environment cloning.

        The clone should compare equal if there are no overrides.
        Update the clone independently afterwards and check that
        the original remains intact (that is, no dangling
        references point to objects in the copied environment).
        Clone the original with some construction variable
        updates and check that the original remains intact
        and the copy has the updated values.
        """
        env1 = self.TestEnvironment(XXX='x', YYY='y')
        env2 = env1.Clone()
        env1copy = env1.Clone()
        assert env1copy == env1
        assert env2 == env1
        env2.Replace(YYY = 'yyy')
        assert env1 != env2
        assert env1 == env1copy

        env3 = env1.Clone(XXX='x3', ZZZ='z3')
        assert env3 != env1
        assert env3.Dictionary('XXX') == 'x3'
        assert env1.Dictionary('XXX') == 'x'
        assert env3.Dictionary('YYY') == 'y'
        assert env3.Dictionary('ZZZ') == 'z3'
        assert env1 == env1copy

        # Ensure that lists and dictionaries are deep copied, but not instances
        class TestA:
            pass

        env1 = self.TestEnvironment(XXX=TestA(), YYY=[1, 2, 3], ZZZ={1: 2, 3: 4})
        env2 = env1.Clone()
        env2.Dictionary('YYY').append(4)
        env2.Dictionary('ZZZ')[5] = 6
        assert env1.Dictionary('XXX') is env2.Dictionary('XXX')
        assert 4 in env2.Dictionary('YYY')
        assert 4 not in env1.Dictionary('YYY')
        assert 5 in env2.Dictionary('ZZZ')
        assert 5 not in env1.Dictionary('ZZZ')

        #
        env1 = self.TestEnvironment(BUILDERS={'b1': Builder()})
        assert hasattr(env1, 'b1'), "env1.b1 was not set"
        assert env1.b1.object == env1, "b1.object doesn't point to env1"
        env2 = env1.Clone(BUILDERS = {'b2' : Builder()})
        assert env2 != env1
        assert hasattr(env1, 'b1'), "b1 was mistakenly cleared from env1"
        assert env1.b1.object == env1, "b1.object was changed"
        assert not hasattr(env2, 'b1'), "b1 was not cleared from env2"
        assert hasattr(env2, 'b2'), "env2.b2 was not set"
        assert env2.b2.object == env2, "b2.object doesn't point to env2"

        # Ensure that specifying new tools in a copied environment works.
        def foo(env): env['FOO'] = 1
        def bar(env): env['BAR'] = 2
        def baz(env): env['BAZ'] = 3
        env1 = self.TestEnvironment(tools=[foo])
        env2 = env1.Clone()
        env3 = env1.Clone(tools=[bar, baz])

        assert env1.get('FOO') == 1
        assert env1.get('BAR') is None
        assert env1.get('BAZ') is None
        assert env2.get('FOO') == 1
        assert env2.get('BAR') is None
        assert env2.get('BAZ') is None
        assert env3.get('FOO') == 1
        assert env3.get('BAR') == 2
        assert env3.get('BAZ') == 3

        # Ensure that recursive variable substitution when copying
        # environments works properly.
        env1 = self.TestEnvironment(CCFLAGS = '-DFOO', XYZ = '-DXYZ')
        env2 = env1.Clone(CCFLAGS = '$CCFLAGS -DBAR',
                         XYZ = ['-DABC', 'x $XYZ y', '-DDEF'])
        x = env2.get('CCFLAGS')
        assert x == '-DFOO -DBAR', x
        x = env2.get('XYZ')
        assert x == ['-DABC', 'x -DXYZ y', '-DDEF'], x

        # Ensure that special properties of a class don't get
        # lost on copying.
        env1 = self.TestEnvironment(FLAGS = CLVar('flag1 flag2'))
        x = env1.get('FLAGS')
        assert x == ['flag1', 'flag2'], x
        env2 = env1.Clone()
        env2.Append(FLAGS = 'flag3 flag4')
        x = env2.get('FLAGS')
        assert x == ['flag1', 'flag2', 'flag3', 'flag4'], x
        x = env1.get('FLAGS')
        assert x == ['flag1', 'flag2'], x

        # Ensure that appending directly to a copied CLVar
        # doesn't modify the original.
        env1 = self.TestEnvironment(FLAGS = CLVar('flag1 flag2'))
        x = env1.get('FLAGS')
        assert x == ['flag1', 'flag2'], x
        env2 = env1.Clone()
        env2['FLAGS'] += ['flag3', 'flag4']
        x = env2.get('FLAGS')
        assert x == ['flag1', 'flag2', 'flag3', 'flag4'], x
        x = env1.get('FLAGS')
        assert x == ['flag1', 'flag2'], x

        # Test that the environment stores the toolpath and
        # re-uses it for copies.
        test = TestCmd.TestCmd(workdir = '')

        test.write('xxx.py', """\
def exists(env):
    return True
def generate(env):
    env['XXX'] = 'one'
""")

        test.write('yyy.py', """\
def exists(env):
    return True
def generate(env):
    env['YYY'] = 'two'
""")

        env = self.TestEnvironment(tools=['xxx'], toolpath=[test.workpath('')])
        assert env['XXX'] == 'one', env['XXX']
        env = env.Clone(tools=['yyy'])
        assert env['YYY'] == 'two', env['YYY']


        # Test that
        real_value = [4]

        def my_tool(env, rv=real_value):
            assert env['KEY_THAT_I_WANT'] == rv[0]
            env['KEY_THAT_I_WANT'] = rv[0] + 1

        env = self.TestEnvironment()

        real_value[0] = 5
        env = env.Clone(KEY_THAT_I_WANT=5, tools=[my_tool])
        assert env['KEY_THAT_I_WANT'] == real_value[0], env['KEY_THAT_I_WANT']

        real_value[0] = 6
        env = env.Clone(KEY_THAT_I_WANT=6, tools=[my_tool])
        assert env['KEY_THAT_I_WANT'] == real_value[0], env['KEY_THAT_I_WANT']

        # test for pull request #150
        env = self.TestEnvironment()
        env._dict.pop('BUILDERS')
        assert ('BUILDERS' in env) is False
        env2 = env.Clone()

    def test_Detect(self):
        """Test Detect()ing tools"""
        test = TestCmd.TestCmd(workdir = '')
        test.subdir('sub1', 'sub2')
        sub1 = test.workpath('sub1')
        sub2 = test.workpath('sub2')

        if sys.platform == 'win32':
            test.write(['sub1', 'xxx'], "sub1/xxx\n")
            test.write(['sub2', 'xxx'], "sub2/xxx\n")

            env = self.TestEnvironment(ENV = { 'PATH' : [sub1, sub2] })

            x = env.Detect('xxx.exe')
            assert x is None, x

            test.write(['sub2', 'xxx.exe'], "sub2/xxx.exe\n")

            env = self.TestEnvironment(ENV = { 'PATH' : [sub1, sub2] })

            x = env.Detect('xxx.exe')
            assert x == 'xxx.exe', x

            test.write(['sub1', 'xxx.exe'], "sub1/xxx.exe\n")

            x = env.Detect('xxx.exe')
            assert x == 'xxx.exe', x

        else:
            test.write(['sub1', 'xxx.exe'], "sub1/xxx.exe\n")
            test.write(['sub2', 'xxx.exe'], "sub2/xxx.exe\n")

            env = self.TestEnvironment(ENV = { 'PATH' : [sub1, sub2] })

            x = env.Detect('xxx.exe')
            assert x is None, x

            sub2_xxx_exe = test.workpath('sub2', 'xxx.exe')
            os.chmod(sub2_xxx_exe, 0o755)

            env = self.TestEnvironment(ENV = { 'PATH' : [sub1, sub2] })

            x = env.Detect('xxx.exe')
            assert x == 'xxx.exe', x

            sub1_xxx_exe = test.workpath('sub1', 'xxx.exe')
            os.chmod(sub1_xxx_exe, 0o755)

            x = env.Detect('xxx.exe')
            assert x == 'xxx.exe', x

        env = self.TestEnvironment(ENV = { 'PATH' : [] })
        x = env.Detect('xxx.exe')
        assert x is None, x

    def test_Dictionary(self):
        """Test retrieval of known construction variables

        Fetch them from the Dictionary and check for well-known
        defaults that get inserted.
        """
        env = self.TestEnvironment(XXX = 'x', YYY = 'y', ZZZ = 'z')
        assert env.Dictionary('XXX') == 'x'
        assert env.Dictionary('YYY') == 'y'
        assert env.Dictionary('XXX', 'ZZZ') == ['x', 'z']
        xxx, zzz = env.Dictionary('XXX', 'ZZZ')
        assert xxx == 'x'
        assert zzz == 'z'
        assert 'BUILDERS' in env.Dictionary()
        assert 'CC' in env.Dictionary()
        assert 'CCFLAGS' in env.Dictionary()
        assert 'ENV' in env.Dictionary()

        assert env['XXX'] == 'x'
        env['XXX'] = 'foo'
        assert env.Dictionary('XXX') == 'foo'
        del env['XXX']
        assert 'XXX' not in env.Dictionary()

    def test_FindIxes(self):
        """Test FindIxes()"""
        env = self.TestEnvironment(LIBPREFIX='lib',
                          LIBSUFFIX='.a',
                          SHLIBPREFIX='lib',
                          SHLIBSUFFIX='.so',
                          PREFIX='pre',
                          SUFFIX='post')

        paths = [os.path.join('dir', 'libfoo.a'),
                 os.path.join('dir', 'libfoo.so')]

        assert paths[0] == env.FindIxes(paths, 'LIBPREFIX', 'LIBSUFFIX')
        assert paths[1] == env.FindIxes(paths, 'SHLIBPREFIX', 'SHLIBSUFFIX')
        assert None is env.FindIxes(paths, 'PREFIX', 'POST')

        paths = ['libfoo.a', 'prefoopost']

        assert paths[0] == env.FindIxes(paths, 'LIBPREFIX', 'LIBSUFFIX')
        assert None is env.FindIxes(paths, 'SHLIBPREFIX', 'SHLIBSUFFIX')
        assert paths[1] == env.FindIxes(paths, 'PREFIX', 'SUFFIX')

    def test_ParseConfig(self):
        """Test the ParseConfig() method"""
        env = self.TestEnvironment(COMMAND='command',
                          ASFLAGS='assembler',
                          CCFLAGS=[''],
                          CPPDEFINES=[],
                          CPPFLAGS=[''],
                          CPPPATH='string',
                          FRAMEWORKPATH=[],
                          FRAMEWORKS=[],
                          LIBPATH=['list'],
                          LIBS='',
                          LINKFLAGS=[''],
                          RPATH=[])

        orig_backtick = env.backtick
        class my_backtick:
            """mocked backtick routine so command is not actually issued.

            Just returns the string it was given.
            """
            def __init__(self, save_command, output):
                self.save_command = save_command
                self.output = output
            def __call__(self, command):
                self.save_command.append(command)
                return self.output

        try:
            save_command = []
            env.backtick = my_backtick(save_command,
                                 "-I/usr/include/fum -I bar -X\n" + \
                                 "-L/usr/fax -L foo -lxxx -l yyy " + \
                                 "-Wa,-as -Wl,-link " + \
                                 "-Wl,-rpath=rpath1 " + \
                                 "-Wl,-R,rpath2 " + \
                                 "-Wl,-Rrpath3 " + \
                                 "-Wp,-cpp abc " + \
                                 "-framework Carbon " + \
                                 "-frameworkdir=fwd1 " + \
                                 "-Ffwd2 " + \
                                 "-F fwd3 " + \
                                 "-pthread " + \
                                 "-fmerge-all-constants " + \
                                 "-mno-cygwin -mwindows " + \
                                 "-arch i386 -isysroot /tmp " + \
                                 "-iquote /usr/include/foo1 " + \
                                 "-isystem /usr/include/foo2 " + \
                                 "-idirafter /usr/include/foo3 " + \
                                 "+DD64 " + \
                                 "-DFOO -DBAR=value")
            env.ParseConfig("fake $COMMAND")
            assert save_command == ['fake command'], save_command
            assert env['ASFLAGS'] == ['assembler', '-as'], env['ASFLAGS']
            assert env['CCFLAGS'] == ['', '-X', '-Wa,-as',
                                      '-pthread', '-fmerge-all-constants', '-mno-cygwin',
                                      ('-arch', 'i386'), ('-isysroot', '/tmp'),
                                      ('-iquote', '/usr/include/foo1'),
                                      ('-isystem', '/usr/include/foo2'),
                                      ('-idirafter', '/usr/include/foo3'),
                                      '+DD64'], env['CCFLAGS']
            assert env['CPPDEFINES'] == ['FOO', ['BAR', 'value']], env['CPPDEFINES']
            assert env['CPPFLAGS'] == ['', '-Wp,-cpp'], env['CPPFLAGS']
            assert env['CPPPATH'] == ['string', '/usr/include/fum', 'bar'], env['CPPPATH']
            assert env['FRAMEWORKPATH'] == ['fwd1', 'fwd2', 'fwd3'], env['FRAMEWORKPATH']
            assert env['FRAMEWORKS'] == ['Carbon'], env['FRAMEWORKS']
            assert env['LIBPATH'] == ['list', '/usr/fax', 'foo'], env['LIBPATH']
            assert env['LIBS'] == ['xxx', 'yyy', env.File('abc')], env['LIBS']
            assert env['LINKFLAGS'] == ['', '-Wl,-link', '-pthread',
                                        '-fmerge-all-constants',
                                        '-mno-cygwin', '-mwindows',
                                        ('-arch', 'i386'),
                                        ('-isysroot', '/tmp'),
                                        '+DD64'], env['LINKFLAGS']
            assert env['RPATH'] == ['rpath1', 'rpath2', 'rpath3'], env['RPATH']

            env.backtick = my_backtick([], "-Ibar")
            env.ParseConfig("fake2")
            assert env['CPPPATH'] == ['string', '/usr/include/fum', 'bar'], env['CPPPATH']
            env.ParseConfig("fake2", unique=0)
            assert env['CPPPATH'] == ['string', '/usr/include/fum', 'bar', 'bar'], env['CPPPATH']
        finally:
            env.backtick = orig_backtick

        # check that we can pass our own function,
        # and that it works for both values of unique

        def my_function(myenv, flags, unique=True):
            import json

            args = json.loads(flags)
            if unique:
                myenv.AppendUnique(**args)
            else:
                myenv.Append(**args)

        json_str = '{"LIBS": ["yyy", "xxx", "yyy"]}'

        env = Environment(LIBS=['xxx'])
        env2 = env.Clone()
        env.backtick = my_backtick([], json_str)
        env2.backtick = my_backtick([], json_str)

        env.ParseConfig("foo", my_function)
        assert env['LIBS'] == ['xxx', 'yyy'], env['LIBS']

        env2.ParseConfig("foo2", my_function, unique=False)
        assert env2['LIBS'] == ['xxx', 'yyy', 'xxx', 'yyy'], env2['LIBS']


    def test_ParseDepends(self):
        """Test the ParseDepends() method"""
        test = TestCmd.TestCmd(workdir = '')

        test.write('single', """
#file: dependency

f0: \
   d1 \
   d2 \
   d3 \

""")

        test.write('multiple', """
f1: foo
f2 f3: bar
f4: abc def
#file: dependency
f5: \
   ghi \
   jkl \
   mno \
""")

        env = self.TestEnvironment(SINGLE = test.workpath('single'))

        tlist = []
        dlist = []
        def my_depends(target, dependency, tlist=tlist, dlist=dlist):
            tlist.extend(target)
            dlist.extend(dependency)

        env.Depends = my_depends

        env.ParseDepends(test.workpath('does_not_exist'))

        exc_caught = None
        try:
            env.ParseDepends(test.workpath('does_not_exist'), must_exist=1)
        except IOError:
            exc_caught = 1
        assert exc_caught, "did not catch expected IOError"

        del tlist[:]
        del dlist[:]

        env.ParseDepends('$SINGLE', only_one=1)
        t = list(map(str, tlist))
        d = list(map(str, dlist))
        assert t == ['f0'], t
        assert d == ['d1', 'd2', 'd3'], d

        del tlist[:]
        del dlist[:]

        env.ParseDepends(test.workpath('multiple'))
        t = list(map(str, tlist))
        d = list(map(str, dlist))
        assert t == ['f1', 'f2', 'f3', 'f4', 'f5'], t
        assert d == ['foo', 'bar', 'abc', 'def', 'ghi', 'jkl', 'mno'], d

        exc_caught = None
        try:
            env.ParseDepends(test.workpath('multiple'), only_one=1)
        except SCons.Errors.UserError:
            exc_caught = 1
        assert exc_caught, "did not catch expected UserError"

    def test_Platform(self):
        """Test the Platform() method"""
        env = self.TestEnvironment(WIN32='win32', NONE='no-such-platform')

        exc_caught = None
        try:
            env.Platform('does_not_exist')
        except SCons.Errors.UserError:
            exc_caught = 1
        assert exc_caught, "did not catch expected UserError"

        exc_caught = None
        try:
            env.Platform('$NONE')
        except SCons.Errors.UserError:
            exc_caught = 1
        assert exc_caught, "did not catch expected UserError"

        env.Platform('posix')
        assert env['OBJSUFFIX'] == '.o', env['OBJSUFFIX']

        env.Platform('$WIN32')
        assert env['OBJSUFFIX'] == '.obj', env['OBJSUFFIX']

    def test_Prepend(self):
        """Test prepending to construction variables in an Environment
        """
        cases = [
            'a1',       'A1',           'A1a1',
            'a2',       ['A2'],         ['A2', 'a2'],
            'a3',       UL(['A3']),     UL(['A3', 'a', '3']),
            'a4',       '',             'a4',
            'a5',       [],             ['a5'],
            'a6',       UL([]),         UL(['a', '6']),
            'a7',       [''],           ['', 'a7'],
            'a8',       UL(['']),       UL(['', 'a', '8']),

            ['e1'],     'E1',           ['E1', 'e1'],
            ['e2'],     ['E2'],         ['E2', 'e2'],
            ['e3'],     UL(['E3']),     UL(['E3', 'e3']),
            ['e4'],     '',             ['e4'],
            ['e5'],     [],             ['e5'],
            ['e6'],     UL([]),         UL(['e6']),
            ['e7'],     [''],           ['', 'e7'],
            ['e8'],     UL(['']),       UL(['', 'e8']),

            UL(['i1']), 'I1',           UL(['I', '1', 'i1']),
            UL(['i2']), ['I2'],         UL(['I2', 'i2']),
            UL(['i3']), UL(['I3']),     UL(['I3', 'i3']),
            UL(['i4']), '',             UL(['i4']),
            UL(['i5']), [],             UL(['i5']),
            UL(['i6']), UL([]),         UL(['i6']),
            UL(['i7']), [''],           UL(['', 'i7']),
            UL(['i8']), UL(['']),       UL(['', 'i8']),

            {'d1':1},   'D1',           {'d1':1, 'D1':None},
            {'d2':1},   ['D2'],         {'d2':1, 'D2':None},
            {'d3':1},   UL(['D3']),     {'d3':1, 'D3':None},
            {'d4':1},   {'D4':1},       {'d4':1, 'D4':1},
            {'d5':1},   UD({'D5':1}),   UD({'d5':1, 'D5':1}),

            UD({'u1':1}), 'U1',         UD({'u1':1, 'U1':None}),
            UD({'u2':1}), ['U2'],       UD({'u2':1, 'U2':None}),
            UD({'u3':1}), UL(['U3']),   UD({'u3':1, 'U3':None}),
            UD({'u4':1}), {'U4':1},     UD({'u4':1, 'U4':1}),
            UD({'u5':1}), UD({'U5':1}), UD({'u5':1, 'U5':1}),

            '',         'M1',           'M1',
            '',         ['M2'],         ['M2'],
            '',         UL(['M3']),     UL(['M3']),
            '',         '',             '',
            '',         [],             [],
            '',         UL([]),         UL([]),
            '',         [''],           [''],
            '',         UL(['']),       UL(['']),

            [],         'N1',           ['N1'],
            [],         ['N2'],         ['N2'],
            [],         UL(['N3']),     UL(['N3']),
            [],         '',             [],
            [],         [],             [],
            [],         UL([]),         UL([]),
            [],         [''],           [''],
            [],         UL(['']),       UL(['']),

            UL([]),     'O1',           UL(['O', '1']),
            UL([]),     ['O2'],         UL(['O2']),
            UL([]),     UL(['O3']),     UL(['O3']),
            UL([]),     '',             UL([]),
            UL([]),     [],             UL([]),
            UL([]),     UL([]),         UL([]),
            UL([]),     [''],           UL(['']),
            UL([]),     UL(['']),       UL(['']),

            [''],       'P1',           ['P1', ''],
            [''],       ['P2'],         ['P2', ''],
            [''],       UL(['P3']),     UL(['P3', '']),
            [''],       '',             [''],
            [''],       [],             [''],
            [''],       UL([]),         UL(['']),
            [''],       [''],           ['', ''],
            [''],       UL(['']),       UL(['', '']),

            UL(['']),   'Q1',           UL(['Q', '1', '']),
            UL(['']),   ['Q2'],         UL(['Q2', '']),
            UL(['']),   UL(['Q3']),     UL(['Q3', '']),
            UL(['']),   '',             UL(['']),
            UL(['']),   [],             UL(['']),
            UL(['']),   UL([]),         UL(['']),
            UL(['']),   [''],           UL(['', '']),
            UL(['']),   UL(['']),       UL(['', '']),
        ]

        env = Environment()
        failed = 0
        while cases:
            input, prepend, expect = cases[:3]
            env['XXX'] = copy.copy(input)
            try:
                env.Prepend(XXX = prepend)
            except Exception as e:
                if failed == 0: print()
                print("    %s Prepend %s exception: %s" % \
                      (repr(input), repr(prepend), e))
                failed = failed + 1
            else:
                result = env['XXX']
                if result != expect:
                    if failed == 0: print()
                    print("    %s Prepend %s => %s did not match %s" % \
                          (repr(input), repr(prepend), repr(result), repr(expect)))
                    failed = failed + 1
            del cases[:3]
        assert failed == 0, "%d Prepend() cases failed" % failed

        env['UL'] = UL(['foo'])
        env.Prepend(UL = 'bar')
        result = env['UL']
        assert isinstance(result, UL), repr(result)
        assert result == ['b', 'a', 'r', 'foo'], result

        env['CLVar'] = CLVar(['foo'])
        env.Prepend(CLVar = 'bar')
        result = env['CLVar']
        assert isinstance(result, CLVar), repr(result)
        assert result == ['bar', 'foo'], result

        env3 = self.TestEnvironment(X = {'x1' : 7})
        env3.Prepend(X = {'x1' : 8, 'x2' : 9}, Y = {'y1' : 10})
        assert env3['X'] == {'x1': 8, 'x2' : 9}, env3['X']
        assert env3['Y'] == {'y1': 10}, env3['Y']

        z1 = Builder()
        z2 = Builder()
        env4 = self.TestEnvironment(BUILDERS = {'z1' : z1})
        env4.Prepend(BUILDERS = {'z2' : z2})
        assert env4['BUILDERS'] == {'z1' : z1, 'z2' : z2}, env4['BUILDERS']
        assert hasattr(env4, 'z1')
        assert hasattr(env4, 'z2')

    def test_PrependENVPath(self):
        """Test prepending to an ENV path."""
        env1 = self.TestEnvironment(
            ENV={'PATH': r'C:\dir\num\one;C:\dir\num\two'},
            MYENV={'MYPATH': r'C:\mydir\num\one;C:\mydir\num\two'},
        )
        # have to include the pathsep here so that the test will work on UNIX too.
        env1.PrependENVPath('PATH', r'C:\dir\num\two', sep=';')
        env1.PrependENVPath('PATH', r'C:\dir\num\three', sep=';')
        assert (
            env1['ENV']['PATH'] == r'C:\dir\num\three;C:\dir\num\two;C:\dir\num\one'
        ), env1['ENV']['PATH']

        env1.PrependENVPath('MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';')
        env1.PrependENVPath('MYPATH', r'C:\mydir\num\one', 'MYENV', sep=';')
        # this should do nothing since delete_existing is 0
        env1.PrependENVPath(
            'MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';', delete_existing=0
        )
        assert (
            env1['MYENV']['MYPATH'] == r'C:\mydir\num\one;C:\mydir\num\three;C:\mydir\num\two'
        ), env1['MYENV']['MYPATH']

        test = TestCmd.TestCmd(workdir='')
        test.subdir('sub1', 'sub2')
        p = env1['ENV']['PATH']
        env1.PrependENVPath('PATH', '#sub1', sep=';')
        env1.PrependENVPath('PATH', env1.fs.Dir('sub2'), sep=';')
        assert env1['ENV']['PATH'] == 'sub2;sub1;' + p, env1['ENV']['PATH']

    def test_PrependUnique(self):
        """Test prepending unique values to construction variables

        This strips values that are already present when lists are
        involved."""
        env = self.TestEnvironment(AAA1 = 'a1',
                          AAA2 = 'a2',
                          AAA3 = 'a3',
                          AAA4 = 'a4',
                          AAA5 = 'a5',
                          BBB1 = ['b1'],
                          BBB2 = ['b2'],
                          BBB3 = ['b3'],
                          BBB4 = ['b4'],
                          BBB5 = ['b5'],
                          CCC1 = '',
                          CCC2 = '',
                          DDD1 = ['a', 'b', 'c'])
        env.PrependUnique(AAA1 = 'a1',
                          AAA2 = ['a2'],
                          AAA3 = ['a3', 'b', 'c', 'b', 'a3'], # ignore dups
                          AAA4 = 'a4.new',
                          AAA5 = ['a5.new'],
                          BBB1 = 'b1',
                          BBB2 = ['b2'],
                          BBB3 = ['b3', 'b', 'c', 'b3'],
                          BBB4 = 'b4.new',
                          BBB5 = ['b5.new'],
                          CCC1 = 'c1',
                          CCC2 = ['c2'],
                          DDD1 = 'b')
        assert env['AAA1'] == 'a1a1', env['AAA1']
        assert env['AAA2'] == ['a2'], env['AAA2']
        assert env['AAA3'] == ['c', 'b', 'a3'], env['AAA3']
        assert env['AAA4'] == 'a4.newa4', env['AAA4']
        assert env['AAA5'] == ['a5.new', 'a5'], env['AAA5']
        assert env['BBB1'] == ['b1'], env['BBB1']
        assert env['BBB2'] == ['b2'], env['BBB2']
        assert env['BBB3'] == ['b', 'c', 'b3'], env['BBB3']
        assert env['BBB4'] == ['b4.new', 'b4'], env['BBB4']
        assert env['BBB5'] == ['b5.new', 'b5'], env['BBB5']
        assert env['CCC1'] == 'c1', env['CCC1']
        assert env['CCC2'] == ['c2'], env['CCC2']
        assert env['DDD1'] == ['a', 'b', 'c'], env['DDD1']

        env.PrependUnique(DDD1 = 'b', delete_existing=1)
        assert env['DDD1'] == ['b', 'a', 'c'], env['DDD1'] # b moves to front
        env.PrependUnique(DDD1 = ['a','c'], delete_existing=1)
        assert env['DDD1'] == ['a', 'c', 'b'], env['DDD1'] # a & c move to front
        env.PrependUnique(DDD1 = ['d','e','d'], delete_existing=1)
        assert env['DDD1'] == ['d', 'e', 'a', 'c', 'b'], env['DDD1']


        env['CLVar'] = CLVar([])
        env.PrependUnique(CLVar = 'bar')
        result = env['CLVar']
        assert isinstance(result, CLVar), repr(result)
        assert result == ['bar'], result

        env['CLVar'] = CLVar(['abc'])
        env.PrependUnique(CLVar = 'bar')
        result = env['CLVar']
        assert isinstance(result, CLVar), repr(result)
        assert result == ['bar', 'abc'], result

        env['CLVar'] = CLVar(['bar'])
        env.PrependUnique(CLVar = 'bar')
        result = env['CLVar']
        assert isinstance(result, CLVar), repr(result)
        assert result == ['bar'], result

    def test_Replace(self):
        """Test replacing construction variables in an Environment

        After creation of the Environment, of course.
        """
        env1 = self.TestEnvironment(AAA = 'a', BBB = 'b')
        env1.Replace(BBB = 'bbb', CCC = 'ccc')

        env2 = self.TestEnvironment(AAA = 'a', BBB = 'bbb', CCC = 'ccc')
        assert env1 == env2, diff_env(env1, env2)

        b1 = Builder()
        b2 = Builder()
        env3 = self.TestEnvironment(BUILDERS = {'b1' : b1})
        assert hasattr(env3, 'b1'), "b1 was not set"
        env3.Replace(BUILDERS = {'b2' : b2})
        assert not hasattr(env3, 'b1'), "b1 was not cleared"
        assert hasattr(env3, 'b2'), "b2 was not set"

    def test_ReplaceIxes(self):
        """Test ReplaceIxes()"""
        env = self.TestEnvironment(LIBPREFIX='lib',
                          LIBSUFFIX='.a',
                          SHLIBPREFIX='lib',
                          SHLIBSUFFIX='.so',
                          PREFIX='pre',
                          SUFFIX='post')

        assert 'libfoo.a' == env.ReplaceIxes('libfoo.so',
                                             'SHLIBPREFIX', 'SHLIBSUFFIX',
                                             'LIBPREFIX', 'LIBSUFFIX')

        assert os.path.join('dir', 'libfoo.a') == env.ReplaceIxes(os.path.join('dir', 'libfoo.so'),
                                                                   'SHLIBPREFIX', 'SHLIBSUFFIX',
                                                                   'LIBPREFIX', 'LIBSUFFIX')

        assert 'libfoo.a' == env.ReplaceIxes('prefoopost',
                                             'PREFIX', 'SUFFIX',
                                             'LIBPREFIX', 'LIBSUFFIX')

    def test_SetDefault(self):
        """Test the SetDefault method"""
        env = self.TestEnvironment(tools = [])
        env.SetDefault(V1 = 1)
        env.SetDefault(V1 = 2)
        assert env['V1'] == 1
        env['V2'] = 2
        env.SetDefault(V2 = 1)
        assert env['V2'] == 2

    def test_Tool(self):
        """Test the Tool() method"""
        env = self.TestEnvironment(LINK='link', NONE='no-such-tool')

        exc_caught = None
        try:
            tool = env.Tool('does_not_exist')
        except SCons.Errors.UserError:
            exc_caught = 1
        else:
            assert isinstance(tool, SCons.Tool.Tool)
        assert exc_caught, "did not catch expected UserError"

        exc_caught = None
        try:
            env.Tool('$NONE')
        except SCons.Errors.UserError:
            exc_caught = 1
        assert exc_caught, "did not catch expected UserError"

        # Use a non-existent toolpath directory just to make sure we
        # can call Tool() with the keyword argument.
        env.Tool('cc', toolpath=['/no/such/directory'])
        assert env['CC'] == 'cc', env['CC']

        env.Tool('$LINK')
        assert env['LINK'] == '$SMARTLINK', env['LINK']

        # Test that the environment stores the toolpath and
        # re-uses it for later calls.
        test = TestCmd.TestCmd(workdir = '')

        test.write('xxx.py', """\
def exists(env):
    return True
def generate(env):
    env['XXX'] = 'one'
""")

        test.write('yyy.py', """\
def exists(env):
    return True
def generate(env):
    env['YYY'] = 'two'
""")

        env = self.TestEnvironment(tools=['xxx'], toolpath=[test.workpath('')])
        assert env['XXX'] == 'one', env['XXX']
        env.Tool('yyy')
        assert env['YYY'] == 'two', env['YYY']

    def test_WhereIs(self):
        """Test the WhereIs() method"""
        test = TestCmd.TestCmd(workdir = '')

        sub1_xxx_exe = test.workpath('sub1', 'xxx.exe')
        sub2_xxx_exe = test.workpath('sub2', 'xxx.exe')
        sub3_xxx_exe = test.workpath('sub3', 'xxx.exe')
        sub4_xxx_exe = test.workpath('sub4', 'xxx.exe')

        test.subdir('subdir', 'sub1', 'sub2', 'sub3', 'sub4')

        if sys.platform != 'win32':
            test.write(sub1_xxx_exe, "\n")

        os.mkdir(sub2_xxx_exe)

        test.write(sub3_xxx_exe, "\n")
        os.chmod(sub3_xxx_exe, 0o777)

        test.write(sub4_xxx_exe, "\n")
        os.chmod(sub4_xxx_exe, 0o777)

        env_path = os.environ['PATH']

        pathdirs_1234 = [ test.workpath('sub1'),
                          test.workpath('sub2'),
                          test.workpath('sub3'),
                          test.workpath('sub4'),
                        ] + env_path.split(os.pathsep)

        pathdirs_1243 = [ test.workpath('sub1'),
                          test.workpath('sub2'),
                          test.workpath('sub4'),
                          test.workpath('sub3'),
                        ] + env_path.split(os.pathsep)

        path = os.pathsep.join(pathdirs_1234)
        env = self.TestEnvironment(ENV = {'PATH' : path})
        wi = env.WhereIs('')
        assert wi is None
        wi = env.WhereIs('xxx.exe')
        assert wi == test.workpath(sub3_xxx_exe), wi
        wi = env.WhereIs('xxx.exe', pathdirs_1243)
        assert wi == test.workpath(sub4_xxx_exe), wi
        wi = env.WhereIs('xxx.exe', os.pathsep.join(pathdirs_1243))
        assert wi == test.workpath(sub4_xxx_exe), wi

        wi = env.WhereIs('xxx.exe', reject = sub3_xxx_exe)
        assert wi == test.workpath(sub4_xxx_exe), wi
        wi = env.WhereIs('xxx.exe', pathdirs_1243, reject = sub3_xxx_exe)
        assert wi == test.workpath(sub4_xxx_exe), wi

        path = os.pathsep.join(pathdirs_1243)
        env = self.TestEnvironment(ENV = {'PATH' : path})
        wi = env.WhereIs('xxx.exe')
        assert wi == test.workpath(sub4_xxx_exe), wi
        wi = env.WhereIs('xxx.exe', pathdirs_1234)
        assert wi == test.workpath(sub3_xxx_exe), wi
        wi = env.WhereIs('xxx.exe', os.pathsep.join(pathdirs_1234))
        assert wi == test.workpath(sub3_xxx_exe), wi

        if sys.platform == 'win32':
            wi = env.WhereIs('xxx', pathext = '')
            assert wi is None, wi

            wi = env.WhereIs('xxx', pathext = '.exe')
            assert wi == test.workpath(sub4_xxx_exe), wi

            wi = env.WhereIs('xxx', path = pathdirs_1234, pathext = '.BAT;.EXE')
            assert wi.lower() == test.workpath(sub3_xxx_exe).lower(), wi

            # Test that we return a normalized path even when
            # the path contains forward slashes.
            forward_slash = test.workpath('') + '/sub3'
            wi = env.WhereIs('xxx', path = forward_slash, pathext = '.EXE')
            assert wi.lower() == test.workpath(sub3_xxx_exe).lower(), wi



    def test_Action(self):
        """Test the Action() method"""
        import SCons.Action

        env = self.TestEnvironment(FOO = 'xyzzy')

        a = env.Action('foo')
        assert a, a
        assert a.__class__ is SCons.Action.CommandAction, a.__class__

        a = env.Action('$FOO')
        assert a, a
        assert a.__class__ is SCons.Action.CommandAction, a.__class__

        a = env.Action('$$FOO')
        assert a, a
        assert a.__class__ is SCons.Action.LazyAction, a.__class__

        a = env.Action(['$FOO', 'foo'])
        assert a, a
        assert a.__class__ is SCons.Action.ListAction, a.__class__

        def func(arg):
            pass
        a = env.Action(func)
        assert a, a
        assert a.__class__ is SCons.Action.FunctionAction, a.__class__

    def test_AddPostAction(self):
        """Test the AddPostAction() method"""
        env = self.TestEnvironment(FOO='fff', BAR='bbb')

        n = env.AddPostAction('$FOO', lambda x: x)
        assert str(n[0]) == 'fff', n[0]

        n = env.AddPostAction(['ggg', '$BAR'], lambda x: x)
        assert str(n[0]) == 'ggg', n[0]
        assert str(n[1]) == 'bbb', n[1]

    def test_AddPreAction(self):
        """Test the AddPreAction() method"""
        env = self.TestEnvironment(FOO='fff', BAR='bbb')

        n = env.AddPreAction('$FOO', lambda x: x)
        assert str(n[0]) == 'fff', n[0]

        n = env.AddPreAction(['ggg', '$BAR'], lambda x: x)
        assert str(n[0]) == 'ggg', n[0]
        assert str(n[1]) == 'bbb', n[1]

    def test_Alias(self):
        """Test the Alias() method"""
        env = self.TestEnvironment(FOO='kkk', BAR='lll', EA='export_alias')

        tgt = env.Alias('new_alias')[0]
        assert str(tgt) == 'new_alias', tgt
        assert tgt.sources == [], tgt.sources
        assert not hasattr(tgt, 'builder'), tgt.builder

        tgt = env.Alias('None_alias', None)[0]
        assert str(tgt) == 'None_alias', tgt
        assert tgt.sources == [], tgt.sources

        tgt = env.Alias('empty_list', [])[0]
        assert str(tgt) == 'empty_list', tgt
        assert tgt.sources == [], tgt.sources

        tgt = env.Alias('export_alias', [ 'asrc1', '$FOO' ])[0]
        assert str(tgt) == 'export_alias', tgt
        assert len(tgt.sources) == 2, list(map(str, tgt.sources))
        assert str(tgt.sources[0]) == 'asrc1', list(map(str, tgt.sources))
        assert str(tgt.sources[1]) == 'kkk', list(map(str, tgt.sources))

        n = env.Alias(tgt, source = ['$BAR', 'asrc4'])[0]
        assert n is tgt, n
        assert len(tgt.sources) == 4, list(map(str, tgt.sources))
        assert str(tgt.sources[2]) == 'lll', list(map(str, tgt.sources))
        assert str(tgt.sources[3]) == 'asrc4', list(map(str, tgt.sources))

        n = env.Alias('$EA', 'asrc5')[0]
        assert n is tgt, n
        assert len(tgt.sources) == 5, list(map(str, tgt.sources))
        assert str(tgt.sources[4]) == 'asrc5', list(map(str, tgt.sources))

        t1, t2 = env.Alias(['t1', 't2'], ['asrc6', 'asrc7'])
        assert str(t1) == 't1', t1
        assert str(t2) == 't2', t2
        assert len(t1.sources) == 2, list(map(str, t1.sources))
        assert str(t1.sources[0]) == 'asrc6', list(map(str, t1.sources))
        assert str(t1.sources[1]) == 'asrc7', list(map(str, t1.sources))
        assert len(t2.sources) == 2, list(map(str, t2.sources))
        assert str(t2.sources[0]) == 'asrc6', list(map(str, t2.sources))
        assert str(t2.sources[1]) == 'asrc7', list(map(str, t2.sources))

        tgt = env.Alias('add', 's1')
        tgt = env.Alias('add', 's2')[0]
        s = list(map(str, tgt.sources))
        assert s == ['s1', 's2'], s
        tgt = env.Alias(tgt, 's3')[0]
        s = list(map(str, tgt.sources))
        assert s == ['s1', 's2', 's3'], s

        tgt = env.Alias('act', None, "action1")[0]
        s = str(tgt.builder.action)
        assert s == "action1", s
        tgt = env.Alias('act', None, "action2")[0]
        s = str(tgt.builder.action)
        assert s == "action1\naction2", s
        tgt = env.Alias(tgt, None, "action3")[0]
        s = str(tgt.builder.action)
        assert s == "action1\naction2\naction3", s

    def test_AlwaysBuild(self):
        """Test the AlwaysBuild() method"""
        env = self.TestEnvironment(FOO='fff', BAR='bbb')
        t = env.AlwaysBuild('a', 'b$FOO', ['c', 'd'], '$BAR',
                            env.fs.Dir('dir'), env.fs.File('file'))
        assert t[0].__class__.__name__ == 'Entry'
        assert t[0].get_internal_path() == 'a'
        assert t[0].always_build
        assert t[1].__class__.__name__ == 'Entry'
        assert t[1].get_internal_path() == 'bfff'
        assert t[1].always_build
        assert t[2].__class__.__name__ == 'Entry'
        assert t[2].get_internal_path() == 'c'
        assert t[2].always_build
        assert t[3].__class__.__name__ == 'Entry'
        assert t[3].get_internal_path() == 'd'
        assert t[3].always_build
        assert t[4].__class__.__name__ == 'Entry'
        assert t[4].get_internal_path() == 'bbb'
        assert t[4].always_build
        assert t[5].__class__.__name__ == 'Dir'
        assert t[5].get_internal_path() == 'dir'
        assert t[5].always_build
        assert t[6].__class__.__name__ == 'File'
        assert t[6].get_internal_path() == 'file'
        assert t[6].always_build

    def test_VariantDir(self):
        """Test the VariantDir() method"""
        class MyFS:
             def Dir(self, name):
                 return name
             def VariantDir(self, variant_dir, src_dir, duplicate):
                 self.variant_dir = variant_dir
                 self.src_dir = src_dir
                 self.duplicate = duplicate

        env = self.TestEnvironment(FOO = 'fff', BAR = 'bbb')
        env.fs = MyFS()

        env.VariantDir('build', 'src')
        assert env.fs.variant_dir == 'build', env.fs.variant_dir
        assert env.fs.src_dir == 'src', env.fs.src_dir
        assert env.fs.duplicate == 1, env.fs.duplicate

        env.VariantDir('build${FOO}', '${BAR}src', 0)
        assert env.fs.variant_dir == 'buildfff', env.fs.variant_dir
        assert env.fs.src_dir == 'bbbsrc', env.fs.src_dir
        assert env.fs.duplicate == 0, env.fs.duplicate

    def test_Builder(self):
        """Test the Builder() method"""
        env = self.TestEnvironment(FOO = 'xyzzy')

        b = env.Builder(action = 'foo')
        assert b is not None, b

        b = env.Builder(action = '$FOO')
        assert b is not None, b

        b = env.Builder(action = ['$FOO', 'foo'])
        assert b is not None, b

        def func(arg):
            pass
        b = env.Builder(action = func)
        assert b is not None, b
        b = env.Builder(generator = func)
        assert b is not None, b

    def test_CacheDir(self):
        """Test the CacheDir() method"""

        test = TestCmd.TestCmd(workdir = '')

        test_cachedir = os.path.join(test.workpath(),'CacheDir')
        test_cachedir_config = os.path.join(test_cachedir, 'config')
        test_foo = os.path.join(test.workpath(), 'foo-cachedir')
        test_foo_config = os.path.join(test_foo,'config')
        test_foo1 = os.path.join(test.workpath(), 'foo1-cachedir')
        test_foo1_config = os.path.join(test_foo1, 'config')

        env = self.TestEnvironment(CD = test_cachedir)

        env.CacheDir(test_foo)
        assert env._CacheDir_path == test_foo, env._CacheDir_path
        assert os.path.isfile(test_foo_config), "No file %s"%test_foo_config

        env.CacheDir('$CD')
        assert env._CacheDir_path == test_cachedir, env._CacheDir_path
        assert os.path.isfile(test_cachedir_config), "No file %s"%test_cachedir_config

        # Now verify that -n/-no_exec wil prevent the CacheDir/config from being created
        import SCons.Action
        SCons.Action.execute_actions = False
        env.CacheDir(test_foo1)
        assert env._CacheDir_path == test_foo1, env._CacheDir_path
        assert not os.path.isfile(test_foo1_config), "No file %s"%test_foo1_config


    def test_Clean(self):
        """Test the Clean() method"""
        env = self.TestEnvironment(FOO = 'fff', BAR = 'bbb')

        CT = SCons.Environment.CleanTargets

        foo = env.arg2nodes('foo')[0]
        fff = env.arg2nodes('fff')[0]

        t = env.Clean('foo', 'aaa')
        l = list(map(str, CT[foo]))
        assert l == ['aaa'], l

        t = env.Clean(foo, ['$BAR', 'ccc'])
        l = list(map(str, CT[foo]))
        assert l == ['aaa', 'bbb', 'ccc'], l

        eee = env.arg2nodes('eee')[0]

        t = env.Clean('$FOO', 'ddd')
        l = list(map(str, CT[fff]))
        assert l == ['ddd'], l
        t = env.Clean(fff, [eee, 'fff'])
        l = list(map(str, CT[fff]))
        assert l == ['ddd', 'eee', 'fff'], l

    def test_Command(self):
        """Test the Command() method."""
        env = Environment()
        t = env.Command(target='foo.out', source=['foo1.in', 'foo2.in'],
                        action='buildfoo $target $source')[0]
        assert t.builder is not None
        assert t.builder.action.__class__.__name__ == 'CommandAction'
        assert t.builder.action.cmd_list == 'buildfoo $target $source'
        assert 'foo1.in' in [x.get_internal_path() for x in t.sources]
        assert 'foo2.in' in [x.get_internal_path() for x in t.sources]

        sub = env.fs.Dir('sub')
        t = env.Command(target='bar.out', source='sub',
                        action='buildbar $target $source')[0]
        assert 'sub' in [x.get_internal_path() for x in t.sources]

        def testFunc(env, target, source):
            assert str(target[0]) == 'foo.out'
            srcs = list(map(str, source))
            assert 'foo1.in' in srcs and 'foo2.in' in srcs, srcs
            return 0

        # avoid spurious output from action
        act = env.Action(testFunc, cmdstr=None)
        t = env.Command(target='foo.out', source=['foo1.in','foo2.in'],
                        action=act)[0]
        assert t.builder is not None
        assert t.builder.action.__class__.__name__ == 'FunctionAction'
        t.build()
        assert 'foo1.in' in [x.get_internal_path() for x in t.sources]
        assert 'foo2.in' in [x.get_internal_path() for x in t.sources]

        x = []
        def test2(baz, x=x):
            x.append(baz)
        env = self.TestEnvironment(TEST2 = test2)
        t = env.Command(target='baz.out', source='baz.in',
                        action='${TEST2(XYZ)}',
                        XYZ='magic word')[0]
        assert t.builder is not None
        t.build()
        assert x[0] == 'magic word', x

        t = env.Command(target='${X}.out', source='${X}.in',
                        action = 'foo',
                        X = 'xxx')[0]
        assert str(t) == 'xxx.out', str(t)
        assert 'xxx.in' in [x.get_internal_path() for x in t.sources]

        env = self.TestEnvironment(source_scanner = 'should_not_find_this')
        t = env.Command(target='file.out', source='file.in',
                        action = 'foo',
                        source_scanner = 'fake')[0]
        assert t.builder.source_scanner == 'fake', t.builder.source_scanner

    def test_Configure(self):
        """Test the Configure() method"""
        # Configure() will write to a local temporary file.
        test = TestCmd.TestCmd(workdir = '')
        save = os.getcwd()

        try:
            os.chdir(test.workpath())

            env = self.TestEnvironment(FOO = 'xyzzy')

            def func(arg):
                pass

            c = env.Configure()
            assert c is not None, c
            c.Finish()

            c = env.Configure(custom_tests = {'foo' : func, '$FOO' : func})
            assert c is not None, c
            assert hasattr(c, 'foo')
            assert hasattr(c, 'xyzzy')
            c.Finish()
        finally:
            os.chdir(save)

    def test_Depends(self):
        """Test the explicit Depends method."""
        env = self.TestEnvironment(FOO = 'xxx', BAR='yyy')
        env.Dir('dir1')
        env.Dir('dir2')
        env.File('xxx.py')
        env.File('yyy.py')
        t = env.Depends(target='EnvironmentTest.py',
                        dependency='Environment.py')[0]
        assert t.__class__.__name__ == 'Entry', t.__class__.__name__
        assert t.get_internal_path() == 'EnvironmentTest.py'
        assert len(t.depends) == 1
        d = t.depends[0]
        assert d.__class__.__name__ == 'Entry', d.__class__.__name__
        assert d.get_internal_path() == 'Environment.py'

        t = env.Depends(target='${FOO}.py', dependency='${BAR}.py')[0]
        assert t.__class__.__name__ == 'File', t.__class__.__name__
        assert t.get_internal_path() == 'xxx.py'
        assert len(t.depends) == 1
        d = t.depends[0]
        assert d.__class__.__name__ == 'File', d.__class__.__name__
        assert d.get_internal_path() == 'yyy.py'

        t = env.Depends(target='dir1', dependency='dir2')[0]
        assert t.__class__.__name__ == 'Dir', t.__class__.__name__
        assert t.get_internal_path() == 'dir1'
        assert len(t.depends) == 1
        d = t.depends[0]
        assert d.__class__.__name__ == 'Dir', d.__class__.__name__
        assert d.get_internal_path() == 'dir2'

    def test_Dir(self):
        """Test the Dir() method"""
        class MyFS:
            def Dir(self, name):
                return 'Dir(%s)' % name

        env = self.TestEnvironment(FOO = 'foodir', BAR = 'bardir')
        env.fs = MyFS()

        d = env.Dir('d')
        assert d == 'Dir(d)', d

        d = env.Dir('$FOO')
        assert d == 'Dir(foodir)', d

        d = env.Dir('${BAR}_$BAR')
        assert d == 'Dir(bardir_bardir)', d

        d = env.Dir(['dir1'])
        assert d == ['Dir(dir1)'], d

        d = env.Dir(['dir1', 'dir2'])
        assert d == ['Dir(dir1)', 'Dir(dir2)'], d

    def test_NoClean(self):
        """Test the NoClean() method"""
        env = self.TestEnvironment(FOO='ggg', BAR='hhh')
        env.Dir('p_hhhb')
        env.File('p_d')
        t = env.NoClean('p_a', 'p_${BAR}b', ['p_c', 'p_d'], 'p_$FOO')

        assert t[0].__class__.__name__ == 'Entry', t[0].__class__.__name__
        assert t[0].get_internal_path() == 'p_a'
        assert t[0].noclean
        assert t[1].__class__.__name__ == 'Dir', t[1].__class__.__name__
        assert t[1].get_internal_path() == 'p_hhhb'
        assert t[1].noclean
        assert t[2].__class__.__name__ == 'Entry', t[2].__class__.__name__
        assert t[2].get_internal_path() == 'p_c'
        assert t[2].noclean
        assert t[3].__class__.__name__ == 'File', t[3].__class__.__name__
        assert t[3].get_internal_path() == 'p_d'
        assert t[3].noclean
        assert t[4].__class__.__name__ == 'Entry', t[4].__class__.__name__
        assert t[4].get_internal_path() == 'p_ggg'
        assert t[4].noclean

    def test_Dump(self):
        """Test the Dump() method"""

        env = self.TestEnvironment(FOO = 'foo')
        assert env.Dump('FOO') == "'foo'", env.Dump('FOO')
        assert len(env.Dump()) > 200, env.Dump()    # no args version

        assert env.Dump('FOO', 'json') == '"foo"'    # JSON key version
        import json
        env_dict = json.loads(env.Dump(format = 'json'))
        assert env_dict['FOO'] == 'foo'    # full JSON version

        try:
            env.Dump(format = 'markdown')
        except ValueError as e:
            assert str(e) == "Unsupported serialization format: markdown."
        else:
            self.fail("Did not catch expected ValueError.")

    def test_Environment(self):
        """Test the Environment() method"""
        env = self.TestEnvironment(FOO = 'xxx', BAR = 'yyy')

        e2 = env.Environment(X = '$FOO', Y = '$BAR')
        assert e2['X'] == 'xxx', e2['X']
        assert e2['Y'] == 'yyy', e2['Y']

    def test_Execute(self):
        """Test the Execute() method"""

        class MyAction:
            def __init__(self, *args, **kw):
                self.args = args
            def __call__(self, target, source, env):
                return "%s executed" % self.args

        env = Environment()
        env.Action = MyAction

        result = env.Execute("foo")
        assert result == "foo executed", result

    def test_Entry(self):
        """Test the Entry() method"""
        class MyFS:
            def Entry(self, name):
                return 'Entry(%s)' % name

        env = self.TestEnvironment(FOO = 'fooentry', BAR = 'barentry')
        env.fs = MyFS()

        e = env.Entry('e')
        assert e == 'Entry(e)', e

        e = env.Entry('$FOO')
        assert e == 'Entry(fooentry)', e

        e = env.Entry('${BAR}_$BAR')
        assert e == 'Entry(barentry_barentry)', e

        e = env.Entry(['entry1'])
        assert e == ['Entry(entry1)'], e

        e = env.Entry(['entry1', 'entry2'])
        assert e == ['Entry(entry1)', 'Entry(entry2)'], e

    def test_File(self):
        """Test the File() method"""
        class MyFS:
            def File(self, name):
                return 'File(%s)' % name

        env = self.TestEnvironment(FOO = 'foofile', BAR = 'barfile')
        env.fs = MyFS()

        f = env.File('f')
        assert f == 'File(f)', f

        f = env.File('$FOO')
        assert f == 'File(foofile)', f

        f = env.File('${BAR}_$BAR')
        assert f == 'File(barfile_barfile)', f

        f = env.File(['file1'])
        assert f == ['File(file1)'], f

        f = env.File(['file1', 'file2'])
        assert f == ['File(file1)', 'File(file2)'], f

    def test_FindFile(self):
        """Test the FindFile() method"""
        env = self.TestEnvironment(FOO = 'fff', BAR = 'bbb')

        r = env.FindFile('foo', ['no_such_directory'])
        assert r is None, r

        # XXX

    def test_Flatten(self):
        """Test the Flatten() method"""
        env = Environment()
        l = env.Flatten([1])
        assert l == [1]
        l = env.Flatten([1, [2, [3, [4]]]])
        assert l == [1, 2, 3, 4], l

    def test_GetBuildPath(self):
        """Test the GetBuildPath() method."""
        env = self.TestEnvironment(MAGIC = 'xyzzy')

        p = env.GetBuildPath('foo')
        assert p == 'foo', p

        p = env.GetBuildPath('$MAGIC')
        assert p == 'xyzzy', p

    def test_Ignore(self):
        """Test the explicit Ignore method."""
        env = self.TestEnvironment(FOO='yyy', BAR='zzz')
        env.Dir('dir1')
        env.Dir('dir2')
        env.File('yyyzzz')
        env.File('zzzyyy')

        t = env.Ignore(target='targ.py', dependency='dep.py')[0]
        assert t.__class__.__name__ == 'Entry', t.__class__.__name__
        assert t.get_internal_path() == 'targ.py'
        assert len(t.ignore) == 1
        i = t.ignore[0]
        assert i.__class__.__name__ == 'Entry', i.__class__.__name__
        assert i.get_internal_path() == 'dep.py'

        t = env.Ignore(target='$FOO$BAR', dependency='$BAR$FOO')[0]
        assert t.__class__.__name__ == 'File', t.__class__.__name__
        assert t.get_internal_path() == 'yyyzzz'
        assert len(t.ignore) == 1
        i = t.ignore[0]
        assert i.__class__.__name__ == 'File', i.__class__.__name__
        assert i.get_internal_path() == 'zzzyyy'

        t = env.Ignore(target='dir1', dependency='dir2')[0]
        assert t.__class__.__name__ == 'Dir', t.__class__.__name__
        assert t.get_internal_path() == 'dir1'
        assert len(t.ignore) == 1
        i = t.ignore[0]
        assert i.__class__.__name__ == 'Dir', i.__class__.__name__
        assert i.get_internal_path() == 'dir2'

    def test_Literal(self):
        """Test the Literal() method"""
        env = self.TestEnvironment(FOO='fff', BAR='bbb')
        list = env.subst_list([env.Literal('$FOO'), '$BAR'])[0]
        assert list == ['$FOO', 'bbb'], list
        list = env.subst_list(['$FOO', env.Literal('$BAR')])[0]
        assert list == ['fff', '$BAR'], list

    def test_Local(self):
        """Test the Local() method."""
        env = self.TestEnvironment(FOO='lll')

        l = env.Local(env.fs.File('fff'))
        assert str(l[0]) == 'fff', l[0]

        l = env.Local('ggg', '$FOO')
        assert str(l[0]) == 'ggg', l[0]
        assert str(l[1]) == 'lll', l[1]

    def test_Precious(self):
        """Test the Precious() method"""
        env = self.TestEnvironment(FOO='ggg', BAR='hhh')
        env.Dir('p_hhhb')
        env.File('p_d')
        t = env.Precious('p_a', 'p_${BAR}b', ['p_c', 'p_d'], 'p_$FOO')

        assert t[0].__class__.__name__ == 'Entry', t[0].__class__.__name__
        assert t[0].get_internal_path() == 'p_a'
        assert t[0].precious
        assert t[1].__class__.__name__ == 'Dir', t[1].__class__.__name__
        assert t[1].get_internal_path() == 'p_hhhb'
        assert t[1].precious
        assert t[2].__class__.__name__ == 'Entry', t[2].__class__.__name__
        assert t[2].get_internal_path() == 'p_c'
        assert t[2].precious
        assert t[3].__class__.__name__ == 'File', t[3].__class__.__name__
        assert t[3].get_internal_path() == 'p_d'
        assert t[3].precious
        assert t[4].__class__.__name__ == 'Entry', t[4].__class__.__name__
        assert t[4].get_internal_path() == 'p_ggg'
        assert t[4].precious

    def test_Pseudo(self):
        """Test the Pseudo() method"""
        env = self.TestEnvironment(FOO='ggg', BAR='hhh')
        env.Dir('p_hhhb')
        env.File('p_d')
        t = env.Pseudo('p_a', 'p_${BAR}b', ['p_c', 'p_d'], 'p_$FOO')

        assert t[0].__class__.__name__ == 'Entry', t[0].__class__.__name__
        assert t[0].get_internal_path() == 'p_a'
        assert t[0].pseudo
        assert t[1].__class__.__name__ == 'Dir', t[1].__class__.__name__
        assert t[1].get_internal_path() == 'p_hhhb'
        assert t[1].pseudo
        assert t[2].__class__.__name__ == 'Entry', t[2].__class__.__name__
        assert t[2].get_internal_path() == 'p_c'
        assert t[2].pseudo
        assert t[3].__class__.__name__ == 'File', t[3].__class__.__name__
        assert t[3].get_internal_path() == 'p_d'
        assert t[3].pseudo
        assert t[4].__class__.__name__ == 'Entry', t[4].__class__.__name__
        assert t[4].get_internal_path() == 'p_ggg'
        assert t[4].pseudo

    def test_Repository(self):
        """Test the Repository() method."""
        class MyFS:
            def __init__(self):
                self.list = []
            def Repository(self, *dirs):
                self.list.extend(list(dirs))
            def Dir(self, name):
                return name
        env = self.TestEnvironment(FOO='rrr', BAR='sss')
        env.fs = MyFS()
        env.Repository('/tmp/foo')
        env.Repository('/tmp/$FOO', '/tmp/$BAR/foo')
        expect = ['/tmp/foo', '/tmp/rrr', '/tmp/sss/foo']
        assert env.fs.list == expect, env.fs.list

    def test_Scanner(self):
        """Test the Scanner() method"""
        def scan(node, env, target, arg):
            pass

        env = self.TestEnvironment(FOO = scan)

        s = env.Scanner('foo')
        assert s is not None, s

        s = env.Scanner(function = 'foo')
        assert s is not None, s

        if 0:
            s = env.Scanner('$FOO')
            assert s is not None, s

            s = env.Scanner(function = '$FOO')
            assert s is not None, s

    def test_SConsignFile(self):
        """Test the SConsignFile() method"""
        import SCons.SConsign

        class MyFS:
            SConstruct_dir = os.sep + 'dir'

        env = self.TestEnvironment(FOO = 'SConsign',
                          BAR = os.path.join(os.sep, 'File'))
        env.fs = MyFS()
        env.Execute = lambda action: None

        try:
            fnames = []
            dbms = []
            def capture(name, dbm_module, fnames=fnames, dbms=dbms):
                fnames.append(name)
                dbms.append(dbm_module)

            save_SConsign_File = SCons.SConsign.File
            SCons.SConsign.File = capture

            env.SConsignFile('foo')
            assert fnames[-1] == os.path.join(os.sep, 'dir', 'foo'), fnames
            assert dbms[-1] is None, dbms

            env.SConsignFile('$FOO')
            assert fnames[-1] == os.path.join(os.sep, 'dir', 'SConsign'), fnames
            assert dbms[-1] is None, dbms

            env.SConsignFile('/$FOO')
            assert fnames[-1] == os.sep + 'SConsign', fnames
            assert dbms[-1] is None, dbms

            env.SConsignFile(os.sep + '$FOO')
            assert fnames[-1] == os.sep + 'SConsign', fnames
            assert dbms[-1] is None, dbms

            env.SConsignFile('$BAR', 'x')
            assert fnames[-1] == os.path.join(os.sep, 'File'), fnames
            assert dbms[-1] == 'x', dbms

            env.SConsignFile('__$BAR', 7)
            assert fnames[-1] == os.path.join(os.sep, 'dir', '__', 'File'), fnames
            assert dbms[-1] == 7, dbms

            env.SConsignFile()
            assert fnames[-1] == os.path.join(os.sep, 'dir', current_sconsign_filename()), fnames
            assert dbms[-1] is None, dbms

            env.SConsignFile(None)
            assert fnames[-1] is None, fnames
            assert dbms[-1] is None, dbms
        finally:
            SCons.SConsign.File = save_SConsign_File

    def test_SideEffect(self):
        """Test the SideEffect() method"""
        env = self.TestEnvironment(LIB='lll', FOO='fff', BAR='bbb')
        env.File('mylll.pdb')
        env.Dir('mymmm.pdb')

        foo = env.Object('foo.obj', 'foo.cpp')[0]
        bar = env.Object('bar.obj', 'bar.cpp')[0]
        s = env.SideEffect('mylib.pdb', ['foo.obj', 'bar.obj'])
        assert len(s) == 1, len(s)
        s = s[0]
        assert s.__class__.__name__ == 'Entry', s.__class__.__name__
        assert s.get_internal_path() == 'mylib.pdb'
        assert s.side_effect
        assert foo.side_effects == [s]
        assert bar.side_effects == [s]

        fff = env.Object('fff.obj', 'fff.cpp')[0]
        bbb = env.Object('bbb.obj', 'bbb.cpp')[0]
        s = env.SideEffect('my${LIB}.pdb', ['${FOO}.obj', '${BAR}.obj'])
        assert len(s) == 1, len(s)
        s = s[0]
        assert s.__class__.__name__ == 'File', s.__class__.__name__
        assert s.get_internal_path() == 'mylll.pdb'
        assert s.side_effect
        assert fff.side_effects == [s], fff.side_effects
        assert bbb.side_effects == [s], bbb.side_effects

        ggg = env.Object('ggg.obj', 'ggg.cpp')[0]
        ccc = env.Object('ccc.obj', 'ccc.cpp')[0]
        s = env.SideEffect('mymmm.pdb', ['ggg.obj', 'ccc.obj'])
        assert len(s) == 1, len(s)
        s = s[0]
        assert s.__class__.__name__ == 'Dir', s.__class__.__name__
        assert s.get_internal_path() == 'mymmm.pdb'
        assert s.side_effect
        assert ggg.side_effects == [s], ggg.side_effects
        assert ccc.side_effects == [s], ccc.side_effects

        # Verify that duplicate side effects are not allowed.
        before = len(ggg.side_effects)
        s = env.SideEffect('mymmm.pdb', ggg)
        assert len(s) == 0, len(s)
        assert len(ggg.side_effects) == before, len(ggg.side_effects)

    def test_Split(self):
        """Test the Split() method"""
        env = self.TestEnvironment(FOO = 'fff', BAR = 'bbb')
        s = env.Split("foo bar")
        assert s == ["foo", "bar"], s
        s = env.Split("$FOO bar")
        assert s == ["fff", "bar"], s
        s = env.Split(["foo", "bar"])
        assert s == ["foo", "bar"], s
        s = env.Split(["foo", "${BAR}-bbb"])
        assert s == ["foo", "bbb-bbb"], s
        s = env.Split("foo")
        assert s == ["foo"], s
        s = env.Split("$FOO$BAR")
        assert s == ["fffbbb"], s


    def test_Value(self):
        """Test creating a Value() object
        """
        env = Environment()
        v1 = env.Value('a')
        assert v1.value == 'a', v1.value

        value2 = 'a'
        v2 = env.Value(value2)
        assert v2.value == value2, v2.value
        assert v2.value is value2, v2.value

        assert v1 is v2

        v3 = env.Value('c', 'build-c')
        assert v3.value == 'c', v3.value

        v4 = env.Value(b'\x00\x0F', name='name')
        assert v4.value == b'\x00\x0F', v4.value
        assert v4.name == 'name', v4.name


    def test_Environment_global_variable(self):
        """Test setting Environment variable to an Environment.Base subclass"""
        class MyEnv(SCons.Environment.Base):
            def xxx(self, string):
                return self.subst(string)

        SCons.Environment.Environment = MyEnv

        env = SCons.Environment.Environment(FOO = 'foo')

        f = env.subst('$FOO')
        assert f == 'foo', f

        f = env.xxx('$FOO')
        assert f == 'foo', f

    def test_bad_keywords(self):
        """Test trying to use reserved keywords in an Environment"""
        added = []

        env = self.TestEnvironment(TARGETS = 'targets',
                                   SOURCES = 'sources',
                                   SOURCE = 'source',
                                   TARGET = 'target',
                                   CHANGED_SOURCES = 'changed_sources',
                                   CHANGED_TARGETS = 'changed_targets',
                                   UNCHANGED_SOURCES = 'unchanged_sources',
                                   UNCHANGED_TARGETS = 'unchanged_targets',
                                   INIT = 'init')
        bad_msg = '%s is not reserved, but got omitted; see Environment.construction_var_name_ok'
        added.append('INIT')
        for x in self.reserved_variables:
            assert x not in env, env[x]
        for x in added:
            assert x in env, bad_msg % x

        env.Append(TARGETS = 'targets',
                   SOURCES = 'sources',
                   SOURCE = 'source',
                   TARGET = 'target',
                   CHANGED_SOURCES = 'changed_sources',
                   CHANGED_TARGETS = 'changed_targets',
                   UNCHANGED_SOURCES = 'unchanged_sources',
                   UNCHANGED_TARGETS = 'unchanged_targets',
                   APPEND = 'append')
        added.append('APPEND')
        for x in self.reserved_variables:
            assert x not in env, env[x]
        for x in added:
            assert x in env, bad_msg % x

        env.AppendUnique(TARGETS = 'targets',
                         SOURCES = 'sources',
                         SOURCE = 'source',
                         TARGET = 'target',
                         CHANGED_SOURCES = 'changed_sources',
                         CHANGED_TARGETS = 'changed_targets',
                         UNCHANGED_SOURCES = 'unchanged_sources',
                         UNCHANGED_TARGETS = 'unchanged_targets',
                         APPENDUNIQUE = 'appendunique')
        added.append('APPENDUNIQUE')
        for x in self.reserved_variables:
            assert x not in env, env[x]
        for x in added:
            assert x in env, bad_msg % x

        env.Prepend(TARGETS = 'targets',
                    SOURCES = 'sources',
                    SOURCE = 'source',
                    TARGET = 'target',
                    CHANGED_SOURCES = 'changed_sources',
                    CHANGED_TARGETS = 'changed_targets',
                    UNCHANGED_SOURCES = 'unchanged_sources',
                    UNCHANGED_TARGETS = 'unchanged_targets',
                    PREPEND = 'prepend')
        added.append('PREPEND')
        for x in self.reserved_variables:
            assert x not in env, env[x]
        for x in added:
            assert x in env, bad_msg % x

        env.Prepend(TARGETS = 'targets',
                    SOURCES = 'sources',
                    SOURCE = 'source',
                    TARGET = 'target',
                    CHANGED_SOURCES = 'changed_sources',
                    CHANGED_TARGETS = 'changed_targets',
                    UNCHANGED_SOURCES = 'unchanged_sources',
                    UNCHANGED_TARGETS = 'unchanged_targets',
                    PREPENDUNIQUE = 'prependunique')
        added.append('PREPENDUNIQUE')
        for x in self.reserved_variables:
            assert x not in env, env[x]
        for x in added:
            assert x in env, bad_msg % x

        env.Replace(TARGETS = 'targets',
                    SOURCES = 'sources',
                    SOURCE = 'source',
                    TARGET = 'target',
                    CHANGED_SOURCES = 'changed_sources',
                    CHANGED_TARGETS = 'changed_targets',
                    UNCHANGED_SOURCES = 'unchanged_sources',
                    UNCHANGED_TARGETS = 'unchanged_targets',
                    REPLACE = 'replace')
        added.append('REPLACE')
        for x in self.reserved_variables:
            assert x not in env, env[x]
        for x in added:
            assert x in env, bad_msg % x

        copy = env.Clone(TARGETS = 'targets',
                         SOURCES = 'sources',
                         SOURCE = 'source',
                         TARGET = 'target',
                         CHANGED_SOURCES = 'changed_sources',
                         CHANGED_TARGETS = 'changed_targets',
                         UNCHANGED_SOURCES = 'unchanged_sources',
                         UNCHANGED_TARGETS = 'unchanged_targets',
                         COPY = 'copy')
        for x in self.reserved_variables:
            assert x not in copy, env[x]
        for x in added + ['COPY']:
            assert x in copy, bad_msg % x

        over = env.Override({'TARGETS' : 'targets',
                             'SOURCES' : 'sources',
                             'SOURCE' : 'source',
                             'TARGET' : 'target',
                             'CHANGED_SOURCES' : 'changed_sources',
                             'CHANGED_TARGETS' : 'changed_targets',
                             'UNCHANGED_SOURCES' : 'unchanged_sources',
                             'UNCHANGED_TARGETS' : 'unchanged_targets',
                             'OVERRIDE' : 'override'})
        for x in self.reserved_variables:
            assert x not in over, over[x]
        for x in added + ['OVERRIDE']:
            assert x in over, bad_msg % x

    def test_parse_flags(self):
        """Test the Base class parse_flags argument"""
        # all we have to show is that it gets to MergeFlags internally
        env = Environment(tools=[], parse_flags = '-X')
        assert env['CCFLAGS'] == ['-X'], env['CCFLAGS']

        env = Environment(tools=[], CCFLAGS=None, parse_flags = '-Y')
        assert env['CCFLAGS'] == ['-Y'], env['CCFLAGS']

        env = Environment(tools=[], CPPDEFINES = 'FOO', parse_flags = '-std=c99 -X -DBAR')
        assert env['CFLAGS']  == ['-std=c99'], env['CFLAGS']
        assert env['CCFLAGS'] == ['-X'], env['CCFLAGS']
        assert env['CPPDEFINES'] == ['FOO', 'BAR'], env['CPPDEFINES']

    def test_clone_parse_flags(self):
        """Test the env.Clone() parse_flags argument"""
        # all we have to show is that it gets to MergeFlags internally
        env = Environment(tools = [])
        env2 = env.Clone(parse_flags = '-X')
        assert 'CCFLAGS' not in env
        assert env2['CCFLAGS'] == ['-X'], env2['CCFLAGS']

        env = Environment(tools = [], CCFLAGS=None)
        env2 = env.Clone(parse_flags = '-Y')
        assert env['CCFLAGS'] is None, env['CCFLAGS']
        assert env2['CCFLAGS'] == ['-Y'], env2['CCFLAGS']

        env = Environment(tools = [], CPPDEFINES = 'FOO')
        env2 = env.Clone(parse_flags = '-std=c99 -X -DBAR')
        assert 'CFLAGS' not in env
        assert env2['CFLAGS']  == ['-std=c99'], env2['CFLAGS']
        assert 'CCFLAGS' not in env
        assert env2['CCFLAGS'] == ['-X'], env2['CCFLAGS']
        assert env['CPPDEFINES'] == 'FOO', env['CPPDEFINES']
        assert env2['CPPDEFINES'] == ['FOO','BAR'], env2['CPPDEFINES']



class OverrideEnvironmentTestCase(unittest.TestCase,TestEnvironmentFixture):

    def setUp(self):
        env = Environment()
        env._dict = {'XXX' : 'x', 'YYY' : 'y'}
        def verify_value(env, key, value, *args, **kwargs):
            """Verifies that key is value on the env this is called with."""
            assert env[key] == value
        env.AddMethod(verify_value)
        env2 = OverrideEnvironment(env, {'XXX' : 'x2'})
        env3 = OverrideEnvironment(env2, {'XXX' : 'x3', 'YYY' : 'y3', 'ZZZ' : 'z3'})
        self.envs = [ env, env2, env3 ]

    def checkpath(self, node, expect):
        return str(node) == os.path.normpath(expect)

    def test___init__(self):
        """Test OverrideEnvironment initialization"""
        env, env2, env3 = self.envs
        assert env['XXX'] == 'x', env['XXX']
        assert env2['XXX'] == 'x2', env2['XXX']
        assert env3['XXX'] == 'x3', env3['XXX']
        assert env['YYY'] == 'y', env['YYY']
        assert env2['YYY'] == 'y', env2['YYY']
        assert env3['YYY'] == 'y3', env3['YYY']

    def test___delitem__(self):
        """Test deleting variables from an OverrideEnvironment"""
        env, env2, env3 = self.envs

        del env3['XXX']
        assert 'XXX' not in env, "env has XXX?"
        assert 'XXX' not in env2, "env2 has XXX?"
        assert 'XXX' not in env3, "env3 has XXX?"

        del env3['YYY']
        assert 'YYY' not in env, "env has YYY?"
        assert 'YYY' not in env2, "env2 has YYY?"
        assert 'YYY' not in env3, "env3 has YYY?"

        del env3['ZZZ']
        assert 'ZZZ' not in env, "env has ZZZ?"
        assert 'ZZZ' not in env2, "env2 has ZZZ?"
        assert 'ZZZ' not in env3, "env3 has ZZZ?"

    def test_get(self):
        """Test the OverrideEnvironment get() method"""
        env, env2, env3 = self.envs
        assert env.get('XXX') == 'x', env.get('XXX')
        assert env2.get('XXX') == 'x2', env2.get('XXX')
        assert env3.get('XXX') == 'x3', env3.get('XXX')
        assert env.get('YYY') == 'y', env.get('YYY')
        assert env2.get('YYY') == 'y', env2.get('YYY')
        assert env3.get('YYY') == 'y3', env3.get('YYY')
        assert env.get('ZZZ') is None, env.get('ZZZ')
        assert env2.get('ZZZ') is None, env2.get('ZZZ')
        assert env3.get('ZZZ') == 'z3', env3.get('ZZZ')

    def test_contains(self):
        """Test the OverrideEnvironment __contains__() method"""
        env, env2, env3 = self.envs
        assert 'XXX' in env, 'XXX' in env
        assert 'XXX' in env2, 'XXX' in env2
        assert 'XXX' in env3, 'XXX' in env3
        assert 'YYY' in env, 'YYY' in env
        assert 'YYY' in env2, 'YYY' in env2
        assert 'YYY' in env3, 'YYY' in env3
        assert 'ZZZ' not in env, 'ZZZ' in env
        assert 'ZZZ' not in env2, 'ZZZ' in env2
        assert 'ZZZ' in env3, 'ZZZ' in env3

    def test_Dictionary(self):
        """Test the OverrideEnvironment Dictionary() method"""
        env, env2, env3 = self.envs
        # nothing overrriden
        items = env.Dictionary()
        assert items == {'XXX' : 'x', 'YYY' : 'y'}, items
        # env2 overrides XXX, YYY unchanged
        items = env2.Dictionary()
        assert items == {'XXX' : 'x2', 'YYY' : 'y'}, items
        # env3 overrides XXX, YYY, adds ZZZ
        items = env3.Dictionary()
        assert items == {'XXX' : 'x3', 'YYY' : 'y3', 'ZZZ' : 'z3'}, items
        # test one-arg and multi-arg Dictionary
        assert env3.Dictionary('XXX') == 'x3', env3.Dictionary('XXX')
        xxx, yyy = env2.Dictionary('XXX', 'YYY')
        assert xxx == 'x2', xxx
        assert yyy == 'y', yyy
        del env3['XXX']
        assert 'XXX' not in env3.Dictionary()
        assert 'XXX' not in env2.Dictionary()
        assert 'XXX' not in env.Dictionary()

    def test_items(self):
        """Test the OverrideEnvironment items() method"""
        env, env2, env3 = self.envs
        items = sorted(env.items())
        assert items == [('XXX', 'x'), ('YYY', 'y')], items
        items = sorted(env2.items())
        assert items == [('XXX', 'x2'), ('YYY', 'y')], items
        items = sorted(env3.items())
        assert items == [('XXX', 'x3'), ('YYY', 'y3'), ('ZZZ', 'z3')], items

    def test_keys(self):
        """Test the OverrideEnvironment keys() method"""
        env, env2, env3 = self.envs
        keys = sorted(env.keys())
        assert keys == ['XXX', 'YYY'], keys
        keys = sorted(env2.keys())
        assert keys == ['XXX', 'YYY'], keys
        keys = sorted(env3.keys())
        assert keys == ['XXX', 'YYY', 'ZZZ'], keys

    def test_values(self):
        """Test the OverrideEnvironment values() method"""
        env, env2, env3 = self.envs
        values = sorted(env.values())
        assert values == ['x', 'y'], values
        values = sorted(env2.values())
        assert values == ['x2', 'y'], values
        values = sorted(env3.values())
        assert values == ['x3', 'y3', 'z3'], values

    def test_setdefault(self):
        """Test the OverrideEnvironment setdefault() method."""
        env, env2, env3 = self.envs
        # does not set for existing key
        assert env2.setdefault('XXX', 'z') == 'x2', env2['XXX']
        # set/return using default for non-existing key
        assert env2.setdefault('ZZZ', 'z2') == 'z2', env2['ZZZ']
        # set did not leak through to base env
        assert 'ZZZ' not in env

    def test_gvars(self):
        """Test the OverrideEnvironment gvars() method"""
        env, env2, env3 = self.envs
        gvars = env.gvars()
        assert gvars == {'XXX' : 'x', 'YYY' : 'y'}, gvars
        gvars = env2.gvars()
        assert gvars == {'XXX' : 'x', 'YYY' : 'y'}, gvars
        gvars = env3.gvars()
        assert gvars == {'XXX' : 'x', 'YYY' : 'y'}, gvars

    def test_lvars(self):
        """Test the OverrideEnvironment lvars() method"""
        env, env2, env3 = self.envs
        lvars = env.lvars()
        assert lvars == {}, lvars
        lvars = env2.lvars()
        assert lvars == {'XXX' : 'x2'}, lvars
        lvars = env3.lvars()
        assert lvars == {'XXX' : 'x3', 'YYY' : 'y3', 'ZZZ' : 'z3'}, lvars

    def test_Replace(self):
        """Test the OverrideEnvironment Replace() method"""
        env, env2, env3 = self.envs
        assert env['XXX'] == 'x', env['XXX']
        assert env2['XXX'] == 'x2', env2['XXX']
        assert env3['XXX'] == 'x3', env3['XXX']
        assert env['YYY'] == 'y', env['YYY']
        assert env2['YYY'] == 'y', env2['YYY']
        assert env3['YYY'] == 'y3', env3['YYY']

        env.Replace(YYY = 'y4')

        assert env['XXX'] == 'x', env['XXX']
        assert env2['XXX'] == 'x2', env2['XXX']
        assert env3['XXX'] == 'x3', env3['XXX']
        assert env['YYY'] == 'y4', env['YYY']
        assert env2['YYY'] == 'y4', env2['YYY']
        assert env3['YYY'] == 'y3', env3['YYY']

    # Tests a number of Base methods through an OverrideEnvironment to
    # make sure they handle overridden constructionv variables properly.
    #
    # The following Base methods also call self.subst(), and so could
    # theoretically be subject to problems with evaluating overridden
    # variables, but they're never really called that way in the rest
    # of our code, so we won't worry about them (at least for now):
    #
    # ParseConfig()
    # ParseDepends()
    # Platform()
    # Tool()
    #
    # Action()
    # Alias()
    # Builder()
    # CacheDir()
    # Configure()
    # Environment()
    # FindFile()
    # Scanner()

    # It's unlikely Clone() will ever be called this way, so let the
    # other methods test that handling overridden values works.
    #def test_Clone(self):
    #    """Test the OverrideEnvironment Clone() method"""
    #    pass

    def test_FindIxes(self):
        """Test the OverrideEnvironment FindIxes() method"""
        env, env2, env3 = self.envs
        x = env.FindIxes(['xaaay'], 'XXX', 'YYY')
        assert x == 'xaaay', x
        x = env2.FindIxes(['x2aaay'], 'XXX', 'YYY')
        assert x == 'x2aaay', x
        x = env3.FindIxes(['x3aaay3'], 'XXX', 'YYY')
        assert x == 'x3aaay3', x

    def test_ReplaceIxes(self):
        """Test the OverrideEnvironment ReplaceIxes() method"""
        env, env2, env3 = self.envs
        x = env.ReplaceIxes('xaaay', 'XXX', 'YYY', 'YYY', 'XXX')
        assert x == 'yaaax', x
        x = env2.ReplaceIxes('x2aaay', 'XXX', 'YYY', 'YYY', 'XXX')
        assert x == 'yaaax2', x
        x = env3.ReplaceIxes('x3aaay3', 'XXX', 'YYY', 'YYY', 'XXX')
        assert x == 'y3aaax3', x

    # It's unlikely WhereIs() will ever be called this way, so let the
    # other methods test that handling overridden values works.
    #def test_WhereIs(self):
    #    """Test the OverrideEnvironment WhereIs() method"""
    #    pass

    def test_PseudoBuilderInherits(self):
        """Test that pseudo-builders inherit the overrided values."""
        env, env2, env3 = self.envs
        env.verify_value('XXX', 'x')
        env2.verify_value('XXX', 'x2')
        env3.verify_value('XXX', 'x3')

    def test_Dir(self):
        """Test the OverrideEnvironment Dir() method"""
        env, env2, env3 = self.envs
        x = env.Dir('ddir/$XXX')
        assert self.checkpath(x, 'ddir/x'), str(x)
        x = env2.Dir('ddir/$XXX')
        assert self.checkpath(x, 'ddir/x2'), str(x)
        x = env3.Dir('ddir/$XXX')
        assert self.checkpath(x, 'ddir/x3'), str(x)

    def test_Entry(self):
        """Test the OverrideEnvironment Entry() method"""
        env, env2, env3 = self.envs
        x = env.Entry('edir/$XXX')
        assert self.checkpath(x, 'edir/x'), str(x)
        x = env2.Entry('edir/$XXX')
        assert self.checkpath(x, 'edir/x2'), str(x)
        x = env3.Entry('edir/$XXX')
        assert self.checkpath(x, 'edir/x3'), str(x)

    def test_File(self):
        """Test the OverrideEnvironment File() method"""
        env, env2, env3 = self.envs
        x = env.File('fdir/$XXX')
        assert self.checkpath(x, 'fdir/x'), str(x)
        x = env2.File('fdir/$XXX')
        assert self.checkpath(x, 'fdir/x2'), str(x)
        x = env3.File('fdir/$XXX')
        assert self.checkpath(x, 'fdir/x3'), str(x)

    def test_Split(self):
        """Test the OverrideEnvironment Split() method"""
        env, env2, env3 = self.envs
        env['AAA'] = '$XXX $YYY $ZZZ'
        x = env.Split('$AAA')
        assert x == ['x', 'y'], x
        x = env2.Split('$AAA')
        assert x == ['x2', 'y'], x
        x = env3.Split('$AAA')
        assert x == ['x3', 'y3', 'z3'], x

    def test_parse_flags(self):
        """Test the OverrideEnvironment parse_flags argument"""
        # all we have to show is that it gets to MergeFlags internally
        env = SubstitutionEnvironment()
        env2 = env.Override({'parse_flags' : '-X'})
        assert 'CCFLAGS' not in env
        assert env2['CCFLAGS'] == ['-X'], env2['CCFLAGS']

        env = SubstitutionEnvironment(CCFLAGS=None)
        env2 = env.Override({'parse_flags' : '-Y'})
        assert env['CCFLAGS'] is None, env['CCFLAGS']
        assert env2['CCFLAGS'] == ['-Y'], env2['CCFLAGS']

        env = SubstitutionEnvironment(CPPDEFINES = 'FOO')
        env2 = env.Override({'parse_flags' : '-std=c99 -X -DBAR'})
        assert 'CFLAGS' not in env
        assert env2['CFLAGS']  == ['-std=c99'], env2['CFLAGS']
        assert 'CCFLAGS' not in env
        assert env2['CCFLAGS'] == ['-X'], env2['CCFLAGS']
        assert env['CPPDEFINES'] == 'FOO', env['CPPDEFINES']
        assert env2['CPPDEFINES'] == ['FOO','BAR'], env2['CPPDEFINES']



class NoSubstitutionProxyTestCase(unittest.TestCase,TestEnvironmentFixture):

    def test___init__(self):
        """Test NoSubstitutionProxy initialization"""
        env = self.TestEnvironment(XXX = 'x', YYY = 'y')
        assert env['XXX'] == 'x', env['XXX']
        assert env['YYY'] == 'y', env['YYY']

        proxy = NoSubstitutionProxy(env)
        assert proxy['XXX'] == 'x', proxy['XXX']
        assert proxy['YYY'] == 'y', proxy['YYY']

    def test_attributes(self):
        """Test getting and setting NoSubstitutionProxy attributes"""
        env = Environment()
        setattr(env, 'env_attr', 'value1')

        proxy = NoSubstitutionProxy(env)
        setattr(proxy, 'proxy_attr', 'value2')

        x = getattr(env, 'env_attr')
        assert x == 'value1', x
        x = getattr(proxy, 'env_attr')
        assert x == 'value1', x

        x = getattr(env, 'proxy_attr')
        assert x == 'value2', x
        x = getattr(proxy, 'proxy_attr')
        assert x == 'value2', x

    def test_subst(self):
        """Test the NoSubstitutionProxy.subst() method"""
        env = self.TestEnvironment(XXX = 'x', YYY = 'y')
        assert env['XXX'] == 'x', env['XXX']
        assert env['YYY'] == 'y', env['YYY']

        proxy = NoSubstitutionProxy(env)
        assert proxy['XXX'] == 'x', proxy['XXX']
        assert proxy['YYY'] == 'y', proxy['YYY']

        x = env.subst('$XXX')
        assert x == 'x', x
        x = proxy.subst('$XXX')
        assert x == '$XXX', x

        x = proxy.subst('$YYY', raw=7, target=None, source=None,
                        conv=None,
                        extra_meaningless_keyword_argument=None)
        assert x == '$YYY', x

    def test_subst_kw(self):
        """Test the NoSubstitutionProxy.subst_kw() method"""
        env = self.TestEnvironment(XXX = 'x', YYY = 'y')
        assert env['XXX'] == 'x', env['XXX']
        assert env['YYY'] == 'y', env['YYY']

        proxy = NoSubstitutionProxy(env)
        assert proxy['XXX'] == 'x', proxy['XXX']
        assert proxy['YYY'] == 'y', proxy['YYY']

        x = env.subst_kw({'$XXX':'$YYY'})
        assert x == {'x':'y'}, x
        x = proxy.subst_kw({'$XXX':'$YYY'})
        assert x == {'$XXX':'$YYY'}, x

    def test_subst_list(self):
        """Test the NoSubstitutionProxy.subst_list() method"""
        env = self.TestEnvironment(XXX = 'x', YYY = 'y')
        assert env['XXX'] == 'x', env['XXX']
        assert env['YYY'] == 'y', env['YYY']

        proxy = NoSubstitutionProxy(env)
        assert proxy['XXX'] == 'x', proxy['XXX']
        assert proxy['YYY'] == 'y', proxy['YYY']

        x = env.subst_list('$XXX')
        assert x == [['x']], x
        x = proxy.subst_list('$XXX')
        assert x == [[]], x

        x = proxy.subst_list('$YYY', raw=0, target=None, source=None, conv=None)
        assert x == [[]], x

    def test_subst_target_source(self):
        """Test the NoSubstitutionProxy.subst_target_source() method"""
        env = self.TestEnvironment(XXX = 'x', YYY = 'y')
        assert env['XXX'] == 'x', env['XXX']
        assert env['YYY'] == 'y', env['YYY']

        proxy = NoSubstitutionProxy(env)
        assert proxy['XXX'] == 'x', proxy['XXX']
        assert proxy['YYY'] == 'y', proxy['YYY']

        args = ('$XXX $TARGET $SOURCE $YYY',)
        kw = {'target' : DummyNode('ttt'), 'source' : DummyNode('sss')}
        x = env.subst_target_source(*args, **kw)
        assert x == 'x ttt sss y', x
        x = proxy.subst_target_source(*args, **kw)
        assert x == ' ttt sss ', x

class EnvironmentVariableTestCase(unittest.TestCase):

    def test_is_valid_construction_var(self):
        """Testing is_valid_construction_var()"""
        r = is_valid_construction_var("_a")
        assert r is not None, r
        r = is_valid_construction_var("z_")
        assert r is not None, r
        r = is_valid_construction_var("X_")
        assert r is not None, r
        r = is_valid_construction_var("2a")
        assert r is None, r
        r = is_valid_construction_var("a2_")
        assert r is not None, r
        r = is_valid_construction_var("/")
        assert r is None, r
        r = is_valid_construction_var("_/")
        assert r is None, r
        r = is_valid_construction_var("a/")
        assert r is None, r
        r = is_valid_construction_var(".b")
        assert r is None, r
        r = is_valid_construction_var("_.b")
        assert r is None, r
        r = is_valid_construction_var("b1._")
        assert r is None, r
        r = is_valid_construction_var("-b")
        assert r is None, r
        r = is_valid_construction_var("_-b")
        assert r is None, r
        r = is_valid_construction_var("b1-_")
        assert r is None, r



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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/EnvironmentValues.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import re

_is_valid_var = re.compile(r'[_a-zA-Z]\w*$')

_rm = re.compile(r'\$[()]')
_remove = re.compile(r'\$\([^$]*(\$[^)][^$]*)*\$\)')

# Regular expressions for splitting strings and handling substitutions,
# for use by the scons_subst() and scons_subst_list() functions:
#
# The first expression compiled matches all of the $-introduced tokens
# that we need to process in some way, and is used for substitutions.
# The expressions it matches are:
#
#       "$$"
#       "$("
#       "$)"
#       "$variable"             [must begin with alphabetic or underscore]
#       "${any stuff}"
#
# The second expression compiled is used for splitting strings into tokens
# to be processed, and it matches all of the tokens listed above, plus
# the following that affect how arguments do or don't get joined together:
#
#       "   "                   [white space]
#       "non-white-space"       [without any dollar signs]
#       "$"                     [single dollar sign]
#
_dollar_exps_str = r'\$[\$\(\)]|\$[_a-zA-Z][\.\w]*|\${[^}]*}'
_dollar_exps = re.compile(r'(%s)' % _dollar_exps_str)
_separate_args = re.compile(r'(%s|\s+|[^\s$]+|\$)' % _dollar_exps_str)

# This regular expression is used to replace strings of multiple white
# space characters in the string result from the scons_subst() function.
_space_sep = re.compile(r'[\t ]+(?![^{]*})')

class ValueTypes:
    """
    Enum to store what type of value the variable holds.
    """
    UNKNOWN = 0
    STRING = 1
    CALLABLE = 2
    VARIABLE = 3


class EnvironmentValue:
    """
    Hold a single value. We're going to cache parsed version of the file
    We're going to keep track of variables which feed into this values evaluation
    """
    def __init__(self, value):
        self.value = value
        self.var_type = ValueTypes.UNKNOWN

        if callable(self.value):
            self.var_type = ValueTypes.CALLABLE
        else:
            self.parse_value()


    def parse_value(self):
        """
        Scan the string and break into component values
        """

        try:
            if '$' not in self.value:
                self._parsed = self.value
                self.var_type = ValueTypes.STRING
            else:
                # Now we need to parse the specified string
                result = _dollar_exps.sub(sub_match, args)
                print(result)
        except TypeError:
            # likely callable? either way we don't parse
            self._parsed = self.value

    def parse_trial(self):
        """
        Try alternate parsing methods.
        :return:
        """
        parts = []
        for c in self.value:
            pass


class EnvironmentValues:
    """
    A class to hold all the environment variables
    """
    def __init__(self, **kw):
        self._dict = {}
        for k in kw:
            self._dict[k] = EnvironmentValue(kw[k])


# SCons/EnvironmentValuesTest.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import unittest

from  SCons.EnvironmentValues import EnvironmentValues

class MyTestCase(unittest.TestCase):
    def test_simple_environmentValues(self):
        """Test comparing SubstitutionEnvironments
        """

        env1 = EnvironmentValues(XXX='x')
        env2 = EnvironmentValues(XXX='x',XX="$X", X1="${X}", X2="$($X$)")



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


# SCons/Errors.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""SCons exception classes.

Used to handle internal and user errors in SCons.
"""

import shutil
import SCons.Util

# Note that not all Errors are defined here, some are at the point of use


class BuildError(Exception):
    """SCons Errors that can occur while building.

    A :class:`BuildError` exception contains information both
    about the erorr itself, and what caused the error.

    Attributes:
       node: (*cause*) the error occurred while building this target node(s)
       errstr: (*info*) a description of the error message
       status: (*info*) the return code of the action that caused the build error.
          Must be set to a non-zero value even if the build error is not due
          to an action returning a non-zero returned code.
       exitstatus: (*info*) SCons exit status due to this build error.
          Must be nonzero unless due to an explicit :meth:`Exit` call.
          Not always the same as ``status``, since actions return a status
          code that should be respected, but SCons typically exits with 2
          irrespective of the return value of the failed action.
       filename: (*info*) The name of the file or directory that caused the
          build error. Set to ``None`` if no files are associated with
          this error. This might be different from the target
          being built. For example, failure to create the
          directory in which the target file will appear. It
          can be ``None`` if the error is not due to a particular
          filename.
       executor: (*cause*) the executor that caused the build to fail (might
          be ``None`` if the build failures is not due to the executor failing)
       action: (*cause*) the action that caused the build to fail (might be
          ``None`` if the build failures is not due to the an
          action failure)
       command: (*cause*) the command line for the action that caused the
          build to fail (might be ``None`` if the build failures
          is not due to the an action failure)
       exc_info: (*info*) Info about exception that caused the build
          error. Set to ``(None, None, None)`` if this build
          error is not due to an exception.

    """

    def __init__(self,
                 node=None, errstr="Unknown error", status=2, exitstatus=2,
                 filename=None, executor=None, action=None, command=None,
                 exc_info=(None, None, None)):

        # py3: errstr should be string and not bytes.

        self.errstr = SCons.Util.to_String(errstr)
        self.status = status
        self.exitstatus = exitstatus
        self.filename = filename
        self.exc_info = exc_info

        self.node = node
        self.executor = executor
        self.action = action
        self.command = command

        super().__init__(node, errstr, status, exitstatus, filename,
                         executor, action, command, exc_info)

    def __str__(self):
        if self.filename:
            return self.filename + ': ' + self.errstr
        else:
            return self.errstr

class InternalError(Exception):
    pass

class UserError(Exception):
    pass

class StopError(Exception):
    pass

class SConsEnvironmentError(Exception):
    pass

class MSVCError(IOError):
    pass

class ExplicitExit(Exception):
    def __init__(self, node=None, status=None, *args):
        self.node = node
        self.status = status
        self.exitstatus = status
        super().__init__(*args)

def convert_to_BuildError(status, exc_info=None):
    """Convert a return code to a BuildError Exception.

    The `buildError.status` we set here will normally be
    used as the exit status of the "scons" process.

    Args:
      status: can either be a return code or an Exception.
      exc_info (tuple, optional): explicit exception information.

    """

    if not exc_info and isinstance(status, Exception):
        exc_info = (status.__class__, status, None)


    if isinstance(status, BuildError):
        buildError = status
        buildError.exitstatus = 2   # always exit with 2 on build errors
    elif isinstance(status, ExplicitExit):
        status = status.status
        errstr = 'Explicit exit, status %s' % status
        buildError = BuildError(
            errstr=errstr,
            status=status,      # might be 0, OK here
            exitstatus=status,      # might be 0, OK here
            exc_info=exc_info)
    elif isinstance(status, (StopError, UserError)):
        buildError = BuildError(
            errstr=str(status),
            status=2,
            exitstatus=2,
            exc_info=exc_info)
    elif isinstance(status, shutil.SameFileError):
        # PY3 has a exception for when copying file to itself
        # It's object provides info differently than below
        try:
            filename = status.filename
        except AttributeError:
            filename = None

        buildError = BuildError(
            errstr=status.args[0],
            status=status.errno,
            exitstatus=2,
            filename=filename,
            exc_info=exc_info)

    elif isinstance(status, (SConsEnvironmentError, OSError, IOError)):
        # If an IOError/OSError happens, raise a BuildError.
        # Report the name of the file or directory that caused the
        # error, which might be different from the target being built
        # (for example, failure to create the directory in which the
        # target file will appear).
        filename = getattr(status, 'filename', None)
        strerror = getattr(status, 'strerror', str(status))
        errno = getattr(status, 'errno', 2)

        buildError = BuildError(
            errstr=strerror,
            status=errno,
            exitstatus=2,
            filename=filename,
            exc_info=exc_info)
    elif isinstance(status, Exception):
        buildError = BuildError(
            errstr='%s : %s' % (status.__class__.__name__, status),
            status=2,
            exitstatus=2,
            exc_info=exc_info)
    elif SCons.Util.is_String(status):
        buildError = BuildError(
            errstr=status,
            status=2,
            exitstatus=2)
    else:
        buildError = BuildError(
            errstr="Error %s" % status,
            status=status,
            exitstatus=2)

    #import sys
    #sys.stderr.write("convert_to_BuildError: status %s => (errstr %s, status %s)\n"%(status,buildError.errstr, buildError.status))
    return buildError

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/ErrorsTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import unittest

import SCons.Errors


class ErrorsTestCase(unittest.TestCase):
    def test_BuildError(self):
        """Test the BuildError exception."""
        try:
            raise SCons.Errors.BuildError(
                errstr = "foo", status=57, filename="file", exc_info=(1,2,3),
                node = "n", executor="e", action="a", command="c")
        except SCons.Errors.BuildError as e:
            assert e.errstr == "foo"
            assert e.status == 57
            assert e.exitstatus == 2, e.exitstatus
            assert e.filename == "file"
            assert e.exc_info == (1,2,3)

            assert e.node == "n"
            assert e.executor == "e"
            assert e.action == "a"
            assert e.command == "c"

        try:
            raise SCons.Errors.BuildError("n", "foo", 57, 3, "file", 
                                          "e", "a", "c", (1,2,3))
        except SCons.Errors.BuildError as e:
            assert e.errstr == "foo", e.errstr
            assert e.status == 57, e.status
            assert e.exitstatus == 3, e.exitstatus
            assert e.filename == "file", e.filename
            assert e.exc_info == (1,2,3), e.exc_info

            assert e.node == "n"
            assert e.executor == "e"
            assert e.action == "a"
            assert e.command == "c"

        try:
            raise SCons.Errors.BuildError()
        except SCons.Errors.BuildError as e:
            assert e.errstr == "Unknown error"
            assert e.status == 2
            assert e.exitstatus == 2
            assert e.filename is None
            assert e.exc_info == (None, None, None)

            assert e.node is None
            assert e.executor is None
            assert e.action is None
            assert e.command is None

    def test_InternalError(self):
        """Test the InternalError exception."""
        try:
            raise SCons.Errors.InternalError("test internal error")
        except SCons.Errors.InternalError as e:
            assert e.args == ("test internal error",)

    def test_UserError(self):
        """Test the UserError exception."""
        try:
            raise SCons.Errors.UserError("test user error")
        except SCons.Errors.UserError as e:
            assert e.args == ("test user error",)

    def test_ExplicitExit(self):
        """Test the ExplicitExit exception."""
        try:
            raise SCons.Errors.ExplicitExit("node")
        except SCons.Errors.ExplicitExit as e:
            assert e.node == "node"

    def test_convert_EnvironmentError_to_BuildError(self):
        """Test the convert_to_BuildError function on SConsEnvironmentError
        exceptions.
        """
        ee = SCons.Errors.SConsEnvironmentError("test env error")
        be = SCons.Errors.convert_to_BuildError(ee)
        assert be.errstr == "test env error"
        assert be.status == 2
        assert be.exitstatus == 2
        assert be.filename is None

    def test_convert_OSError_to_BuildError(self):
        """Test the convert_to_BuildError function on OSError
        exceptions.
        """
        ose = OSError(7, 'test oserror')
        be = SCons.Errors.convert_to_BuildError(ose)
        assert be.errstr == 'test oserror'
        assert be.status == 7
        assert be.exitstatus == 2
        assert be.filename is None


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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/Executor.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Execute actions with specific lists of target and source Nodes."""

import collections

import SCons.Errors
import SCons.Memoize
import SCons.Util
from SCons.compat import NoSlotsPyPy
import SCons.Debug
from SCons.Debug import logInstanceCreation

class Batch:
    """Remembers exact association between targets
    and sources of executor."""

    __slots__ = ('targets',
                 'sources')

    def __init__(self, targets=[], sources=[]):
        self.targets = targets
        self.sources = sources



class TSList(collections.UserList):
    """A class that implements $TARGETS or $SOURCES expansions by wrapping
    an executor Method.  This class is used in the Executor.lvars()
    to delay creation of NodeList objects until they're needed.

    Note that we subclass collections.UserList purely so that the
    is_Sequence() function will identify an object of this class as
    a list during variable expansion.  We're not really using any
    collections.UserList methods in practice.
    """
    def __init__(self, func):
        self.func = func
    def __getattr__(self, attr):
        nl = self.func()
        return getattr(nl, attr)
    def __getitem__(self, i):
        nl = self.func()
        return nl[i]
    def __str__(self):
        nl = self.func()
        return str(nl)
    def __repr__(self):
        nl = self.func()
        return repr(nl)

class TSObject:
    """A class that implements $TARGET or $SOURCE expansions by wrapping
    an Executor method.
    """
    def __init__(self, func):
        self.func = func
    def __getattr__(self, attr):
        n = self.func()
        return getattr(n, attr)
    def __str__(self):
        n = self.func()
        if n:
            return str(n)
        return ''
    def __repr__(self):
        n = self.func()
        if n:
            return repr(n)
        return ''

def rfile(node):
    """
    A function to return the results of a Node's rfile() method,
    if it exists, and the Node itself otherwise (if it's a Value
    Node, e.g.).
    """
    try:
        rfile = node.rfile
    except AttributeError:
        return node
    else:
        return rfile()


def execute_nothing(obj, target, kw):
    return 0

def execute_action_list(obj, target, kw):
    """Actually execute the action list."""
    env = obj.get_build_env()
    kw = obj.get_kw(kw)
    status = 0
    for act in obj.get_action_list():
        args = ([], [], env)
        status = act(*args, **kw)
        if isinstance(status, SCons.Errors.BuildError):
            status.executor = obj
            raise status    # TODO pylint E0702: raising int not allowed
        elif status:
            msg = "Error %s" % status
            raise SCons.Errors.BuildError(
                errstr=msg,
                node=obj.batches[0].targets,
                executor=obj,
                action=act)
    return status

_do_execute_map = {0 : execute_nothing,
                   1 : execute_action_list}


def execute_actions_str(obj):
    env = obj.get_build_env()
    return "\n".join([action.genstring(obj.get_all_targets(),
                                       obj.get_all_sources(),
                                       env)
                      for action in obj.get_action_list()])

def execute_null_str(obj):
    return ''

_execute_str_map = {0 : execute_null_str,
                    1 : execute_actions_str}


class Executor(object, metaclass=NoSlotsPyPy):
    """A class for controlling instances of executing an action.

    This largely exists to hold a single association of an action,
    environment, list of environment override dictionaries, targets
    and sources for later processing as needed.
    """

    __slots__ = ('pre_actions',
                 'post_actions',
                 'env',
                 'overridelist',
                 'batches',
                 'builder_kw',
                 '_memo',
                 'lvars',
                 '_changed_sources_list',
                 '_changed_targets_list',
                 '_unchanged_sources_list',
                 '_unchanged_targets_list',
                 'action_list',
                 '_do_execute',
                 '_execute_str')

    def __init__(self, action, env=None, overridelist=[{}],
                 targets=[], sources=[], builder_kw={}):
        if SCons.Debug.track_instances: logInstanceCreation(self, 'Executor.Executor')
        self.set_action_list(action)
        self.pre_actions = []
        self.post_actions = []
        self.env = env
        self.overridelist = overridelist
        if targets or sources:
            self.batches = [Batch(targets[:], sources[:])]
        else:
            self.batches = []
        self.builder_kw = builder_kw
        self._do_execute = 1
        self._execute_str = 1
        self._memo = {}

    def get_lvars(self):
        try:
            return self.lvars
        except AttributeError:
            self.lvars = {
                'CHANGED_SOURCES' : TSList(self._get_changed_sources),
                'CHANGED_TARGETS' : TSList(self._get_changed_targets),
                'SOURCE' : TSObject(self._get_source),
                'SOURCES' : TSList(self._get_sources),
                'TARGET' : TSObject(self._get_target),
                'TARGETS' : TSList(self._get_targets),
                'UNCHANGED_SOURCES' : TSList(self._get_unchanged_sources),
                'UNCHANGED_TARGETS' : TSList(self._get_unchanged_targets),
            }
            return self.lvars

    def _get_changes(self):
        cs = []
        ct = []
        us = []
        ut = []
        for b in self.batches:
            # don't add targets marked always build to unchanged lists
            # add to changed list as they always need to build
            if not b.targets[0].always_build and b.targets[0].is_up_to_date():
                us.extend(list(map(rfile, b.sources)))
                ut.extend(b.targets)
            else:
                cs.extend(list(map(rfile, b.sources)))
                ct.extend(b.targets)
        self._changed_sources_list = SCons.Util.NodeList(cs)
        self._changed_targets_list = SCons.Util.NodeList(ct)
        self._unchanged_sources_list = SCons.Util.NodeList(us)
        self._unchanged_targets_list = SCons.Util.NodeList(ut)

    def _get_changed_sources(self, *args, **kw):
        try:
            return self._changed_sources_list
        except AttributeError:
            self._get_changes()
            return self._changed_sources_list

    def _get_changed_targets(self, *args, **kw):
        try:
            return self._changed_targets_list
        except AttributeError:
            self._get_changes()
            return self._changed_targets_list

    def _get_source(self, *args, **kw):
        return rfile(self.batches[0].sources[0]).get_subst_proxy()

    def _get_sources(self, *args, **kw):
        return SCons.Util.NodeList([rfile(n).get_subst_proxy() for n in self.get_all_sources()])

    def _get_target(self, *args, **kw):
        return self.batches[0].targets[0].get_subst_proxy()

    def _get_targets(self, *args, **kw):
        return SCons.Util.NodeList([n.get_subst_proxy() for n in self.get_all_targets()])

    def _get_unchanged_sources(self, *args, **kw):
        try:
            return self._unchanged_sources_list
        except AttributeError:
            self._get_changes()
            return self._unchanged_sources_list

    def _get_unchanged_targets(self, *args, **kw):
        try:
            return self._unchanged_targets_list
        except AttributeError:
            self._get_changes()
            return self._unchanged_targets_list

    def get_action_targets(self):
        if not self.action_list:
            return []
        targets_string = self.action_list[0].get_targets(self.env, self)
        if targets_string[0] == '$':
            targets_string = targets_string[1:]
        return self.get_lvars()[targets_string]

    def set_action_list(self, action):
        if not SCons.Util.is_List(action):
            if not action:
                raise SCons.Errors.UserError("Executor must have an action.")
            action = [action]
        self.action_list = action

    def get_action_list(self):
        if self.action_list is None:
            return []
        return self.pre_actions + self.action_list + self.post_actions

    def get_all_targets(self):
        """Returns all targets for all batches of this Executor."""
        result = []
        for batch in self.batches:
            result.extend(batch.targets)
        return result

    def get_all_sources(self):
        """Returns all sources for all batches of this Executor."""
        result = []
        for batch in self.batches:
            result.extend(batch.sources)
        return result

    def get_all_children(self):
        """Returns all unique children (dependencies) for all batches
        of this Executor.

        The Taskmaster can recognize when it's already evaluated a
        Node, so we don't have to make this list unique for its intended
        canonical use case, but we expect there to be a lot of redundancy
        (long lists of batched .cc files #including the same .h files
        over and over), so removing the duplicates once up front should
        save the Taskmaster a lot of work.
        """
        result = []
        for target in self.get_all_targets():
            result.extend(target.children())
        return SCons.Util.uniquer_hashables(result)

    def get_all_prerequisites(self):
        """Returns all unique (order-only) prerequisites for all batches
        of this Executor.
        """
        result = []
        for target in self.get_all_targets():
            if target.prerequisites is not None:
                result.extend(target.prerequisites)
        return SCons.Util.uniquer_hashables(result)

    def get_action_side_effects(self):

        """Returns all side effects for all batches of this
        Executor used by the underlying Action.
        """
        result = []
        for target in self.get_action_targets():
            result.extend(target.side_effects)
        return SCons.Util.uniquer_hashables(result)

    @SCons.Memoize.CountMethodCall
    def get_build_env(self):
        """Fetch or create the appropriate build Environment
        for this Executor.
        """
        try:
            return self._memo['get_build_env']
        except KeyError:
            pass

        # Create the build environment instance with appropriate
        # overrides.  These get evaluated against the current
        # environment's construction variables so that users can
        # add to existing values by referencing the variable in
        # the expansion.
        overrides = {}
        for odict in self.overridelist:
            overrides.update(odict)

        import SCons.Defaults
        env = self.env or SCons.Defaults.DefaultEnvironment()
        build_env = env.Override(overrides)

        self._memo['get_build_env'] = build_env

        return build_env

    def get_build_scanner_path(self, scanner):
        """Fetch the scanner path for this executor's targets and sources.
        """
        env = self.get_build_env()
        try:
            cwd = self.batches[0].targets[0].cwd
        except (IndexError, AttributeError):
            cwd = None
        return scanner.path(env, cwd,
                            self.get_all_targets(),
                            self.get_all_sources())

    def get_kw(self, kw={}):
        result = self.builder_kw.copy()
        result.update(kw)
        result['executor'] = self
        return result

    # use extra indirection because with new-style objects (Python 2.2
    # and above) we can't override special methods, and nullify() needs
    # to be able to do this.

    def __call__(self, target, **kw):
        return _do_execute_map[self._do_execute](self, target, kw)

    def cleanup(self):
        self._memo = {}

    def add_sources(self, sources):
        """Add source files to this Executor's list.  This is necessary
        for "multi" Builders that can be called repeatedly to build up
        a source file list for a given target."""
        # TODO(batch):  extend to multiple batches
        assert (len(self.batches) == 1)
        # TODO(batch):  remove duplicates?
        sources = [x for x in sources if x not in self.batches[0].sources]
        self.batches[0].sources.extend(sources)

    def get_sources(self):
        return self.batches[0].sources

    def add_batch(self, targets, sources):
        """Add pair of associated target and source to this Executor's list.
        This is necessary for "batch" Builders that can be called repeatedly
        to build up a list of matching target and source files that will be
        used in order to update multiple target files at once from multiple
        corresponding source files, for tools like MSVC that support it."""
        self.batches.append(Batch(targets, sources))

    def prepare(self):
        """
        Preparatory checks for whether this Executor can go ahead
        and (try to) build its targets.
        """
        for s in self.get_all_sources():
            if s.missing():
                msg = "Source `%s' not found, needed by target `%s'."
                raise SCons.Errors.StopError(msg % (s, self.batches[0].targets[0]))

    def add_pre_action(self, action):
        self.pre_actions.append(action)

    def add_post_action(self, action):
        self.post_actions.append(action)

    # another extra indirection for new-style objects and nullify...

    def __str__(self):
        return _execute_str_map[self._execute_str](self)

    def nullify(self):
        self.cleanup()
        self._do_execute = 0
        self._execute_str = 0

    @SCons.Memoize.CountMethodCall
    def get_contents(self):
        """Fetch the signature contents.  This is the main reason this
        class exists, so we can compute this once and cache it regardless
        of how many target or source Nodes there are.

        Returns bytes
        """
        try:
            return self._memo['get_contents']
        except KeyError:
            pass
        env = self.get_build_env()

        action_list = self.get_action_list()
        all_targets = self.get_all_targets()
        all_sources = self.get_all_sources()

        result = bytearray("",'utf-8').join([action.get_contents(all_targets,
                                                                 all_sources,
                                                                 env)
                                             for action in action_list])

        self._memo['get_contents'] = result
        return result

    def get_timestamp(self):
        """Fetch a time stamp for this Executor.  We don't have one, of
        course (only files do), but this is the interface used by the
        timestamp module.
        """
        return 0

    def scan_targets(self, scanner):
        # TODO(batch):  scan by batches
        self.scan(scanner, self.get_all_targets())

    def scan_sources(self, scanner):
        # TODO(batch):  scan by batches
        if self.batches[0].sources:
            self.scan(scanner, self.get_all_sources())

    def scan(self, scanner, node_list):
        """Scan a list of this Executor's files (targets or sources) for
        implicit dependencies and update all of the targets with them.
        This essentially short-circuits an N*M scan of the sources for
        each individual target, which is a hell of a lot more efficient.
        """
        env = self.get_build_env()
        path = self.get_build_scanner_path
        kw = self.get_kw()

        # TODO(batch):  scan by batches)
        deps = []

        for node in node_list:
            node.disambiguate()
            deps.extend(node.get_implicit_deps(env, scanner, path, kw))

        deps.extend(self.get_implicit_deps())

        for tgt in self.get_all_targets():
            tgt.add_to_implicit(deps)

    def _get_unignored_sources_key(self, node, ignore=()):
        return (node,) + tuple(ignore)

    @SCons.Memoize.CountDictCall(_get_unignored_sources_key)
    def get_unignored_sources(self, node, ignore=()):
        key = (node,) + tuple(ignore)
        try:
            memo_dict = self._memo['get_unignored_sources']
        except KeyError:
            memo_dict = {}
            self._memo['get_unignored_sources'] = memo_dict
        else:
            try:
                return memo_dict[key]
            except KeyError:
                pass

        if node:
            # TODO:  better way to do this (it's a linear search,
            # but it may not be critical path)?
            sourcelist = []
            for b in self.batches:
                if node in b.targets:
                    sourcelist = b.sources
                    break
        else:
            sourcelist = self.get_all_sources()
        if ignore:
            idict = {}
            for i in ignore:
                idict[i] = 1
            sourcelist = [s for s in sourcelist if s not in idict]

        memo_dict[key] = sourcelist

        return sourcelist

    def get_implicit_deps(self):
        """Return the executor's implicit dependencies, i.e. the nodes of
        the commands to be executed."""
        result = []
        build_env = self.get_build_env()
        for act in self.get_action_list():
            deps = act.get_implicit_deps(self.get_all_targets(),
                                         self.get_all_sources(),
                                         build_env)
            result.extend(deps)
        return result



_batch_executors = {}

def GetBatchExecutor(key):
    return _batch_executors[key]

def AddBatchExecutor(key, executor):
    assert key not in _batch_executors
    _batch_executors[key] = executor

nullenv = None


class NullEnvironment(SCons.Util.Null):
    import SCons.CacheDir
    _CacheDir_path = None
    _CacheDir = SCons.CacheDir.CacheDir(None)
    def get_CacheDir(self):
        return self._CacheDir


def get_NullEnvironment():
    """Use singleton pattern for Null Environments."""
    global nullenv

    if nullenv is None:
        nullenv = NullEnvironment()
    return nullenv

class Null(object, metaclass=NoSlotsPyPy):
    """A null Executor, with a null build Environment, that does
    nothing when the rest of the methods call it.

    This might be able to disappear when we refactor things to
    disassociate Builders from Nodes entirely, so we're not
    going to worry about unit tests for this--at least for now.
    """

    __slots__ = ('pre_actions',
                 'post_actions',
                 'env',
                 'overridelist',
                 'batches',
                 'builder_kw',
                 '_memo',
                 'lvars',
                 '_changed_sources_list',
                 '_changed_targets_list',
                 '_unchanged_sources_list',
                 '_unchanged_targets_list',
                 'action_list',
                 '_do_execute',
                 '_execute_str')

    def __init__(self, *args, **kw):
        if SCons.Debug.track_instances:
            logInstanceCreation(self, 'Executor.Null')
        self.batches = [Batch(kw['targets'][:], [])]
    def get_build_env(self):
        return get_NullEnvironment()
    def get_build_scanner_path(self):
        return None
    def cleanup(self):
        pass
    def prepare(self):
        pass
    def get_unignored_sources(self, *args, **kw):
        return tuple(())
    def get_action_targets(self):
        return []
    def get_action_list(self):
        return []
    def get_all_targets(self):
        return self.batches[0].targets
    def get_all_sources(self):
        return self.batches[0].targets[0].sources
    def get_all_children(self):
        return self.batches[0].targets[0].children()
    def get_all_prerequisites(self):
        return []
    def get_action_side_effects(self):
        return []
    def __call__(self, *args, **kw):
        return 0
    def get_contents(self):
        return ''
    def _morph(self):
        """Morph this Null executor to a real Executor object."""
        batches = self.batches
        self.__class__ = Executor
        self.__init__([])
        self.batches = batches

    # The following methods require morphing this Null Executor to a
    # real Executor object.

    def add_pre_action(self, action):
        self._morph()
        self.add_pre_action(action)
    def add_post_action(self, action):
        self._morph()
        self.add_post_action(action)
    def set_action_list(self, action):
        self._morph()
        self.set_action_list(action)

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/ExecutorTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import unittest

import SCons.Executor


class MyEnvironment:
    def __init__(self, **kw):
        self._dict = {}
        self._dict.update(kw)
    def __getitem__(self, key):
        return self._dict[key]
    def Override(self, overrides):
        d = self._dict.copy()
        d.update(overrides)
        return MyEnvironment(**d)
    def _update(self, dict):
        self._dict.update(dict)

class MyAction:
    def __init__(self, actions=['action1', 'action2']):
        self.actions = actions
    def __call__(self, target, source, env, **kw):
        for action in self.actions:
            action(target, source, env, **kw)
    def genstring(self, target, source, env):
        return ' '.join(['GENSTRING'] + list(map(str, self.actions)) + target + source)
    def get_contents(self, target, source, env):
        return b' '.join(
            [SCons.Util.to_bytes(aa) for aa in self.actions] + 
            [SCons.Util.to_bytes(tt) for tt in target] +
            [SCons.Util.to_bytes(ss) for ss in source]
        )
    def get_implicit_deps(self, target, source, env):
        return []

class MyBuilder:
    def __init__(self, env, overrides):
        self.env = env
        self.overrides = overrides
        self.action = MyAction()

class MyNode:
    def __init__(self, name=None, pre=[], post=[]):
        self.name = name
        self.implicit = []
        self.pre_actions = pre
        self.post_actions = post
        self.missing_val = None
        self.always_build = False
        self.up_to_date = False

    def __str__(self):
        return self.name

    def build(self):
        executor = SCons.Executor.Executor(MyAction(self.pre_actions +
                                                    [self.builder.action] +
                                                    self.post_actions),
                                           self.builder.env,
                                           [],
                                           [self],
                                           ['s1', 's2'])
        executor(self)
    def get_env_scanner(self, env, kw):
        return MyScanner('dep-')
    def get_implicit_deps(self, env, scanner, path, kw={}):
        if not scanner:
            scanner = self.get_env_scanner(env, kw)
        return [scanner.prefix + str(self)]
    def add_to_implicit(self, deps):
        self.implicit.extend(deps)
    def missing(self):
        return self.missing_val
    def calc_signature(self, calc):
        return 'cs-'+calc+'-'+self.name
    def disambiguate(self):
        return self

    def is_up_to_date(self):
        return self.up_to_date

class MyScanner:
    def __init__(self, prefix):
        self.prefix = prefix
    def path(self, env, cwd, target, source):
        return ()
    def select(self, node):
        return self

class ExecutorTestCase(unittest.TestCase):

    def test__init__(self):
        """Test creating an Executor"""
        source_list = ['s1', 's2']
        x = SCons.Executor.Executor('a', 'e', ['o'], 't', source_list)
        assert x.action_list == ['a'], x.action_list
        assert x.env == 'e', x.env
        assert x.overridelist == ['o'], x.overridelist
        targets = x.get_all_targets()
        assert targets == ['t'], targets
        source_list.append('s3')
        sources = x.get_all_sources()
        assert sources == ['s1', 's2'], sources
        try:
            x = SCons.Executor.Executor(None, 'e', ['o'], 't', source_list)
        except SCons.Errors.UserError:
            pass
        else:
            raise Exception("Did not catch expected UserError")

    def test__action_list(self):
        """Test the {get,set}_action_list() methods"""
        x = SCons.Executor.Executor('a', 'e', 'o', 't', ['s1', 's2'])

        l = x.get_action_list()
        assert l == ['a'], l

        x.add_pre_action('pre')
        x.add_post_action('post')
        l = x.get_action_list()
        assert l == ['pre', 'a', 'post'], l

        x.set_action_list('b')
        l = x.get_action_list()
        assert l == ['pre', 'b', 'post'], l

        x.set_action_list(['c'])
        l = x.get_action_list()
        assert l == ['pre', 'c', 'post'], l

    def test_get_build_env(self):
        """Test fetching and generating a build environment"""
        x = SCons.Executor.Executor(MyAction(), MyEnvironment(e=1), [],
                                    't', ['s1', 's2'])
        x.env = MyEnvironment(eee=1)
        be = x.get_build_env()
        assert be['eee'] == 1, be

        env = MyEnvironment(X='xxx')
        x = SCons.Executor.Executor(MyAction(),
                                    env,
                                    [{'O':'o2'}],
                                    't',
                                    ['s1', 's2'])
        be = x.get_build_env()
        assert be['O'] == 'o2', be['O']
        assert be['X'] == 'xxx', be['X']

        env = MyEnvironment(Y='yyy')
        overrides = [{'O':'ob3'}, {'O':'oo3'}]
        x = SCons.Executor.Executor(MyAction(), env, overrides, ['t'], ['s'])
        be = x.get_build_env()
        assert be['O'] == 'oo3', be['O']
        assert be['Y'] == 'yyy', be['Y']
        overrides = [{'O':'ob3'}]
        x = SCons.Executor.Executor(MyAction(), env, overrides, ['t'], ['s'])
        be = x.get_build_env()
        assert be['O'] == 'ob3', be['O']
        assert be['Y'] == 'yyy', be['Y']

    def test_get_build_scanner_path(self):
        """Test fetching the path for the specified scanner."""
        t = MyNode('t')
        t.cwd = 'here'
        x = SCons.Executor.Executor(MyAction(),
                                    MyEnvironment(SCANNERVAL='sss'),
                                    [],
                                    [t],
                                    ['s1', 's2'])

        class LocalScanner:
            def path(self, env, dir, target, source):
                target = list(map(str, target))
                source = list(map(str, source))
                return "scanner: %s, %s, %s, %s" % (env['SCANNERVAL'], dir, target, source)
        s = LocalScanner()

        p = x.get_build_scanner_path(s)
        assert p == "scanner: sss, here, ['t'], ['s1', 's2']", p

    def test_get_kw(self):
        """Test the get_kw() method"""
        t = MyNode('t')
        x = SCons.Executor.Executor(MyAction(),
                                    MyEnvironment(),
                                    [],
                                    [t],
                                    ['s1', 's2'],
                                    builder_kw={'X':1, 'Y':2})
        kw = x.get_kw()
        assert kw == {'X':1, 'Y':2, 'executor':x}, kw
        kw = x.get_kw({'Z':3})
        assert kw == {'X':1, 'Y':2, 'Z':3, 'executor':x}, kw
        kw = x.get_kw({'X':4})
        assert kw == {'X':4, 'Y':2, 'executor':x}, kw

    def test__call__(self):
        """Test calling an Executor"""
        result = []
        def pre(target, source, env, result=result, **kw):
            result.append('pre')
        def action1(target, source, env, result=result, **kw):
            result.append('action1')
        def action2(target, source, env, result=result, **kw):
            result.append('action2')
        def post(target, source, env, result=result, **kw):
            result.append('post')

        env = MyEnvironment()
        a = MyAction([action1, action2])
        t = MyNode('t')

        x = SCons.Executor.Executor(a, env, [], [t], ['s1', 's2'])
        x.add_pre_action(pre)
        x.add_post_action(post)
        x(t)
        assert result == ['pre', 'action1', 'action2', 'post'], result
        del result[:]

        def pre_err(target, source, env, result=result, **kw):
            result.append('pre_err')
            return 1

        x = SCons.Executor.Executor(a, env, [], [t], ['s1', 's2'])
        x.add_pre_action(pre_err)
        x.add_post_action(post)
        try:
            x(t)
        except SCons.Errors.BuildError:
            pass
        else:
            raise Exception("Did not catch expected BuildError")
        assert result == ['pre_err'], result
        del result[:]

    def test_cleanup(self):
        """Test cleaning up an Executor"""
        orig_env = MyEnvironment(e=1)
        x = SCons.Executor.Executor('b', orig_env, [{'o':1}],
                                    't', ['s1', 's2'])

        be = x.get_build_env()
        assert be['e'] == 1, be['e']
        
        x.cleanup()

        x.env = MyEnvironment(eee=1)
        be = x.get_build_env()
        assert be['eee'] == 1, be['eee']

        x.cleanup()

        be = x.get_build_env()
        assert be['eee'] == 1, be['eee']

    def test_add_sources(self):
        """Test adding sources to an Executor"""
        x = SCons.Executor.Executor('b', 'e', 'o', 't', ['s1', 's2'])
        sources = x.get_all_sources()
        assert sources == ['s1', 's2'], sources

        x.add_sources(['s1', 's2'])
        sources = x.get_all_sources()
        assert sources == ['s1', 's2'], sources

        x.add_sources(['s3', 's1', 's4'])
        sources = x.get_all_sources()
        assert sources == ['s1', 's2', 's3', 's4'], sources

    def test_get_sources(self):
        """Test getting sources from an Executor"""
        x = SCons.Executor.Executor('b', 'e', 'o', 't', ['s1', 's2'])
        sources = x.get_sources()
        assert sources == ['s1', 's2'], sources

        x.add_sources(['s1', 's2'])
        sources = x.get_sources()
        assert sources == ['s1', 's2'], sources

        x.add_sources(['s3', 's1', 's4'])
        sources = x.get_sources()
        assert sources == ['s1', 's2', 's3', 's4'], sources

    def test_prepare(self):
        """Test the Executor's prepare() method"""
        env = MyEnvironment()
        t1 = MyNode('t1')
        s1 = MyNode('s1')
        s2 = MyNode('s2')
        s3 = MyNode('s3')
        x = SCons.Executor.Executor('b', env, [{}], [t1], [s1, s2, s3])

        s2.missing_val = True

        try:
            r = x.prepare()
        except SCons.Errors.StopError as e:
            assert str(e) == "Source `s2' not found, needed by target `t1'.", e
        else:
            raise AssertionError("did not catch expected StopError: %s" % r)

    def test_add_pre_action(self):
        """Test adding pre-actions to an Executor"""
        x = SCons.Executor.Executor('b', 'e', 'o', 't', ['s1', 's2'])
        x.add_pre_action('a1')
        assert x.pre_actions == ['a1']
        x.add_pre_action('a2')
        assert x.pre_actions == ['a1', 'a2']

    def test_add_post_action(self):
        """Test adding post-actions to an Executor"""
        x = SCons.Executor.Executor('b', 'e', 'o', 't', ['s1', 's2'])
        x.add_post_action('a1')
        assert x.post_actions == ['a1']
        x.add_post_action('a2')
        assert x.post_actions == ['a1', 'a2']

    def test___str__(self):
        """Test the __str__() method"""
        env = MyEnvironment(S='string')

        x = SCons.Executor.Executor(MyAction(), env, [], ['t'], ['s'])
        c = str(x)
        assert c == 'GENSTRING action1 action2 t s', c

        x = SCons.Executor.Executor(MyAction(), env, [], ['t'], ['s'])
        x.add_pre_action(MyAction(['pre']))
        x.add_post_action(MyAction(['post']))
        c = str(x)
        expect = 'GENSTRING pre t s\n' + \
                 'GENSTRING action1 action2 t s\n' + \
                 'GENSTRING post t s'
        assert c == expect, c

    def test_nullify(self):
        """Test the nullify() method"""
        env = MyEnvironment(S='string')

        result = []
        def action1(target, source, env, result=result, **kw):
            result.append('action1')

        env = MyEnvironment()
        a = MyAction([action1])
        x = SCons.Executor.Executor(a, env, [], ['t1', 't2'], ['s1', 's2'])

        x(MyNode('', [], []))
        assert result == ['action1'], result
        s = str(x)
        assert s[:10] == 'GENSTRING ', s

        del result[:]
        x.nullify()

        assert result == [], result
        x(MyNode('', [], []))
        assert result == [], result
        s = str(x)
        assert s == '', s

    def test_get_contents(self):
        """Test fetching the signatures contents"""
        env = MyEnvironment(C='contents')

        x = SCons.Executor.Executor(MyAction(), env, [], ['t'], ['s'])
        c = x.get_contents()
        assert c == b'action1 action2 t s', c

        x = SCons.Executor.Executor(MyAction(actions=['grow']), env, [],
                                    ['t'], ['s'])
        x.add_pre_action(MyAction(['pre']))
        x.add_post_action(MyAction(['post']))
        c = x.get_contents()
        assert c == b'pre t sgrow t spost t s', c

    def test_get_timestamp(self):
        """Test fetching the "timestamp" """
        x = SCons.Executor.Executor('b', 'e', 'o', 't', ['s1', 's2'])
        ts = x.get_timestamp()
        assert ts == 0, ts

    def test_scan_targets(self):
        """Test scanning the targets for implicit dependencies"""
        env = MyEnvironment(S='string')
        t1 = MyNode('t1')
        t2 = MyNode('t2')
        sources = [MyNode('s1'), MyNode('s2')]
        x = SCons.Executor.Executor(MyAction(), env, [{}], [t1, t2], sources)

        deps = x.scan_targets(None)
        assert t1.implicit == ['dep-t1', 'dep-t2'], t1.implicit
        assert t2.implicit == ['dep-t1', 'dep-t2'], t2.implicit

        t1.implicit = []
        t2.implicit = []

        deps = x.scan_targets(MyScanner('scanner-'))
        assert t1.implicit == ['scanner-t1', 'scanner-t2'], t1.implicit
        assert t2.implicit == ['scanner-t1', 'scanner-t2'], t2.implicit

    def test_scan_sources(self):
        """Test scanning the sources for implicit dependencies"""
        env = MyEnvironment(S='string')
        t1 = MyNode('t1')
        t2 = MyNode('t2')
        sources = [MyNode('s1'), MyNode('s2')]
        x = SCons.Executor.Executor(MyAction(), env, [{}], [t1, t2], sources)

        deps = x.scan_sources(None)
        assert t1.implicit == ['dep-s1', 'dep-s2'], t1.implicit
        assert t2.implicit == ['dep-s1', 'dep-s2'], t2.implicit

        t1.implicit = []
        t2.implicit = []

        deps = x.scan_sources(MyScanner('scanner-'))
        assert t1.implicit == ['scanner-s1', 'scanner-s2'], t1.implicit
        assert t2.implicit == ['scanner-s1', 'scanner-s2'], t2.implicit

    def test_get_unignored_sources(self):
        """Test fetching the unignored source list"""
        env = MyEnvironment()
        s1 = MyNode('s1')
        s2 = MyNode('s2')
        s3 = MyNode('s3')
        x = SCons.Executor.Executor('b', env, [{}], [], [s1, s2, s3])

        r = x.get_unignored_sources(None, [])
        assert r == [s1, s2, s3], list(map(str, r))

        r = x.get_unignored_sources(None, [s2])
        assert r == [s1, s3], list(map(str, r))

        r = x.get_unignored_sources(None, [s1, s3])
        assert r == [s2], list(map(str, r))

    def test_changed_sources_for_alwaysBuild(self):
        """
        Ensure if a target is marked always build that the sources are always marked changed sources
        :return:
        """
        env = MyEnvironment()
        s1 = MyNode('s1')
        s2 = MyNode('s2')
        t1 = MyNode('t1')
        t1.up_to_date = True
        t1.always_build = True

        x = SCons.Executor.Executor('b', env, [{}], [t1], [s1, s2])

        changed_sources = x._get_changed_sources()
        assert changed_sources == [s1, s2], "If target marked AlwaysBuild sources should always be marked changed"




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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/Memoize.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Decorator-based memoizer to count caching stats.

A decorator-based implementation to count hits and misses of the computed
values that various methods cache in memory.

Use of this modules assumes that wrapped methods be coded to cache their
values in a consistent way. In particular, it requires that the class uses a
dictionary named "_memo" to store the cached values.

Here is an example of wrapping a method that returns a computed value,
with no input parameters::

    @SCons.Memoize.CountMethodCall
    def foo(self):

        try:                                                    # Memoization
            return self._memo['foo']                            # Memoization
        except KeyError:                                        # Memoization
            pass                                                # Memoization

        result = self.compute_foo_value()

        self._memo['foo'] = result                              # Memoization

        return result

Here is an example of wrapping a method that will return different values
based on one or more input arguments::

    def _bar_key(self, argument):                               # Memoization
        return argument                                         # Memoization

    @SCons.Memoize.CountDictCall(_bar_key)
    def bar(self, argument):

        memo_key = argument                                     # Memoization
        try:                                                    # Memoization
            memo_dict = self._memo['bar']                       # Memoization
        except KeyError:                                        # Memoization
            memo_dict = {}                                      # Memoization
            self._memo['dict'] = memo_dict                      # Memoization
        else:                                                   # Memoization
            try:                                                # Memoization
                return memo_dict[memo_key]                      # Memoization
            except KeyError:                                    # Memoization
                pass                                            # Memoization

        result = self.compute_bar_value(argument)

        memo_dict[memo_key] = result                            # Memoization

        return result

Deciding what to cache is tricky, because different configurations
can have radically different performance tradeoffs, and because the
tradeoffs involved are often so non-obvious.  Consequently, deciding
whether or not to cache a given method will likely be more of an art than
a science, but should still be based on available data from this module.
Here are some VERY GENERAL guidelines about deciding whether or not to
cache return values from a method that's being called a lot:

    --  The first question to ask is, "Can we change the calling code
        so this method isn't called so often?"  Sometimes this can be
        done by changing the algorithm.  Sometimes the *caller* should
        be memoized, not the method you're looking at.

    --  The memoized function should be timed with multiple configurations
        to make sure it doesn't inadvertently slow down some other
        configuration.

    --  When memoizing values based on a dictionary key composed of
        input arguments, you don't need to use all of the arguments
        if some of them don't affect the return values.

"""

# A flag controlling whether or not we actually use memoization.
use_memoizer = None

# Global list of counter objects
CounterList = {}

class Counter:
    """
    Base class for counting memoization hits and misses.

    We expect that the initialization in a matching decorator will
    fill in the correct class name and method name that represents
    the name of the function being counted.
    """
    def __init__(self, cls_name, method_name):
        """
        """
        self.cls_name = cls_name
        self.method_name = method_name
        self.hit = 0
        self.miss = 0
    def key(self):
        return self.cls_name+'.'+self.method_name
    def display(self):
        print("    {:7d} hits {:7d} misses    {}()".format(self.hit, self.miss, self.key()))
    def __eq__(self, other):
        try:
            return self.key() == other.key()
        except AttributeError:
            return True

class CountValue(Counter):
    """
    A counter class for simple, atomic memoized values.

    A CountValue object should be instantiated in a decorator for each of
    the class's methods that memoizes its return value by simply storing
    the return value in its _memo dictionary.
    """
    def count(self, *args, **kw):
        """ Counts whether the memoized value has already been
            set (a hit) or not (a miss).
        """
        obj = args[0]
        if self.method_name in obj._memo:
            self.hit = self.hit + 1
        else:
            self.miss = self.miss + 1

class CountDict(Counter):
    """
    A counter class for memoized values stored in a dictionary, with
    keys based on the method's input arguments.

    A CountDict object is instantiated in a decorator for each of the
    class's methods that memoizes its return value in a dictionary,
    indexed by some key that can be computed from one or more of
    its input arguments.
    """
    def __init__(self, cls_name, method_name, keymaker):
        """
        """
        super().__init__(cls_name, method_name)
        self.keymaker = keymaker
    def count(self, *args, **kw):
        """ Counts whether the computed key value is already present
           in the memoization dictionary (a hit) or not (a miss).
        """
        obj = args[0]
        try:
            memo_dict = obj._memo[self.method_name]
        except KeyError:
            self.miss = self.miss + 1
        else:
            key = self.keymaker(*args, **kw)
            if key in memo_dict:
                self.hit = self.hit + 1
            else:
                self.miss = self.miss + 1

def Dump(title=None):
    """ Dump the hit/miss count for all the counters
        collected so far.
    """
    if title:
        print(title)
    for counter in sorted(CounterList):
        CounterList[counter].display()

def EnableMemoization():
    global use_memoizer
    use_memoizer = 1

def CountMethodCall(fn):
    """ Decorator for counting memoizer hits/misses while retrieving
        a simple value in a class method. It wraps the given method
        fn and uses a CountValue object to keep track of the
        caching statistics.
        Wrapping gets enabled by calling EnableMemoization().
    """
    if use_memoizer:
        def wrapper(self, *args, **kwargs):
            global CounterList
            key = self.__class__.__name__+'.'+fn.__name__
            if key not in CounterList:
                CounterList[key] = CountValue(self.__class__.__name__, fn.__name__)
            CounterList[key].count(self, *args, **kwargs)
            return fn(self, *args, **kwargs)
        wrapper.__name__= fn.__name__
        return wrapper
    else:
        return fn

def CountDictCall(keyfunc):
    """ Decorator for counting memoizer hits/misses while accessing
        dictionary values with a key-generating function. Like
        CountMethodCall above, it wraps the given method
        fn and uses a CountDict object to keep track of the
        caching statistics. The dict-key function keyfunc has to
        get passed in the decorator call and gets stored in the
        CountDict instance.
        Wrapping gets enabled by calling EnableMemoization().
    """
    def decorator(fn):
        if use_memoizer:
            def wrapper(self, *args, **kwargs):
                global CounterList
                key = self.__class__.__name__+'.'+fn.__name__
                if key not in CounterList:
                    CounterList[key] = CountDict(self.__class__.__name__, fn.__name__, keyfunc)
                CounterList[key].count(self, *args, **kwargs)
                return fn(self, *args, **kwargs)
            wrapper.__name__= fn.__name__
            return wrapper
        else:
            return fn
    return decorator

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/MemoizeTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import unittest

import SCons.Memoize

# Enable memoization counting
SCons.Memoize.EnableMemoization()

class FakeObject:
    def __init__(self):
        self._memo = {}

    def _dict_key(self, argument):
        return argument

    @SCons.Memoize.CountDictCall(_dict_key)
    def dict(self, argument):

        memo_key = argument
        try:
            memo_dict = self._memo['dict']
        except KeyError:
            memo_dict = {}
            self._memo['dict'] = memo_dict
        else:
            try:
                return memo_dict[memo_key]
            except KeyError:
                pass

        result = self.compute_dict(argument)

        memo_dict[memo_key] = result

        return result

    @SCons.Memoize.CountMethodCall
    def value(self):

        try:
            return self._memo['value']
        except KeyError:
            pass

        result = self.compute_value()

        self._memo['value'] = result

        return result

    def get_memoizer_counter(self, name):
        return SCons.Memoize.CounterList.get(self.__class__.__name__+'.'+name, None)

class Returner:
    def __init__(self, result):
        self.result = result
        self.calls = 0
    def __call__(self, *args, **kw):
        self.calls = self.calls + 1
        return self.result


class CountDictTestCase(unittest.TestCase):

    def test___call__(self):
        """Calling a Memoized dict method
        """
        obj = FakeObject()

        called = []

        fd1 = Returner(1)
        fd2 = Returner(2)

        obj.compute_dict = fd1

        r = obj.dict(11)
        assert r == 1, r

        obj.compute_dict = fd2

        r = obj.dict(12)
        assert r == 2, r

        r = obj.dict(11)
        assert r == 1, r

        obj.compute_dict = fd1

        r = obj.dict(11)
        assert r == 1, r

        r = obj.dict(12)
        assert r == 2, r

        assert fd1.calls == 1, fd1.calls
        assert fd2.calls == 1, fd2.calls

        c = obj.get_memoizer_counter('dict')

        assert c.hit == 3, c.hit
        assert c.miss == 2, c.miss


class CountValueTestCase(unittest.TestCase):

    def test___call__(self):
        """Calling a Memoized value method
        """
        obj = FakeObject()

        called = []

        fv1 = Returner(1)
        fv2 = Returner(2)

        obj.compute_value = fv1

        r = obj.value()
        assert r == 1, r
        r = obj.value()
        assert r == 1, r

        obj.compute_value = fv2

        r = obj.value()
        assert r == 1, r
        r = obj.value()
        assert r == 1, r

        assert fv1.calls == 1, fv1.calls
        assert fv2.calls == 0, fv2.calls

        c = obj.get_memoizer_counter('value')

        assert c.hit == 3, c.hit
        assert c.miss == 1, c.miss


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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/PathList.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Handle lists of directory paths.

These are the path lists that get set as CPPPATH, LIBPATH,
etc.) with as much caching of data and efficiency as we can, while
still keeping the evaluation delayed so that we Do the Right Thing
(almost) regardless of how the variable is specified.
"""

import os

import SCons.Memoize
import SCons.Node
import SCons.Util

#
# Variables to specify the different types of entries in a PathList object:
#

TYPE_STRING_NO_SUBST = 0        # string with no '$'
TYPE_STRING_SUBST = 1           # string containing '$'
TYPE_OBJECT = 2                 # other object

def node_conv(obj):
    """
    This is the "string conversion" routine that we have our substitutions
    use to return Nodes, not strings.  This relies on the fact that an
    EntryProxy object has a get() method that returns the underlying
    Node that it wraps, which is a bit of architectural dependence
    that we might need to break or modify in the future in response to
    additional requirements.
    """
    try:
        get = obj.get
    except AttributeError:
        if isinstance(obj, SCons.Node.Node) or SCons.Util.is_Sequence( obj ):
            result = obj
        else:
            result = str(obj)
    else:
        result = get()
    return result

class _PathList:
    """
    An actual PathList object.
    """
    def __init__(self, pathlist):
        """
        Initializes a PathList object, canonicalizing the input and
        pre-processing it for quicker substitution later.

        The stored representation of the PathList is a list of tuples
        containing (type, value), where the "type" is one of the TYPE_*
        variables defined above.  We distinguish between:

            strings that contain no '$' and therefore need no
            delayed-evaluation string substitution (we expect that there
            will be many of these and that we therefore get a pretty
            big win from avoiding string substitution)

            strings that contain '$' and therefore need substitution
            (the hard case is things like '${TARGET.dir}/include',
            which require re-evaluation for every target + source)

            other objects (which may be something like an EntryProxy
            that needs a method called to return a Node)

        Pre-identifying the type of each element in the PathList up-front
        and storing the type in the list of tuples is intended to reduce
        the amount of calculation when we actually do the substitution
        over and over for each target.
        """
        if SCons.Util.is_String(pathlist):
            pathlist = pathlist.split(os.pathsep)
        elif not SCons.Util.is_Sequence(pathlist):
            pathlist = [pathlist]

        pl = []
        for p in pathlist:
            try:
                found = '$' in p
            except (AttributeError, TypeError):
                type = TYPE_OBJECT
            else:
                if not found:
                    type = TYPE_STRING_NO_SUBST
                else:
                    type = TYPE_STRING_SUBST
            pl.append((type, p))

        self.pathlist = tuple(pl)

    def __len__(self): return len(self.pathlist)

    def __getitem__(self, i): return self.pathlist[i]

    def subst_path(self, env, target, source):
        """
        Performs construction variable substitution on a pre-digested
        PathList for a specific target and source.
        """
        result = []
        for type, value in self.pathlist:
            if type == TYPE_STRING_SUBST:
                value = env.subst(value, target=target, source=source,
                                  conv=node_conv)
                if SCons.Util.is_Sequence(value):
                    result.extend(SCons.Util.flatten(value))
                elif value:
                    result.append(value)
            elif type == TYPE_OBJECT:
                value = node_conv(value)
                if value:
                    result.append(value)
            elif value:
                result.append(value)
        return tuple(result)


class PathListCache:
    """
    A class to handle caching of PathList lookups.

    This class gets instantiated once and then deleted from the namespace,
    so it's used as a Singleton (although we don't enforce that in the
    usual Pythonic ways).  We could have just made the cache a dictionary
    in the module namespace, but putting it in this class allows us to
    use the same Memoizer pattern that we use elsewhere to count cache
    hits and misses, which is very valuable.

    Lookup keys in the cache are computed by the _PathList_key() method.
    Cache lookup should be quick, so we don't spend cycles canonicalizing
    all forms of the same lookup key.  For example, 'x:y' and ['x',
    'y'] logically represent the same list, but we don't bother to
    split string representations and treat those two equivalently.
    (Note, however, that we do, treat lists and tuples the same.)

    The main type of duplication we're trying to catch will come from
    looking up the same path list from two different clones of the
    same construction environment.  That is, given
    
        env2 = env1.Clone()

    both env1 and env2 will have the same CPPPATH value, and we can
    cheaply avoid re-parsing both values of CPPPATH by using the
    common value from this cache.
    """
    def __init__(self):
        self._memo = {}

    def _PathList_key(self, pathlist):
        """
        Returns the key for memoization of PathLists.

        Note that we want this to be pretty quick, so we don't completely
        canonicalize all forms of the same list.  For example,
        'dir1:$ROOT/dir2' and ['$ROOT/dir1', 'dir'] may logically
        represent the same list if you're executing from $ROOT, but
        we're not going to bother splitting strings into path elements,
        or massaging strings into Nodes, to identify that equivalence.
        We just want to eliminate obvious redundancy from the normal
        case of re-using exactly the same cloned value for a path.
        """
        if SCons.Util.is_Sequence(pathlist):
            pathlist = tuple(SCons.Util.flatten(pathlist))
        return pathlist

    @SCons.Memoize.CountDictCall(_PathList_key)
    def PathList(self, pathlist):
        """
        Returns the cached _PathList object for the specified pathlist,
        creating and caching a new object as necessary.
        """
        pathlist = self._PathList_key(pathlist)
        try:
            memo_dict = self._memo['PathList']
        except KeyError:
            memo_dict = {}
            self._memo['PathList'] = memo_dict
        else:
            try:
                return memo_dict[pathlist]
            except KeyError:
                pass

        result = _PathList(pathlist)

        memo_dict[pathlist] = result

        return result

PathList = PathListCache().PathList


del PathListCache

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/PathListTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import unittest

import SCons.PathList


class subst_pathTestCase(unittest.TestCase):

    def setUp(self):

        class FakeEnvironment:
            def __init__(self, **kw):
                self.kw = kw
            def subst(self, s, target=None, source=None, conv=lambda x: x):
                if s[0] == '$':
                    s = s[1:]
                    if s == 'target':
                        s = target
                    elif s == 'source':
                        s = source
                    else:
                        s = self.kw[s]
                return s

        self.env = FakeEnvironment(AAA = 'aaa', NULL = '')
        from SCons.Environment import Environment
        self.env = Environment(AAA = 'aaa', NULL = '')

    def test_node(self):
        """Test the subst_path() method on a Node
        """

        import SCons.Node

        class A:
            pass

        n = SCons.Node.Node()

        pl = SCons.PathList.PathList((n,))

        result = pl.subst_path(self.env, 'y', 'z')

        assert result == (n,), result

    def test_object(self):
        """Test the subst_path() method on a non-Node object
        """

        class A:
            def __str__(self):
                return '<object A>'

        a = A()

        pl = SCons.PathList.PathList((a,))

        result = pl.subst_path(self.env, 'y', 'z')

        assert result == ('<object A>',), result

    def test_object_get(self):
        """Test the subst_path() method on an object with a get() method
        """

        class B:
            def get(self):
                return 'b'

        b = B()

        pl = SCons.PathList.PathList((b,))

        result = pl.subst_path(self.env, 'y', 'z')

        assert result == ('b',), result

    def test_string(self):
        """Test the subst_path() method on a non-substitution string
        """

        self.env.subst = lambda s, target, source, conv: 'NOT THIS STRING'

        pl = SCons.PathList.PathList(('x',))

        result = pl.subst_path(self.env, 'y', 'z')

        assert result == ('x',), result

    def test_subst(self):
        """Test the subst_path() method on substitution strings
        """

        pl = SCons.PathList.PathList(('$AAA', '$NULL'))

        result = pl.subst_path(self.env, 'y', 'z')

        assert result == ('aaa',), result

    def test_list_of_lists(self):
        """Test the subst_path() method on substitution of nested lists.
        """
        pl = SCons.PathList.PathList((['$AAA', '$AAA'], '$NULL'))
        result = pl.subst_path(self.env, 'y', 'z')
        assert result == ('aaa', 'aaa'), result

    def test_subst_nested(self):
        """Test the subst_path() method on nested substitution of strings.
        """
        self.env.Append(L1 = ['a', 'b'],
                        L2 = ['c', 'd'],
                        L3 = ['$L2'])
        pl = SCons.PathList.PathList(['$L1'])
        result = pl.subst_path(self.env, 'y', 'z')
        assert result == ('a', 'b'), result
        self.env.Append(L1 = ['$L2'])
        pl = SCons.PathList.PathList(['$L1'])
        result = pl.subst_path(self.env, 'y', 'z')
        assert result == ('a', 'b', 'c', 'd'), result
        self.env.Append(L1 = ['$L3'])
        pl = SCons.PathList.PathList(['$L1'])
        result = pl.subst_path(self.env, 'y', 'z')
        assert result == ('a', 'b', 'c', 'd', 'c', 'd'), result

    def test_another_env(self):
        """Test the subst_path does lazy evaluation.
        """
        pl = SCons.PathList.PathList(('$AAA', '$NULL'))
        result = pl.subst_path(self.env, 'y', 'z')
        assert result == ('aaa',), result
        e = self.env.Clone(AAA = 'bbb')
        result = pl.subst_path(e, 'y', 'z')
        assert result == ('bbb',), result

class PathListCacheTestCase(unittest.TestCase):

    def test_no_PathListCache(self):
        """Make sure the PathListCache class is not visible
        """
        try:
            SCons.PathList.PathListCache
        except AttributeError:
            pass
        else:
            self.fail("Found PathListCache unexpectedly\n")


class PathListTestCase(unittest.TestCase):

    def test_PathList(self):
        """Test the PathList() entry point
        """

        x1 = SCons.PathList.PathList(('x',))
        x2 = SCons.PathList.PathList(['x',])

        assert x1 is x2, (x1, x2)

        x3 = SCons.PathList.PathList('x')

        assert x1 is not x3, (x1, x3)


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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/SConf.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Autoconf-like configuration support.

In other words, SConf allows to run tests on the build machine to detect
capabilities of system and do some things based on result: generate config
files, header files for C/C++, update variables in environment.

Tests on the build system can detect if compiler sees header files, if
libraries are installed, if some command line options are supported etc.
"""

import SCons.compat

import atexit
import io
import os
import re
import sys
import traceback

import SCons.Action
import SCons.Builder
import SCons.Errors
import SCons.Taskmaster.Job
import SCons.Node.FS
import SCons.Taskmaster
import SCons.Util
import SCons.Warnings
import SCons.Conftest

from SCons.Debug import Trace
from collections import defaultdict

# Turn off the Conftest error logging
SCons.Conftest.LogInputFiles = 0
SCons.Conftest.LogErrorMessages = 0

# Set
build_type = None
build_types = ['clean', 'help']

def SetBuildType(buildtype):
    global build_type
    build_type = buildtype

# to be set, if we are in dry-run mode
dryrun = 0

AUTO=0  # use SCons dependency scanning for up-to-date checks
FORCE=1 # force all tests to be rebuilt
CACHE=2 # force all tests to be taken from cache (raise an error, if necessary)
cache_mode = AUTO

def _set_conftest_node(node):
    node.attributes.conftest_node = 1

def SetCacheMode(mode):
    """Set the Configure cache mode. mode must be one of "auto", "force",
    or "cache"."""
    global cache_mode
    if mode == "auto":
        cache_mode = AUTO
    elif mode == "force":
        cache_mode = FORCE
    elif mode == "cache":
        cache_mode = CACHE
    else:
        raise ValueError("SCons.SConf.SetCacheMode: Unknown mode " + mode)

progress_display = SCons.Util.display # will be overwritten by SCons.Script
def SetProgressDisplay(display):
    """Set the progress display to use (called from SCons.Script)"""
    global progress_display
    progress_display = display

SConfFS = None

_ac_build_counter = defaultdict(int)
_ac_config_logs = {}  # all config.log files created in this build
_ac_config_hs   = {}  # all config.h files created in this build
sconf_global = None   # current sconf object

def _createConfigH(target, source, env):
    t = open(str(target[0]), "w")
    defname = re.sub('[^A-Za-z0-9_]', '_', str(target[0]).upper())
    t.write("""#ifndef %(DEFNAME)s_SEEN
#define %(DEFNAME)s_SEEN

""" % {'DEFNAME' : defname})
    t.write(source[0].get_contents().decode())
    t.write("""
#endif /* %(DEFNAME)s_SEEN */
""" % {'DEFNAME' : defname})
    t.close()

def _stringConfigH(target, source, env):
    return "scons: Configure: creating " + str(target[0])


def NeedConfigHBuilder():
    if len(_ac_config_hs) == 0:
       return False
    else:
       return True

def CreateConfigHBuilder(env):
    """Called if necessary just before the building targets phase begins."""
    action = SCons.Action.Action(_createConfigH,
                                 _stringConfigH)
    sconfigHBld = SCons.Builder.Builder(action=action)
    env.Append( BUILDERS={'SConfigHBuilder':sconfigHBld} )
    for k, v in _ac_config_hs.items():
        env.SConfigHBuilder(k, env.Value(v))


class SConfWarning(SCons.Warnings.SConsWarning):
    pass
SCons.Warnings.enableWarningClass(SConfWarning)

# some error definitions
class SConfError(SCons.Errors.UserError):
    def __init__(self,msg):
        super().__init__(msg)

class ConfigureDryRunError(SConfError):
    """Raised when a file or directory needs to be updated during a Configure
    process, but the user requested a dry-run"""
    def __init__(self,target):
        if not isinstance(target, SCons.Node.FS.File):
            msg = 'Cannot create configure directory "%s" within a dry-run.' % str(target)
        else:
            msg = 'Cannot update configure test "%s" within a dry-run.' % str(target)
        super().__init__(msg)

class ConfigureCacheError(SConfError):
    """Raised when a use explicitely requested the cache feature, but the test
    is run the first time."""
    def __init__(self,target):
        super().__init__('"%s" is not yet built and cache is forced.' % str(target))


# define actions for building text files
def _createSource(target, source, env):
    fd = open(str(target[0]), "w")
    fd.write(source[0].get_contents().decode())
    fd.close()


def _stringSource( target, source, env ):
    return (str(target[0]) + ' <-\n  |' +
            source[0].get_contents().decode().replace( '\n', "\n  |" ) )

class SConfBuildInfo(SCons.Node.FS.FileBuildInfo):
    """
    Special build info for targets of configure tests. Additional members
    are result (did the builder succeed last time?) and string, which
    contains messages of the original build phase.
    """
    __slots__ = ('result', 'string')

    def __init__(self):
        self.result = None # -> 0/None -> no error, != 0 error
        self.string = None # the stdout / stderr output when building the target

    def set_build_result(self, result, string):
        self.result = result
        self.string = string


class Streamer:
    """
    'Sniffer' for a file-like writable object. Similar to the unix tool tee.
    """
    def __init__(self, orig):
        self.orig = orig
        self.s = io.StringIO()

    def write(self, str):
        if self.orig:
            self.orig.write(str)
        try:
            self.s.write(str)
        except TypeError as e:
            # "unicode argument expected" bug in IOStream (python 2.x)
            self.s.write(str.decode())

    def writelines(self, lines):
        for l in lines:
            self.write(l + '\n')

    def getvalue(self):
        """
        Return everything written to orig since the Streamer was created.
        """
        return self.s.getvalue()

    def flush(self):
        if self.orig:
            self.orig.flush()
        self.s.flush()


class SConfBuildTask(SCons.Taskmaster.AlwaysTask):
    """
    This is almost the same as SCons.Script.BuildTask. Handles SConfErrors
    correctly and knows about the current cache_mode.
    """
    def display(self, message):
        if sconf_global.logstream:
            sconf_global.logstream.write("scons: Configure: " + message + "\n")

    def display_cached_string(self, bi):
        """
        Logs the original builder messages, given the SConfBuildInfo instance
        bi.
        """
        if not isinstance(bi, SConfBuildInfo):
            SCons.Warnings.warn(
                SConfWarning,
                "The stored build information has an unexpected class: %s" % bi.__class__
            )
        else:
            self.display("The original builder output was:\n" +
                         ("  |" + str(bi.string)).replace("\n", "\n  |"))

    def failed(self):
        # check, if the reason was a ConfigureDryRunError or a
        # ConfigureCacheError and if yes, reraise the exception
        exc_type = self.exc_info()[0]
        if issubclass(exc_type, SConfError):
            # TODO pylint E0704: bare raise not inside except
            raise
        elif issubclass(exc_type, SCons.Errors.BuildError):
            # we ignore Build Errors (occurs, when a test doesn't pass)
            # Clear the exception to prevent the contained traceback
            # to build a reference cycle.
            self.exc_clear()
        else:
            self.display('Caught exception while building "%s":\n' %
                         self.targets[0])
            sys.excepthook(*self.exc_info())
        return SCons.Taskmaster.Task.failed(self)

    def collect_node_states(self):
        # returns (is_up_to_date, cached_error, cachable)
        # where is_up_to_date is 1, if the node(s) are up_to_date
        #       cached_error  is 1, if the node(s) are up_to_date, but the
        #                           build will fail
        #       cachable      is 0, if some nodes are not in our cache
        T = 0
        changed = False
        cached_error = False
        cachable = True
        for t in self.targets:
            if T: Trace('%s' % t)
            bi = t.get_stored_info().binfo
            if isinstance(bi, SConfBuildInfo):
                if T: Trace(': SConfBuildInfo')
                if cache_mode == CACHE:
                    t.set_state(SCons.Node.up_to_date)
                    if T: Trace(': set_state(up_to-date)')
                else:
                    if T: Trace(': get_state() %s' % t.get_state())
                    if T: Trace(': changed() %s' % t.changed())
                    if t.get_state() != SCons.Node.up_to_date and t.changed():
                        changed = True
                    if T: Trace(': changed %s' % changed)
                cached_error = cached_error or bi.result
            else:
                if T: Trace(': else')
                # the node hasn't been built in a SConf context or doesn't
                # exist
                cachable = False
                changed = ( t.get_state() != SCons.Node.up_to_date )
                if T: Trace(': changed %s' % changed)
        if T: Trace('\n')
        return (not changed, cached_error, cachable)

    def execute(self):
        if not self.targets[0].has_builder():
            return

        sconf = sconf_global

        is_up_to_date, cached_error, cachable = self.collect_node_states()

        if cache_mode == CACHE and not cachable:
            raise ConfigureCacheError(self.targets[0])
        elif cache_mode == FORCE:
            is_up_to_date = 0

        if cached_error and is_up_to_date:
            self.display("Building \"%s\" failed in a previous run and all "
                         "its sources are up to date." % str(self.targets[0]))
            binfo = self.targets[0].get_stored_info().binfo
            self.display_cached_string(binfo)
            raise SCons.Errors.BuildError # will be 'caught' in self.failed
        elif is_up_to_date:
            self.display("\"%s\" is up to date." % str(self.targets[0]))
            binfo = self.targets[0].get_stored_info().binfo
            self.display_cached_string(binfo)
        elif dryrun:
            raise ConfigureDryRunError(self.targets[0])
        else:
            # note stdout and stderr are the same here
            s = sys.stdout = sys.stderr = Streamer(sys.stdout)
            try:
                env = self.targets[0].get_build_env()
                env['PSTDOUT'] = env['PSTDERR'] = s
                try:
                    sconf.cached = 0
                    self.targets[0].build()
                finally:
                    sys.stdout = sys.stderr = env['PSTDOUT'] = \
                                 env['PSTDERR'] = sconf.logstream
            except KeyboardInterrupt:
                raise
            except SystemExit:
                exc_value = sys.exc_info()[1]
                raise SCons.Errors.ExplicitExit(self.targets[0],exc_value.code)
            except Exception as e:
                for t in self.targets:
                    binfo = SConfBuildInfo()
                    binfo.merge(t.get_binfo())
                    binfo.set_build_result(1, s.getvalue())
                    sconsign_entry = SCons.SConsign.SConsignEntry()
                    sconsign_entry.binfo = binfo
                    #sconsign_entry.ninfo = self.get_ninfo()
                    # We'd like to do this as follows:
                    #    t.store_info(binfo)
                    # However, we need to store it as an SConfBuildInfo
                    # object, and store_info() will turn it into a
                    # regular FileNodeInfo if the target is itself a
                    # regular File.
                    sconsign = t.dir.sconsign()
                    sconsign.set_entry(t.name, sconsign_entry)
                    sconsign.merge()
                raise e
            else:
                for t in self.targets:
                    binfo = SConfBuildInfo()
                    binfo.merge(t.get_binfo())
                    binfo.set_build_result(0, s.getvalue())
                    sconsign_entry = SCons.SConsign.SConsignEntry()
                    sconsign_entry.binfo = binfo
                    #sconsign_entry.ninfo = self.get_ninfo()
                    # We'd like to do this as follows:
                    #    t.store_info(binfo)
                    # However, we need to store it as an SConfBuildInfo
                    # object, and store_info() will turn it into a
                    # regular FileNodeInfo if the target is itself a
                    # regular File.
                    sconsign = t.dir.sconsign()
                    sconsign.set_entry(t.name, sconsign_entry)
                    sconsign.merge()

class SConfBase:
    """This is simply a class to represent a configure context. After
    creating a SConf object, you can call any tests. After finished with your
    tests, be sure to call the Finish() method, which returns the modified
    environment.
    Some words about caching: In most cases, it is not necessary to cache
    Test results explicitly. Instead, we use the scons dependency checking
    mechanism. For example, if one wants to compile a test program
    (SConf.TryLink), the compiler is only called, if the program dependencies
    have changed. However, if the program could not be compiled in a former
    SConf run, we need to explicitly cache this error.
    """

    def __init__(self, env, custom_tests = {}, conf_dir='$CONFIGUREDIR',
                 log_file='$CONFIGURELOG', config_h = None, _depth = 0):
        """Constructor. Pass additional tests in the custom_tests-dictionary,
        e.g. custom_tests={'CheckPrivate':MyPrivateTest}, where MyPrivateTest
        defines a custom test.
        Note also the conf_dir and log_file arguments (you may want to
        build tests in the VariantDir, not in the SourceDir)
        """
        global SConfFS

        # Now create isolated override so setting source_decider doesn't affect parent Environment
        if cache_mode == FORCE:
            self.original_env = env
            self.env = env.Clone()

            # Set up the Decider() to force rebuilds by saying
            # that every source has changed.  Note that we still
            # call the environment's underlying source decider so
            # that the correct .sconsign info will get calculated
            # and keep the build state consistent.
            def force_build(dependency, target, prev_ni,
                            repo_node=None,
                            env_decider=env.decide_source):
                try:
                    env_decider(dependency, target, prev_ni, repo_node)
                except Exception as e:
                    raise e
                return True

            if self.env.decide_source.__code__ is not force_build.__code__:
                self.env.Decider(force_build)

        else:
            self.env = env

        # print("Override env:%s"%env)

        if not SConfFS:
            SConfFS = SCons.Node.FS.default_fs or \
                      SCons.Node.FS.FS(env.fs.pathTop)
        if sconf_global is not None:
            raise SCons.Errors.UserError("""Configure() called while another Configure() exists.
            Please call .Finish() before creating and second Configure() context""")

        if log_file is not None:
            log_file = SConfFS.File(env.subst(log_file))
        self.logfile = log_file
        self.logstream = None
        self.lastTarget = None
        self.depth = _depth
        self.cached = 0 # will be set, if all test results are cached

        # add default tests
        default_tests = {
                 'CheckCC'            : CheckCC,
                 'CheckCXX'           : CheckCXX,
                 'CheckSHCC'          : CheckSHCC,
                 'CheckSHCXX'         : CheckSHCXX,
                 'CheckFunc'          : CheckFunc,
                 'CheckType'          : CheckType,
                 'CheckTypeSize'      : CheckTypeSize,
                 'CheckMember'        : CheckMember,
                 'CheckDeclaration'   : CheckDeclaration,
                 'CheckHeader'        : CheckHeader,
                 'CheckCHeader'       : CheckCHeader,
                 'CheckCXXHeader'     : CheckCXXHeader,
                 'CheckLib'           : CheckLib,
                 'CheckLibWithHeader' : CheckLibWithHeader,
                 'CheckProg'          : CheckProg,
               }
        self.AddTests(default_tests)
        self.AddTests(custom_tests)
        self.confdir = SConfFS.Dir(env.subst(conf_dir))
        if config_h is not None:
            config_h = SConfFS.File(config_h)
        self.config_h = config_h
        self._startup()

    def Finish(self):
        """Call this method after finished with your tests:
                env = sconf.Finish()
        """
        self._shutdown()

        return self.env

    def Define(self, name, value = None, comment = None):
        """
        Define a pre processor symbol name, with the optional given value in the
        current config header.

        If value is None (default), then #define name is written. If value is not
        none, then #define name value is written.

        comment is a string which will be put as a C comment in the header, to explain the meaning of the value
        (appropriate C comments will be added automatically).
        """
        lines = []
        if comment:
            comment_str = "/* %s */" % comment
            lines.append(comment_str)

        if value is not None:
            define_str = "#define %s %s" % (name, value)
        else:
            define_str = "#define %s" % name
        lines.append(define_str)
        lines.append('')

        self.config_h_text = self.config_h_text + '\n'.join(lines)

    def BuildNodes(self, nodes):
        """
        Tries to build the given nodes immediately. Returns 1 on success,
        0 on error.
        """
        if self.logstream is not None:
            # override stdout / stderr to write in log file
            oldStdout = sys.stdout
            sys.stdout = self.logstream
            oldStderr = sys.stderr
            sys.stderr = self.logstream

        # the engine assumes the current path is the SConstruct directory ...
        old_fs_dir = SConfFS.getcwd()
        old_os_dir = os.getcwd()
        SConfFS.chdir(SConfFS.Top, change_os_dir=True)

        # Because we take responsibility here for writing out our
        # own .sconsign info (see SConfBuildTask.execute(), above),
        # we override the store_info() method with a null place-holder
        # so we really control how it gets written.
        for n in nodes:
            _set_conftest_node(n)
            n.store_info = 0
            if not hasattr(n, 'attributes'):
                n.attributes = SCons.Node.Node.Attrs()
            n.attributes.keep_targetinfo = 1

            if True:
                # Some checkers have intermediate files (for example anything that compiles a c file into a program to run
                # Those files need to be set to not release their target info, otherwise taskmaster will throw a
                # Nonetype not callable
                for c in n.children(scan=False):
                    # Keep debug code here.
                    # print("Checking [%s] for builders and then setting keep_targetinfo"%c)
                    _set_conftest_node(c)
                    if  c.has_builder():
                        n.store_info = 0
                        if not hasattr(c, 'attributes'):
                            c.attributes = SCons.Node.Node.Attrs()
                        c.attributes.keep_targetinfo = 1
                    # pass

        ret = 1

        try:
            # ToDo: use user options for calc
            save_max_drift = SConfFS.get_max_drift()
            SConfFS.set_max_drift(0)
            tm = SCons.Taskmaster.Taskmaster(nodes, SConfBuildTask)
            # we don't want to build tests in parallel
            jobs = SCons.Taskmaster.Job.Jobs(1, tm)
            jobs.run()
            for n in nodes:
                state = n.get_state()
                if (state != SCons.Node.executed and
                    state != SCons.Node.up_to_date):
                    # the node could not be built. we return 0 in this case
                    ret = 0
        finally:
            SConfFS.set_max_drift(save_max_drift)
            os.chdir(old_os_dir)
            SConfFS.chdir(old_fs_dir, change_os_dir=False)
            if self.logstream is not None:
                # restore stdout / stderr
                sys.stdout = oldStdout
                sys.stderr = oldStderr
        return ret

    def pspawn_wrapper(self, sh, escape, cmd, args, env):
        """Wrapper function for handling piped spawns.

        This looks to the calling interface (in Action.py) like a "normal"
        spawn, but associates the call with the PSPAWN variable from
        the construction environment and with the streams to which we
        want the output logged.  This gets slid into the construction
        environment as the SPAWN variable so Action.py doesn't have to
        know or care whether it's spawning a piped command or not.
        """
        return self.pspawn(sh, escape, cmd, args, env, self.logstream, self.logstream)

    def TryBuild(self, builder, text=None, extension=""):
        """Low level TryBuild implementation. Normally you don't need to
        call that - you can use TryCompile / TryLink / TryRun instead
        """
        global _ac_build_counter

        # Make sure we have a PSPAWN value, and save the current
        # SPAWN value.
        try:
            self.pspawn = self.env['PSPAWN']
        except KeyError:
            raise SCons.Errors.UserError('Missing PSPAWN construction variable.')
        try:
            save_spawn = self.env['SPAWN']
        except KeyError:
            raise SCons.Errors.UserError('Missing SPAWN construction variable.')

        nodesToBeBuilt = []
        sourcetext = self.env.Value(text)
        _set_conftest_node(sourcetext)
        f = "conftest"

        if text is not None:
            textSig = SCons.Util.hash_signature(sourcetext)
            textSigCounter = str(_ac_build_counter[textSig])
            _ac_build_counter[textSig] += 1

            f = "_".join([f, textSig, textSigCounter])
            textFile = self.confdir.File(f + extension)
            _set_conftest_node(textFile)
            textFileNode = self.env.SConfSourceBuilder(target=textFile,
                                                       source=sourcetext)
            nodesToBeBuilt.extend(textFileNode)

            source = textFile
            target = textFile.File(f + "SConfActionsContentDummyTarget")
            _set_conftest_node(target)
        else:
            source = None
            target = None

        action = builder.builder.action.get_contents(target=target, source=[source], env=self.env)
        actionsig = SCons.Util.hash_signature(action)
        f = "_".join([f, actionsig])

        pref = self.env.subst( builder.builder.prefix )
        suff = self.env.subst( builder.builder.suffix )
        target = self.confdir.File(pref + f + suff)
        _set_conftest_node(target)

        try:
            # Slide our wrapper into the construction environment as
            # the SPAWN function.
            self.env['SPAWN'] = self.pspawn_wrapper

            nodes = builder(target = target, source = source, SCONF_NODE=True)
            if not SCons.Util.is_List(nodes):
                nodes = [nodes]
            nodesToBeBuilt.extend(nodes)
            result = self.BuildNodes(nodesToBeBuilt)

        finally:
            self.env['SPAWN'] = save_spawn

        if result:
            self.lastTarget = nodes[0]
        else:
            self.lastTarget = None

        return result

    def TryAction(self, action, text = None, extension = ""):
        """Tries to execute the given action with optional source file
        contents <text> and optional source file extension <extension>,
        Returns the status (0 : failed, 1 : ok) and the contents of the
        output file.
        """
        builder = SCons.Builder.Builder(action=action)
        self.env.Append( BUILDERS = {'SConfActionBuilder' : builder} )
        ok = self.TryBuild(self.env.SConfActionBuilder, text, extension)
        del self.env['BUILDERS']['SConfActionBuilder']
        if ok:
            outputStr = self.lastTarget.get_text_contents()
            return (1, outputStr)
        return (0, "")

    def TryCompile( self, text, extension):
        """Compiles the program given in text to an env.Object, using extension
        as file extension (e.g. '.c'). Returns 1, if compilation was
        successful, 0 otherwise. The target is saved in self.lastTarget (for
        further processing).
        """
        return self.TryBuild(self.env.Object, text, extension)

    def TryLink( self, text, extension ):
        """Compiles the program given in text to an executable env.Program,
        using extension as file extension (e.g. '.c'). Returns 1, if
        compilation was successful, 0 otherwise. The target is saved in
        self.lastTarget (for further processing).
        """
        return self.TryBuild(self.env.Program, text, extension )

    def TryRun(self, text, extension ):
        """Compiles and runs the program given in text, using extension
        as file extension (e.g. '.c'). Returns (1, outputStr) on success,
        (0, '') otherwise. The target (a file containing the program's stdout)
        is saved in self.lastTarget (for further processing).
        """
        ok = self.TryLink(text, extension)
        if ok:
            prog = self.lastTarget
            pname = prog.get_internal_path()
            output = self.confdir.File(os.path.basename(pname)+'.out')
            node = self.env.Command(output, prog, [ [ pname, ">", "${TARGET}"] ])
            ok = self.BuildNodes(node)
            if ok:
                outputStr = SCons.Util.to_str(output.get_contents())
                return( 1, outputStr)
        return (0, "")

    class TestWrapper:
        """A wrapper around Tests (to ensure sanity)"""
        def __init__(self, test, sconf):
            self.test = test
            self.sconf = sconf
        def __call__(self, *args, **kw):
            if not self.sconf.active:
                raise SCons.Errors.UserError
            context = CheckContext(self.sconf)
            ret = self.test(context, *args, **kw)
            if self.sconf.config_h is not None:
                self.sconf.config_h_text = self.sconf.config_h_text + context.config_h
            context.Result("error: no result")
            return ret

    def AddTest(self, test_name, test_instance):
        """Adds test_class to this SConf instance. It can be called with
        self.test_name(...)"""
        setattr(self, test_name, SConfBase.TestWrapper(test_instance, self))

    def AddTests(self, tests):
        """Adds all the tests given in the tests dictionary to this SConf
        instance
        """
        for name in tests.keys():
            self.AddTest(name, tests[name])

    def _createDir( self, node ):
        dirName = str(node)
        if dryrun:
            if not os.path.isdir( dirName ):
                raise ConfigureDryRunError(dirName)
        else:
            if not os.path.isdir( dirName ):
                os.makedirs( dirName )

    def _startup(self):
        """Private method. Set up logstream, and set the environment
        variables necessary for a piped build
        """
        global _ac_config_logs
        global sconf_global
        global SConfFS

        self.lastEnvFs = self.env.fs
        self.env.fs = SConfFS
        self._createDir(self.confdir)
        self.confdir.up().add_ignore( [self.confdir] )

        if self.logfile is not None and not dryrun:
            # truncate logfile, if SConf.Configure is called for the first time
            # in a build
            if self.logfile in _ac_config_logs:
                log_mode = "a"
            else:
                _ac_config_logs[self.logfile] = None
                log_mode = "w"
            fp = open(str(self.logfile), log_mode)

            def conflog_cleanup(logf):
                logf.close()

            atexit.register(conflog_cleanup, fp)
            self.logstream = SCons.Util.Unbuffered(fp)
            # logfile may stay in a build directory, so we tell
            # the build system not to override it with an eventually
            # existing file with the same name in the source directory
            self.logfile.dir.add_ignore([self.logfile])

            tb = traceback.extract_stack()[-3-self.depth]
            old_fs_dir = SConfFS.getcwd()
            SConfFS.chdir(SConfFS.Top, change_os_dir=False)
            self.logstream.write('file %s,line %d:\n\tConfigure(confdir = %s)\n' %
                                 (tb[0], tb[1], str(self.confdir)) )
            SConfFS.chdir(old_fs_dir)
        else:
            self.logstream = None
        # we use a special builder to create source files from TEXT
        action = SCons.Action.Action(_createSource,
                                     _stringSource)
        sconfSrcBld = SCons.Builder.Builder(action=action)
        self.env.Append( BUILDERS={'SConfSourceBuilder':sconfSrcBld} )
        self.config_h_text = _ac_config_hs.get(self.config_h, "")
        self.active = 1
        # only one SConf instance should be active at a time ...
        sconf_global = self

    def _shutdown(self):
        """Private method. Reset to non-piped spawn"""
        global sconf_global, _ac_config_hs

        if not self.active:
            raise SCons.Errors.UserError("Finish may be called only once!")
        if self.logstream is not None and not dryrun:
            self.logstream.write("\n")
            self.logstream.close()
            self.logstream = None

        # Now reset the decider if we changed it due to --config=force
        # We saved original Environment passed in and cloned it to isolate
        # it from being changed.
        if cache_mode == FORCE:
            self.env.Decider(self.original_env.decide_source)

            # remove the SConfSourceBuilder from the environment
            blds = self.env['BUILDERS']
            del blds['SConfSourceBuilder']
            self.env.Replace( BUILDERS=blds )

        self.active = 0
        sconf_global = None
        if self.config_h is not None:
            _ac_config_hs[self.config_h] = self.config_h_text
        self.env.fs = self.lastEnvFs

class CheckContext:
    """Provides a context for configure tests. Defines how a test writes to the
    screen and log file.

    A typical test is just a callable with an instance of CheckContext as
    first argument:

        def CheckCustom(context, ...):
            context.Message('Checking my weird test ... ')
            ret = myWeirdTestFunction(...)
            context.Result(ret)

    Often, myWeirdTestFunction will be one of
    context.TryCompile/context.TryLink/context.TryRun. The results of
    those are cached, for they are only rebuild, if the dependencies have
    changed.
    """

    def __init__(self, sconf):
        """Constructor. Pass the corresponding SConf instance."""
        self.sconf = sconf
        self.did_show_result = 0

        # for Conftest.py:
        self.vardict = {}
        self.havedict = {}
        self.headerfilename = None
        self.config_h = "" # config_h text will be stored here
        # we don't regenerate the config.h file after each test. That means,
        # that tests won't be able to include the config.h file, and so
        # they can't do an #ifdef HAVE_XXX_H. This shouldn't be a major
        # issue, though. If it turns out, that we need to include config.h
        # in tests, we must ensure, that the dependencies are worked out
        # correctly. Note that we can't use Conftest.py's support for config.h,
        # cause we will need to specify a builder for the config.h file ...

    def Message(self, text):
        """Inform about what we are doing right now, e.g.
        'Checking for SOMETHING ... '
        """
        self.Display(text)
        self.sconf.cached = 1
        self.did_show_result = 0

    def Result(self, res):
        """Inform about the result of the test. If res is not a string, displays
        'yes' or 'no' depending on whether res is evaluated as true or false.
        The result is only displayed when self.did_show_result is not set.
        """
        if isinstance(res, str):
            text = res
        elif res:
            text = "yes"
        else:
            text = "no"

        if self.did_show_result == 0:
            # Didn't show result yet, do it now.
            self.Display(text + "\n")
            self.did_show_result = 1

    def TryBuild(self, *args, **kw):
        return self.sconf.TryBuild(*args, **kw)

    def TryAction(self, *args, **kw):
        return self.sconf.TryAction(*args, **kw)

    def TryCompile(self, *args, **kw):
        return self.sconf.TryCompile(*args, **kw)

    def TryLink(self, *args, **kw):
        return self.sconf.TryLink(*args, **kw)

    def TryRun(self, *args, **kw):
        return self.sconf.TryRun(*args, **kw)

    def __getattr__( self, attr ):
        if attr == 'env':
            return self.sconf.env
        elif attr == 'lastTarget':
            return self.sconf.lastTarget
        else:
            raise AttributeError("CheckContext instance has no attribute '%s'" % attr)

    #### Stuff used by Conftest.py (look there for explanations).

    def BuildProg(self, text, ext):
        self.sconf.cached = 1
        # TODO: should use self.vardict for $CC, $CPPFLAGS, etc.
        return not self.TryBuild(self.env.Program, text, ext)

    def CompileProg(self, text, ext):
        self.sconf.cached = 1
        # TODO: should use self.vardict for $CC, $CPPFLAGS, etc.
        return not self.TryBuild(self.env.Object, text, ext)

    def CompileSharedObject(self, text, ext):
        self.sconf.cached = 1
        # TODO: should use self.vardict for $SHCC, $CPPFLAGS, etc.
        return not self.TryBuild(self.env.SharedObject, text, ext)

    def RunProg(self, text, ext):
        self.sconf.cached = 1
        # TODO: should use self.vardict for $CC, $CPPFLAGS, etc.
        st, out = self.TryRun(text, ext)
        return not st, out

    def AppendLIBS(self, lib_name_list):
        oldLIBS = self.env.get( 'LIBS', [] )
        self.env.Append(LIBS = lib_name_list)
        return oldLIBS

    def PrependLIBS(self, lib_name_list):
        oldLIBS = self.env.get( 'LIBS', [] )
        self.env.Prepend(LIBS = lib_name_list)
        return oldLIBS

    def SetLIBS(self, val):
        oldLIBS = self.env.get( 'LIBS', [] )
        self.env.Replace(LIBS = val)
        return oldLIBS

    def Display(self, msg):
        if self.sconf.cached:
            # We assume that Display is called twice for each test here
            # once for the Checking for ... message and once for the result.
            # The self.sconf.cached flag can only be set between those calls
            msg = "(cached) " + msg
            self.sconf.cached = 0
        progress_display(msg, append_newline=0)
        self.Log("scons: Configure: " + msg + "\n")

    def Log(self, msg):
        if self.sconf.logstream is not None:
            self.sconf.logstream.write(msg)

    #### End of stuff used by Conftest.py.


def SConf(*args, **kw):
    if kw.get(build_type, True):
        kw['_depth'] = kw.get('_depth', 0) + 1
        for bt in build_types:
            try:
                del kw[bt]
            except KeyError:
                pass
        return SConfBase(*args, **kw)
    else:
        return SCons.Util.Null()


def CheckFunc(context, function_name, header = None, language = None):
    res = SCons.Conftest.CheckFunc(context, function_name, header = header, language = language)
    context.did_show_result = 1
    return not res

def CheckType(context, type_name, includes = "", language = None):
    res = SCons.Conftest.CheckType(context, type_name,
                                   header = includes, language = language)
    context.did_show_result = 1
    return not res

def CheckTypeSize(context, type_name, includes = "", language = None, expect = None):
    res = SCons.Conftest.CheckTypeSize(context, type_name,
                                       header = includes, language = language,
                                       expect = expect)
    context.did_show_result = 1
    return res

def CheckDeclaration(context, declaration, includes = "", language = None):
    res = SCons.Conftest.CheckDeclaration(context, declaration,
                                          includes = includes,
                                          language = language)
    context.did_show_result = 1
    return not res

def CheckMember(context, aggregate_member, header = None, language = None):
    '''Returns the status (False : failed, True : ok).'''
    res = SCons.Conftest.CheckMember(context, aggregate_member, header=header, language=language)
    context.did_show_result = 1
    return not res


def createIncludesFromHeaders(headers, leaveLast, include_quotes = '""'):
    # used by CheckHeader and CheckLibWithHeader to produce C - #include
    # statements from the specified header (list)
    if not SCons.Util.is_List(headers):
        headers = [headers]
    l = []
    if leaveLast:
        lastHeader = headers[-1]
        headers = headers[:-1]
    else:
        lastHeader = None
    for s in headers:
        l.append("#include %s%s%s\n"
                 % (include_quotes[0], s, include_quotes[1]))
    return ''.join(l), lastHeader

def CheckHeader(context, header, include_quotes = '<>', language = None):
    """
    A test for a C or C++ header file.
    """
    prog_prefix, hdr_to_check = \
                 createIncludesFromHeaders(header, 1, include_quotes)
    res = SCons.Conftest.CheckHeader(context, hdr_to_check, prog_prefix,
                                     language = language,
                                     include_quotes = include_quotes)
    context.did_show_result = 1
    return not res

def CheckCC(context):
    res = SCons.Conftest.CheckCC(context)
    context.did_show_result = 1
    return not res

def CheckCXX(context):
    res = SCons.Conftest.CheckCXX(context)
    context.did_show_result = 1
    return not res

def CheckSHCC(context):
    res = SCons.Conftest.CheckSHCC(context)
    context.did_show_result = 1
    return not res

def CheckSHCXX(context):
    res = SCons.Conftest.CheckSHCXX(context)
    context.did_show_result = 1
    return not res

# Bram: Make this function obsolete?  CheckHeader() is more generic.

def CheckCHeader(context, header, include_quotes = '""'):
    """
    A test for a C header file.
    """
    return CheckHeader(context, header, include_quotes, language = "C")


# Bram: Make this function obsolete?  CheckHeader() is more generic.

def CheckCXXHeader(context, header, include_quotes = '""'):
    """
    A test for a C++ header file.
    """
    return CheckHeader(context, header, include_quotes, language = "C++")


def CheckLib(context, library = None, symbol = "main",
             header = None, language = None, autoadd = 1):
    """
    A test for a library. See also CheckLibWithHeader.
    Note that library may also be None to test whether the given symbol
    compiles without flags.
    """

    if not library:
        library = [None]

    if not SCons.Util.is_List(library):
        library = [library]

    # ToDo: accept path for the library
    res = SCons.Conftest.CheckLib(context, library, symbol, header = header,
                                        language = language, autoadd = autoadd)
    context.did_show_result = 1
    return not res

# XXX
# Bram: Can only include one header and can't use #ifdef HAVE_HEADER_H.

def CheckLibWithHeader(context, libs, header, language,
                       call = None, autoadd = 1):
    # ToDo: accept path for library. Support system header files.
    """
    Another (more sophisticated) test for a library.
    Checks, if library and header is available for language (may be 'C'
    or 'CXX'). Call maybe be a valid expression _with_ a trailing ';'.
    As in CheckLib, we support library=None, to test if the call compiles
    without extra link flags.
    """
    prog_prefix, dummy = \
                 createIncludesFromHeaders(header, 0)
    if not libs:
        libs = [None]

    if not SCons.Util.is_List(libs):
        libs = [libs]

    res = SCons.Conftest.CheckLib(context, libs, None, prog_prefix,
            call = call, language = language, autoadd = autoadd)
    context.did_show_result = 1
    return not res

def CheckProg(context, prog_name):
    """Simple check if a program exists in the path.  Returns the path
    for the application, or None if not found.
    """
    res = SCons.Conftest.CheckProg(context, prog_name)
    context.did_show_result = 1
    return res

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/SConfTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import io
import os
import re
import sys
from types import ModuleType
import unittest

import TestCmd

sys.stdout = io.StringIO()

if sys.platform == 'win32':
    existing_lib = "msvcrt"
else:
    existing_lib = "m"

class SConfTestCase(unittest.TestCase):

    def setUp(self):
        # we always want to start with a clean directory
        self.save_cwd = os.getcwd()
        self.test = TestCmd.TestCmd(workdir = '')
        os.chdir(self.test.workpath(''))

    def tearDown(self):
        self.test.cleanup()
        import SCons.SConsign
        SCons.SConsign.Reset()
        os.chdir(self.save_cwd)

    def _resetSConfState(self):
        # Ok, this is tricky, and i do not know, if everything is sane.
        # We try to reset scons' state (including all global variables)
        import SCons.SConsign
        SCons.SConsign.write() # simulate normal scons-finish
        for n in list(sys.modules.keys()):
            if n.split('.')[0] == 'SCons' and n[:12] != 'SCons.compat':
                m = sys.modules[n]
                if isinstance(m, ModuleType):
                    # if this is really a scons module, clear its namespace
                    del sys.modules[n]
                    m.__dict__.clear()
        # we only use SCons.Environment and SCons.SConf for these tests.
        import SCons.Environment
        import SCons.SConf
        self.Environment = SCons.Environment
        self.SConf = SCons.SConf
        # and we need a new environment, cause references may point to
        # old modules (well, at least this is safe ...)
        self.scons_env = self.Environment.Environment()
        self.scons_env.AppendENVPath('PATH', os.environ['PATH'])

        # we want to do some autodetection here
        # this stuff works with
        #    - cygwin on Windows (using cmd.exe, not bash)
        #    - posix
        #    - msvc on Windows (hopefully)
        if (not self.scons_env.Detect( self.scons_env.subst('$CXX') ) or
            not self.scons_env.Detect( self.scons_env.subst('$CC') ) or
            not self.scons_env.Detect( self.scons_env.subst('$LINK') )):
            raise Exception("This test needs an installed compiler!")
        if self.scons_env['CXX'] == 'g++':
            global existing_lib
            existing_lib = 'm'

    def _baseTryXXX(self, TryFunc):
        # TryCompile and TryLink are much the same, so we can test them
        # in one method, we pass the function as a string ('TryCompile',
        # 'TryLink'), so we are aware of reloading modules.

        def checks(self, sconf, TryFuncString):
            TryFunc = self.SConf.SConfBase.__dict__[TryFuncString]
            res1 = TryFunc( sconf, "int main(void) { return 0; }\n", ".c" )
            res2 = TryFunc( sconf,
                            '#include "no_std_header.h"\nint main(void) {return 0; }\n',
                            '.c' )
            return (res1,res2)

        # 1. test initial behaviour (check ok / failed)
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            res = checks( self, sconf, TryFunc )
            assert res[0] and not res[1], res
        finally:
            sconf.Finish()

        # 2.1 test the error caching mechanism (no dependencies have changed)
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            res = checks( self, sconf, TryFunc )
            assert res[0] and not res[1], res
        finally:
            sconf.Finish()
        # we should have exactly one one error cached
        log = str(self.test.read( self.test.workpath('config.log') ))
        expr = re.compile( ".*failed in a previous run and all", re.DOTALL )
        firstOcc = expr.match( log )
        assert firstOcc is not None, log
        secondOcc = expr.match( log, firstOcc.end(0) )
        assert secondOcc is None, log

        # 2.2 test the error caching mechanism (dependencies have changed)
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        no_std_header_h = self.test.workpath('config.tests', 'no_std_header.h')
        test_h = self.test.write( no_std_header_h,
                                  "/* we are changing a dependency now */\n" )
        try:
            res = checks( self, sconf, TryFunc )
            log = self.test.read( self.test.workpath('config.log') )
            assert res[0] and res[1], res
        finally:
            sconf.Finish()

    def test_TryBuild(self):
        """Test SConf.TryBuild
        """
        # 1 test that we can try a builder that returns a list of nodes
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        import SCons.Builder
        import SCons.Node

        class MyAction:
            def get_contents(self, target, source, env):
                return 'MyBuilder-MyAction $SOURCE $TARGET'

        class Attrs:
            __slots__ = ('shared', '__dict__')

        class MyBuilder(SCons.Builder.BuilderBase):
            def __init__(self):
                self.prefix = ''
                self.suffix = ''
                # need action because temporary file name uses hash of actions get_contents()
                self.action = MyAction()

            def __call__(self, env, target, source, *args, **kw):
                class MyNode:
                    def __init__(self, name):
                        self.name = name
                        self.state = SCons.Node.no_state
                        self.waiting_parents = set()
                        self.side_effects = []
                        self.builder = None
                        self.prerequisites = None
                        self.attributes = Attrs()
                    def disambiguate(self):
                        return self
                    def has_builder(self):
                        return 1
                    def add_pre_action(self, *actions):
                        pass
                    def add_post_action(self, *actions):
                        pass
                    def children(self, scan = 1):
                        return []
                    def get_state(self):
                        return self.state
                    def set_state(self, state):
                        self.state = state
                    def alter_targets(self):
                        return [], None
                    def depends_on(self, nodes):
                        return None
                    def postprocess(self):
                        pass
                    def clear(self):
                        pass
                    def is_up_to_date(self):
                        return None
                    def prepare(self):
                        pass
                    def push_to_cache(self):
                        pass
                    def retrieve_from_cache(self):
                        return 0
                    def build(self, **kw):
                        return
                    def built(self):
                        pass
                    def get_stored_info(self):
                        pass
                    def get_executor(self):
                        class Executor:
                            def __init__(self, targets):
                                self.targets = targets
                            def get_all_targets(self):
                                return self.targets
                        return Executor([self])
                return [MyNode('n1'), MyNode('n2')]
        try:
            self.scons_env.Append(BUILDERS = {'SConfActionBuilder' : MyBuilder()})
            sconf.TryBuild(self.scons_env.SConfActionBuilder)
        finally:
            sconf.Finish()

    def test_TryCompile(self):
        """Test SConf.TryCompile
        """
        self._baseTryXXX( "TryCompile" ) #self.SConf.SConf.TryCompile )

    def test_TryLink(self):
        """Test SConf.TryLink
        """
        self._baseTryXXX( "TryLink" ) #self.SConf.SConf.TryLink )

    def test_TryRun(self):
        """Test SConf.TryRun
        """
        def checks(sconf):
            prog = """
#include <stdio.h>
int main(void) {
  printf( "Hello" );
  return 0;
}
"""
            res1 = sconf.TryRun( prog, ".c" )
            res2 = sconf.TryRun( "not a c program\n", ".c" )
            return (res1, res2)

        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            res = checks(sconf)
            assert res[0][0] and res[0][1] == "Hello", res
            assert not res[1][0] and res[1][1] == "", res
        finally:
            sconf.Finish()
        log = self.test.read( self.test.workpath('config.log') )

        # test the caching mechanism
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            res = checks(sconf)
            assert res[0][0] and res[0][1] == "Hello", res
            assert not res[1][0] and res[1][1] == "", res
        finally:
            sconf.Finish()
        # we should have exactly one error cached
        # creating string here because it's bytes by default on py3
        log = str(self.test.read( self.test.workpath('config.log') ))
        expr = re.compile( ".*failed in a previous run and all", re.DOTALL )
        firstOcc = expr.match( log )
        assert firstOcc is not None, log
        secondOcc = expr.match( log, firstOcc.end(0) )
        assert secondOcc is None, log


    def test_TryAction(self):
        """Test SConf.TryAction
        """
        def actionOK(target, source, env):
            with open(str(target[0]), "w") as f:
                f.write("RUN OK\n")
            return None
        def actionFAIL(target, source, env):
            return 1


        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                  conf_dir=self.test.workpath('config.tests'),
                                  log_file=self.test.workpath('config.log'))
        try:
            (ret, output) = sconf.TryAction(action=actionOK)
            assert ret and output.encode('utf-8') == bytearray("RUN OK"+os.linesep, 'utf-8'), (ret, output)
            (ret, output) = sconf.TryAction(action=actionFAIL)
            assert not ret and output == "", (ret, output)


        finally:
            sconf.Finish()

    def _test_check_compilers(self, comp, func, name):
        """This is the implementation for CheckCC and CheckCXX tests."""
        from copy import deepcopy

        # Check that Check* works
        r = func()
        assert r, "could not find %s ?" % comp

        # Check that Check* does fail if comp is not available in env
        oldcomp = deepcopy(self.scons_env[comp])
        del self.scons_env[comp]
        r = func()
        assert not r, "%s worked wo comp ?" % name

        # Check that Check* does fail if comp is set but empty
        self.scons_env[comp] = ''
        r = func()
        assert not r, "%s worked with comp = '' ?" % name

        # Check that Check* does fail if comp is set to buggy executable
        self.scons_env[comp] = 'thiscccompilerdoesnotexist'
        r = func()
        assert not r, "%s worked with comp = thiscompilerdoesnotexist ?" % name

        # Check that Check* does fail if CFLAGS is buggy
        self.scons_env[comp] = oldcomp
        self.scons_env['%sFLAGS' % comp] = '/WX qwertyuiop.c'
        r = func()
        assert not r, "%s worked with %sFLAGS = qwertyuiop ?" % (name, comp)

    def test_CheckCC(self):
        """Test SConf.CheckCC()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            try:
                self._test_check_compilers('CC', sconf.CheckCC, 'CheckCC')
            except AssertionError:
                sys.stderr.write(self.test.read('config.log', mode='r'))
                raise
        finally:
            sconf.Finish()

    def test_CheckSHCC(self):
        """Test SConf.CheckSHCC()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            try:
                self._test_check_compilers('SHCC', sconf.CheckSHCC, 'CheckSHCC')
            except AssertionError:
                sys.stderr.write(self.test.read('config.log', mode='r'))
                raise
        finally:
            sconf.Finish()

    def test_CheckCXX(self):
        """Test SConf.CheckCXX()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            try:
                self._test_check_compilers('CXX', sconf.CheckCXX, 'CheckCXX')
            except AssertionError:
                sys.stderr.write(self.test.read('config.log', mode='r'))
                raise
        finally:
            sconf.Finish()

    def test_CheckSHCXX(self):
        """Test SConf.CheckSHCXX()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            try:
                self._test_check_compilers('SHCXX', sconf.CheckSHCXX, 'CheckSHCXX')
            except AssertionError:
                sys.stderr.write(self.test.read('config.log', mode='r'))
                raise
        finally:
            sconf.Finish()


    def test_CheckHeader(self):
        """Test SConf.CheckHeader()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            # CheckHeader()
            r = sconf.CheckHeader( "stdio.h", include_quotes="<>", language="C" )
            assert r, "did not find stdio.h"
            r = sconf.CheckHeader( "HopefullyNoHeader.noh", language="C" )
            assert not r, "unexpectedly found HopefullyNoHeader.noh"
            r = sconf.CheckHeader( "vector", include_quotes="<>", language="C++" )
            assert r, "did not find vector"
            r = sconf.CheckHeader( "HopefullyNoHeader.noh", language="C++" )
            assert not r, "unexpectedly found HopefullyNoHeader.noh"

        finally:
            sconf.Finish()

    def test_CheckCHeader(self):
        """Test SConf.CheckCHeader()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))

        try:
            # CheckCHeader()
            r = sconf.CheckCHeader( "stdio.h", include_quotes="<>" )
            assert r, "did not find stdio.h"
            r = sconf.CheckCHeader( ["math.h", "stdio.h"], include_quotes="<>" )
            assert r, "did not find stdio.h, #include math.h first"
            r = sconf.CheckCHeader( "HopefullyNoCHeader.noh" )
            assert not r, "unexpectedly found HopefullyNoCHeader.noh"

        finally:
            sconf.Finish()

    def test_CheckCXXHeader(self):
        """Test SConf.CheckCXXHeader()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))

        try:
            # CheckCXXHeader()
            r = sconf.CheckCXXHeader( "vector", include_quotes="<>" )
            assert r, "did not find vector"
            r = sconf.CheckCXXHeader( ["stdio.h", "vector"], include_quotes="<>" )
            assert r, "did not find vector, #include stdio.h first"
            r = sconf.CheckCXXHeader( "HopefullyNoCXXHeader.noh" )
            assert not r, "unexpectedly found HopefullyNoCXXHeader.noh"

        finally:
            sconf.Finish()

    def test_CheckLib(self):
        """Test SConf.CheckLib()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))

        try:
            # CheckLib()
            r = sconf.CheckLib( existing_lib, "main", autoadd=0 )
            assert r, "did not find %s" % existing_lib
            r = sconf.CheckLib( "hopefullynolib", "main", autoadd=0 )
            assert not r, "unexpectedly found hopefullynolib"

            # CheckLib() with list of libs
            r = sconf.CheckLib( [existing_lib], "main", autoadd=0 )
            assert r, "did not find %s" % existing_lib
            r = sconf.CheckLib( ["hopefullynolib"], "main", autoadd=0 )
            assert not r, "unexpectedly found hopefullynolib"
            # This is a check that a null list doesn't find functions
            # that are in libraries that must be explicitly named.
            # This works on POSIX systems where you have to -lm to
            # get the math functions, but it fails on Visual Studio
            # where you apparently get all those functions for free.
            # Comment out this check until someone who understands
            # Visual Studio better can come up with a corresponding
            # test (if that ever really becomes necessary).
            #r = sconf.CheckLib( [], "sin", autoadd=0 )
            #assert not r, "unexpectedly found nonexistent library"
            r = sconf.CheckLib( [existing_lib,"hopefullynolib"], "main", autoadd=0 )
            assert r, "did not find %s,%s " % (existing_lib,r)
            r = sconf.CheckLib( ["hopefullynolib",existing_lib], "main", autoadd=0 )
            assert r, "did not find %s " % existing_lib

            # CheckLib() with autoadd
            def libs(env):
                return env.get('LIBS', [])

            env = sconf.env.Clone()

            try:
                r = sconf.CheckLib( existing_lib, "main", autoadd=1 )
                assert r, "did not find main in %s" % existing_lib
                expect = libs(env) + [existing_lib]
                got = libs(sconf.env)
                assert got == expect, "LIBS: expected %s, got %s" % (expect, got)

                sconf.env = env.Clone()
                r = sconf.CheckLib( existing_lib, "main", autoadd=0 )
                assert r, "did not find main in %s" % existing_lib
                expect = libs(env)
                got = libs(sconf.env)
                assert got == expect, "before and after LIBS were not the same"
            finally:
                sconf.env = env
        finally:
            sconf.Finish()

    def test_CheckLibWithHeader(self):
        """Test SConf.CheckLibWithHeader()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))

        try:
            # CheckLibWithHeader()
            r = sconf.CheckLibWithHeader( existing_lib, "math.h", "C", autoadd=0 )
            assert r, "did not find %s" % existing_lib
            r = sconf.CheckLibWithHeader( existing_lib, ["stdio.h", "math.h"], "C", autoadd=0 )
            assert r, "did not find %s, #include stdio.h first" % existing_lib
            r = sconf.CheckLibWithHeader( "hopefullynolib", "math.h", "C", autoadd=0 )
            assert not r, "unexpectedly found hopefullynolib"

            # CheckLibWithHeader() with lists of libs
            r = sconf.CheckLibWithHeader( [existing_lib], "math.h", "C", autoadd=0 )
            assert r, "did not find %s" % existing_lib
            r = sconf.CheckLibWithHeader( [existing_lib], ["stdio.h", "math.h"], "C", autoadd=0 )
            assert r, "did not find %s, #include stdio.h first" % existing_lib
            # This is a check that a null list doesn't find functions
            # that are in libraries that must be explicitly named.
            # This works on POSIX systems where you have to -lm to
            # get the math functions, but it fails on Visual Studio
            # where you apparently get all those functions for free.
            # Comment out this check until someone who understands
            # Visual Studio better can come up with a corresponding
            # test (if that ever really becomes necessary).
            #r = sconf.CheckLibWithHeader( [], "math.h", "C", call="sin(3);", autoadd=0 )
            #assert not r, "unexpectedly found non-existent library"
            r = sconf.CheckLibWithHeader( ["hopefullynolib"], "math.h", "C", autoadd=0 )
            assert not r, "unexpectedly found hopefullynolib"
            r = sconf.CheckLibWithHeader( ["hopefullynolib",existing_lib], ["stdio.h", "math.h"], "C", autoadd=0 )
            assert r, "did not find %s, #include stdio.h first" % existing_lib
            r = sconf.CheckLibWithHeader( [existing_lib,"hopefullynolib"], ["stdio.h", "math.h"], "C", autoadd=0 )
            assert r, "did not find %s, #include stdio.h first" % existing_lib

            # CheckLibWithHeader with autoadd
            def libs(env):
                return env.get('LIBS', [])

            env = sconf.env.Clone()

            try:
                r = sconf.CheckLibWithHeader( existing_lib, "math.h", "C", autoadd=1 )
                assert r, "did not find math.h with %s" % existing_lib
                expect = libs(env) + [existing_lib]
                got = libs(sconf.env)
                assert got == expect, "LIBS: expected %s, got %s" % (expect, got)

                sconf.env = env.Clone()
                r = sconf.CheckLibWithHeader( existing_lib, "math.h", "C", autoadd=0 )
                assert r, "did not find math.h with %s" % existing_lib
                expect = libs(env)
                got = libs(sconf.env)
                assert got == expect, "before and after LIBS were not the same"
            finally:
                sconf.env = env

        finally:
            sconf.Finish()

    def test_CheckFunc(self):
        """Test SConf.CheckFunc()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))

        try:
            # CheckFunc()
            r = sconf.CheckFunc('strcpy')
            assert r, "did not find strcpy"
            r = sconf.CheckFunc('strcpy', '/* header */ char strcpy();')
            assert r, "did not find strcpy"
            r = sconf.CheckFunc('hopefullynofunction')
            assert not r, "unexpectedly found hopefullynofunction"

        finally:
            sconf.Finish()

    def test_CheckProg(self):
        """Test SConf.CheckProg()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))

        try:
            if os.name != 'nt':
                r = sconf.CheckProg('sh')
                assert r, "/bin/sh"
            else:
                r = sconf.CheckProg('cmd.exe')
                self.assertIn('cmd.exe',r)


            r = sconf.CheckProg('hopefully-not-a-program')
            assert r is None

        finally:
            sconf.Finish()


    def test_Define(self):
        """Test SConf.Define()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'),
                                 config_h = self.test.workpath('config.h'))
        try:
            # XXX: we test the generated config.h string. This is not so good,
            # ideally, we would like to test if the generated file included in
            # a test program does what we want.

            # Test defining one symbol wo value
            sconf.config_h_text = ''
            sconf.Define('YOP')
            assert sconf.config_h_text == '#define YOP\n'

            # Test defining one symbol with integer value
            sconf.config_h_text = ''
            sconf.Define('YOP', 1)
            assert sconf.config_h_text == '#define YOP 1\n'

            # Test defining one symbol with string value
            sconf.config_h_text = ''
            sconf.Define('YOP', '"YIP"')
            assert sconf.config_h_text == '#define YOP "YIP"\n'

            # Test defining one symbol with string value
            sconf.config_h_text = ''
            sconf.Define('YOP', "YIP")
            assert sconf.config_h_text == '#define YOP YIP\n'

        finally:
            sconf.Finish()

    def test_CheckTypeSize(self):
        """Test SConf.CheckTypeSize()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            # CheckTypeSize()

            # In ANSI C, sizeof(char) == 1.
            r = sconf.CheckTypeSize('char', expect = 1)
            assert r == 1, "sizeof(char) != 1 ??"
            r = sconf.CheckTypeSize('char', expect = 0)
            assert r == 0, "sizeof(char) == 0 ??"
            r = sconf.CheckTypeSize('char', expect = 2)
            assert r == 0, "sizeof(char) == 2 ??"
            r = sconf.CheckTypeSize('char')
            assert r == 1, "sizeof(char) != 1 ??"
            r = sconf.CheckTypeSize('const unsigned char')
            assert r == 1, "sizeof(const unsigned char) != 1 ??"

            # Checking C++
            r = sconf.CheckTypeSize('const unsigned char', language = 'C++')
            assert r == 1, "sizeof(const unsigned char) != 1 ??"

            # Checking Non-existing type
            r = sconf.CheckTypeSize('thistypedefhasnotchancetosexist_scons')
            assert r == 0, \
                   "Checking size of thistypedefhasnotchancetosexist_scons succeeded ?"

        finally:
            sconf.Finish()

    def test_CheckDeclaration(self):
        """Test SConf.CheckDeclaration()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            # In ANSI C, malloc should be available in stdlib
            r = sconf.CheckDeclaration('malloc', includes = "#include <stdlib.h>")
            assert r, "malloc not declared ??"
            # For C++, __cplusplus should be declared
            r = sconf.CheckDeclaration('__cplusplus', language = 'C++')
            assert r, "__cplusplus not declared in C++ ??"
            r = sconf.CheckDeclaration('__cplusplus', language = 'C')
            assert not r, "__cplusplus declared  in C ??"
            r = sconf.CheckDeclaration('unknown', language = 'Unknown')
            assert not r, "unknown language was supported ??"
        finally:
            sconf.Finish()

    def test_CheckMember(self):
        """Test SConf.CheckMember()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))

        try:
            # CheckMember()
            r = sconf.CheckMember('struct timespec.tv_sec', '#include <time.h>')
            assert r, "did not find timespec.tv_sec"
            r = sconf.CheckMember('struct timespec.tv_nano', '#include <time.h>')
            assert not r, "unexpectedly found struct timespec.tv_nano"
            r = sconf.CheckMember('hopefullynomember')
            assert not r, "unexpectedly found hopefullynomember :%s" % r

        finally:
            sconf.Finish()


    def test_(self):
        """Test SConf.CheckType()
        """
        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            # CheckType()
            r = sconf.CheckType('off_t', '#include <sys/types.h>\n')
            assert r, "did not find off_t"
            r = sconf.CheckType('hopefullynotypedef_not')
            assert not r, "unexpectedly found hopefullynotypedef_not"

        finally:
            sconf.Finish()

    def test_CustomChecks(self):
        """Test Custom Checks
        """
        def CheckCustom(test):
            test.Message( "Checking UserTest ... " )
            prog = """
#include <stdio.h>

int main(void) {
  printf( "Hello" );
  return 0;
}
"""
            (ret, output) = test.TryRun( prog, ".c" )
            test.Result( ret )
            assert ret and output == "Hello", (ret, output)
            return ret


        self._resetSConfState()
        sconf = self.SConf.SConf(self.scons_env,
                                 custom_tests={'CheckCustom': CheckCustom},
                                 conf_dir=self.test.workpath('config.tests'),
                                 log_file=self.test.workpath('config.log'))
        try:
            ret = sconf.CheckCustom()
            assert ret, ret
        finally:
            sconf.Finish()


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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/SConsign.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Operations on signature database files (.sconsign). """

import SCons.compat

import os
import pickle
import time

import SCons.dblite
import SCons.Warnings
from SCons.compat import PICKLE_PROTOCOL
from SCons.Util import print_time


def corrupt_dblite_warning(filename):
    SCons.Warnings.warn(
        SCons.Warnings.CorruptSConsignWarning,
        "Ignoring corrupt .sconsign file: %s" % filename,
    )

SCons.dblite.IGNORE_CORRUPT_DBFILES = True
SCons.dblite.corruption_warning = corrupt_dblite_warning

# XXX Get rid of the global array so this becomes re-entrant.
sig_files = []

# Info for the database SConsign implementation (now the default):
# "DataBase" is a dictionary that maps top-level SConstruct directories
# to open database handles.
# "DB_Module" is the Python database module to create the handles.
# "DB_Name" is the base name of the database file (minus any
# extension the underlying DB module will add).
DataBase = {}
DB_Module = SCons.dblite
DB_Name = None
DB_sync_list = []

def current_sconsign_filename():
    hash_format = SCons.Util.get_hash_format()
    current_hash_algorithm = SCons.Util.get_current_hash_algorithm_used()
    # if the user left the options defaulted AND the default algorithm set by
    # SCons is md5, then set the database name to be the special default name
    #
    # otherwise, if it defaults to something like 'sha1' or the user explicitly
    # set 'md5' as the hash format, set the database name to .sconsign_<algorithm>
    # eg .sconsign_sha1, etc.
    if hash_format is None and current_hash_algorithm == 'md5':
        return ".sconsign"
    else:
        return ".sconsign_" + current_hash_algorithm

def Get_DataBase(dir):
    global DataBase, DB_Module, DB_Name

    if DB_Name is None:
        DB_Name = current_sconsign_filename()

    top = dir.fs.Top
    if not os.path.isabs(DB_Name) and top.repositories:
        mode = "c"
        for d in [top] + top.repositories:
            if dir.is_under(d):
                try:
                    return DataBase[d], mode
                except KeyError:
                    path = d.entry_abspath(DB_Name)
                    try: db = DataBase[d] = DB_Module.open(path, mode)
                    except (IOError, OSError):
                        pass
                    else:
                        if mode != "r":
                            DB_sync_list.append(db)
                        return db, mode
            mode = "r"
    try:
        return DataBase[top], "c"
    except KeyError:
        db = DataBase[top] = DB_Module.open(DB_Name, "c")
        DB_sync_list.append(db)
        return db, "c"
    except TypeError:
        print("DataBase =", DataBase)
        raise


def Reset():
    """Reset global state.  Used by unit tests that end up using
    SConsign multiple times to get a clean slate for each test."""
    global sig_files, DB_sync_list
    sig_files = []
    DB_sync_list = []

normcase = os.path.normcase


def write():
    global sig_files

    if print_time():
        start_time = time.perf_counter()

    for sig_file in sig_files:
        sig_file.write(sync=0)
    for db in DB_sync_list:
        try:
            syncmethod = db.sync
        except AttributeError:
            pass # Not all dbm modules have sync() methods.
        else:
            syncmethod()
        try:
            closemethod = db.close
        except AttributeError:
            pass # Not all dbm modules have close() methods.
        else:
            closemethod()

    if print_time():
        elapsed = time.perf_counter() - start_time
        print('Total SConsign sync time: %f seconds' % elapsed)


class SConsignEntry:
    """
    Wrapper class for the generic entry in a .sconsign file.
    The Node subclass populates it with attributes as it pleases.

    XXX As coded below, we do expect a '.binfo' attribute to be added,
    but we'll probably generalize this in the next refactorings.
    """
    __slots__ = ("binfo", "ninfo", "__weakref__")
    current_version_id = 2

    def __init__(self):
        # Create an object attribute from the class attribute so it ends up
        # in the pickled data in the .sconsign file.
        #_version_id = self.current_version_id
        pass

    def convert_to_sconsign(self):
        self.binfo.convert_to_sconsign()

    def convert_from_sconsign(self, dir, name):
        self.binfo.convert_from_sconsign(dir, name)

    def __getstate__(self):
        state = getattr(self, '__dict__', {}).copy()
        for obj in type(self).mro():
            for name in getattr(obj, '__slots__', ()):
                if hasattr(self, name):
                    state[name] = getattr(self, name)

        state['_version_id'] = self.current_version_id
        try:
            del state['__weakref__']
        except KeyError:
            pass
        return state

    def __setstate__(self, state):
        for key, value in state.items():
            if key not in ('_version_id', '__weakref__'):
                setattr(self, key, value)


class Base:
    """
    This is the controlling class for the signatures for the collection of
    entries associated with a specific directory.  The actual directory
    association will be maintained by a subclass that is specific to
    the underlying storage method.  This class provides a common set of
    methods for fetching and storing the individual bits of information
    that make up signature entry.
    """
    def __init__(self):
        self.entries = {}
        self.dirty = False
        self.to_be_merged = {}

    def get_entry(self, filename):
        """
        Fetch the specified entry attribute.
        """
        return self.entries[filename]

    def set_entry(self, filename, obj):
        """
        Set the entry.
        """
        self.entries[filename] = obj
        self.dirty = True

    def do_not_set_entry(self, filename, obj):
        pass

    def store_info(self, filename, node):
        entry = node.get_stored_info()
        entry.binfo.merge(node.get_binfo())
        self.to_be_merged[filename] = node
        self.dirty = True

    def do_not_store_info(self, filename, node):
        pass

    def merge(self):
        for key, node in self.to_be_merged.items():
            entry = node.get_stored_info()
            try:
                ninfo = entry.ninfo
            except AttributeError:
                # This happens with SConf Nodes, because the configuration
                # subsystem takes direct control over how the build decision
                # is made and its information stored.
                pass
            else:
                ninfo.merge(node.get_ninfo())
            self.entries[key] = entry
        self.to_be_merged = {}


class DB(Base):
    """
    A Base subclass that reads and writes signature information
    from a global .sconsign.db* file--the actual file suffix is
    determined by the database module.
    """
    def __init__(self, dir):
        super().__init__()

        self.dir = dir

        db, mode = Get_DataBase(dir)

        # Read using the path relative to the top of the Repository
        # (self.dir.tpath) from which we're fetching the signature
        # information.
        path = normcase(dir.get_tpath())
        try:
            rawentries = db[path]
        except KeyError:
            pass
        else:
            try:
                self.entries = pickle.loads(rawentries)
                if not isinstance(self.entries, dict):
                    self.entries = {}
                    raise TypeError
            except KeyboardInterrupt:
                raise
            except Exception as e:
                SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
                                    "Ignoring corrupt sconsign entry : %s (%s)\n"%(self.dir.get_tpath(), e))
            for key, entry in self.entries.items():
                entry.convert_from_sconsign(dir, key)

        if mode == "r":
            # This directory is actually under a repository, which means
            # likely they're reaching in directly for a dependency on
            # a file there.  Don't actually set any entry info, so we
            # won't try to write to that .sconsign.dblite file.
            self.set_entry = self.do_not_set_entry
            self.store_info = self.do_not_store_info

        global sig_files
        sig_files.append(self)

    def write(self, sync=1):
        if not self.dirty:
            return

        self.merge()

        db, mode = Get_DataBase(self.dir)

        # Write using the path relative to the top of the SConstruct
        # directory (self.dir.path), not relative to the top of
        # the Repository; we only write to our own .sconsign file,
        # not to .sconsign files in Repositories.
        path = normcase(self.dir.get_internal_path())
        for key, entry in self.entries.items():
            entry.convert_to_sconsign()
        db[path] = pickle.dumps(self.entries, PICKLE_PROTOCOL)

        if sync:
            try:
                syncmethod = db.sync
            except AttributeError:
                # Not all anydbm modules have sync() methods.
                pass
            else:
                syncmethod()


class Dir(Base):
    def __init__(self, fp=None, dir=None):
        """
        fp - file pointer to read entries from
        """
        super().__init__()

        if not fp:
            return

        self.entries = pickle.load(fp)
        if not isinstance(self.entries, dict):
            self.entries = {}
            raise TypeError

        if dir:
            for key, entry in self.entries.items():
                entry.convert_from_sconsign(dir, key)


class DirFile(Dir):
    """
    Encapsulates reading and writing a per-directory .sconsign file.
    """
    def __init__(self, dir):
        """
        dir - the directory for the file
        """

        self.dir = dir
        self.sconsign = os.path.join(dir.get_internal_path(), current_sconsign_filename())

        try:
            fp = open(self.sconsign, 'rb')
        except IOError:
            fp = None

        try:
            super().__init__(fp, dir)
        except KeyboardInterrupt:
            raise
        except Exception:
            SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
                                "Ignoring corrupt .sconsign file: %s"%self.sconsign)

        try:
            fp.close()
        except AttributeError:
            pass

        global sig_files
        sig_files.append(self)

    def write(self, sync=1):
        """
        Write the .sconsign file to disk.

        Try to write to a temporary file first, and rename it if we
        succeed.  If we can't write to the temporary file, it's
        probably because the directory isn't writable (and if so,
        how did we build anything in this directory, anyway?), so
        try to write directly to the .sconsign file as a backup.
        If we can't rename, try to copy the temporary contents back
        to the .sconsign file.  Either way, always try to remove
        the temporary file at the end.
        """
        if not self.dirty:
            return

        self.merge()

        temp = os.path.join(self.dir.get_internal_path(), '.scons%d' % os.getpid())
        try:
            file = open(temp, 'wb')
            fname = temp
        except IOError:
            try:
                file = open(self.sconsign, 'wb')
                fname = self.sconsign
            except IOError:
                return
        for key, entry in self.entries.items():
            entry.convert_to_sconsign()
        pickle.dump(self.entries, file, PICKLE_PROTOCOL)
        file.close()
        if fname != self.sconsign:
            try:
                mode = os.stat(self.sconsign)[0]
                os.chmod(self.sconsign, 0o666)
                os.unlink(self.sconsign)
            except (IOError, OSError):
                # Try to carry on in the face of either OSError
                # (things like permission issues) or IOError (disk
                # or network issues).  If there's a really dangerous
                # issue, it should get re-raised by the calls below.
                pass
            try:
                os.rename(fname, self.sconsign)
            except OSError:
                # An OSError failure to rename may indicate something
                # like the directory has no write permission, but
                # the .sconsign file itself might still be writable,
                # so try writing on top of it directly.  An IOError
                # here, or in any of the following calls, would get
                # raised, indicating something like a potentially
                # serious disk or network issue.
                with open(self.sconsign, 'wb') as f, open(fname, 'rb') as f2:
                    f.write(f2.read())
                os.chmod(self.sconsign, mode)
        try:
            os.unlink(temp)
        except (IOError, OSError):
            pass

ForDirectory = DB


def File(name, dbm_module=None):
    """
    Arrange for all signatures to be stored in a global .sconsign.db*
    file.
    """
    global ForDirectory, DB_Name, DB_Module
    if name is None:
        ForDirectory = DirFile
        DB_Module = None
    else:
        ForDirectory = DB
        DB_Name = name
        if dbm_module is not None:
            DB_Module = dbm_module

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/SConsignTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import os
import unittest

import TestCmd

import SCons.dblite
import SCons.SConsign
from SCons.Util import get_hash_format, get_current_hash_algorithm_used

class BuildInfo:
    def merge(self, object):
        pass

class DummySConsignEntry:
    def __init__(self, name):
        self.name = name
        self.binfo = BuildInfo()
    def convert_to_sconsign(self):
        self.c_to_s = 1
    def convert_from_sconsign(self, dir, name):
        self.c_from_s = 1

class FS:
    def __init__(self, top):
        self.Top = top
        self.Top.repositories = []

class DummyNode:
    def __init__(self, path='not_a_valid_path', binfo=None):
        self.path = path
        self.tpath = path
        self.fs = FS(self)
        self.binfo = binfo
    def get_stored_info(self):
        return self.binfo
    def get_binfo(self):
        return self.binfo
    def get_internal_path(self):
        return self.path
    def get_tpath(self):
        return self.tpath

class SConsignTestCase(unittest.TestCase):
    def setUp(self):
        self.save_cwd = os.getcwd()
        self.test = TestCmd.TestCmd(workdir = '')
        os.chdir(self.test.workpath(''))
    def tearDown(self):
        self.test.cleanup()
        SCons.SConsign.Reset()
        os.chdir(self.save_cwd)

class BaseTestCase(SConsignTestCase):

    def test_Base(self):
        aaa = DummySConsignEntry('aaa')
        bbb = DummySConsignEntry('bbb')
        bbb.arg1 = 'bbb arg1'
        ccc = DummySConsignEntry('ccc')
        ccc.arg2 = 'ccc arg2'

        f = SCons.SConsign.Base()
        f.set_entry('aaa', aaa)
        f.set_entry('bbb', bbb)

        #f.merge()

        e = f.get_entry('aaa')
        assert e == aaa, e
        assert e.name == 'aaa', e.name

        e = f.get_entry('bbb')
        assert e == bbb, e
        assert e.name == 'bbb', e.name
        assert e.arg1 == 'bbb arg1', e.arg1
        assert not hasattr(e, 'arg2'), e

        f.set_entry('bbb', ccc)

        e = f.get_entry('bbb')
        assert e.name == 'ccc', e.name
        assert not hasattr(e, 'arg1'), e
        assert e.arg2 == 'ccc arg2', e.arg1

        ddd = DummySConsignEntry('ddd')
        eee = DummySConsignEntry('eee')
        fff = DummySConsignEntry('fff')
        fff.arg = 'fff arg'

        f = SCons.SConsign.Base()
        f.set_entry('ddd', ddd)
        f.set_entry('eee', eee)

        e = f.get_entry('ddd')
        assert e == ddd, e
        assert e.name == 'ddd', e.name

        e = f.get_entry('eee')
        assert e == eee, e
        assert e.name == 'eee', e.name
        assert not hasattr(e, 'arg'), e

        f.set_entry('eee', fff)

        e = f.get_entry('eee')
        assert e.name == 'fff', e.name
        assert e.arg == 'fff arg', e.arg

    def test_store_info(self):
        aaa = DummySConsignEntry('aaa')
        bbb = DummySConsignEntry('bbb')
        bbb.arg1 = 'bbb arg1'
        ccc = DummySConsignEntry('ccc')
        ccc.arg2 = 'ccc arg2'

        f = SCons.SConsign.Base()
        f.store_info('aaa', DummyNode('aaa', aaa))
        f.store_info('bbb', DummyNode('bbb', bbb))

        try:
            e = f.get_entry('aaa')
        except KeyError:
            pass
        else:
            raise Exception("unexpected entry %s" % e)

        try:
            e = f.get_entry('bbb')
        except KeyError:
            pass
        else:
            raise Exception("unexpected entry %s" % e)

        f.merge()

        e = f.get_entry('aaa')
        assert e == aaa, "aaa = %s, e = %s" % (aaa, e)
        assert e.name == 'aaa', e.name

        e = f.get_entry('bbb')
        assert e == bbb, "bbb = %s, e = %s" % (bbb, e)
        assert e.name == 'bbb', e.name
        assert e.arg1 == 'bbb arg1', e.arg1
        assert not hasattr(e, 'arg2'), e

        f.store_info('bbb', DummyNode('bbb', ccc))

        e = f.get_entry('bbb')
        assert e == bbb, e
        assert e.name == 'bbb', e.name
        assert e.arg1 == 'bbb arg1', e.arg1
        assert not hasattr(e, 'arg2'), e

        f.merge()

        e = f.get_entry('bbb')
        assert e.name == 'ccc', e.name
        assert not hasattr(e, 'arg1'), e
        assert e.arg2 == 'ccc arg2', e.arg1

        ddd = DummySConsignEntry('ddd')
        eee = DummySConsignEntry('eee')
        fff = DummySConsignEntry('fff')
        fff.arg = 'fff arg'

        f = SCons.SConsign.Base()
        f.store_info('ddd', DummyNode('ddd', ddd))
        f.store_info('eee', DummyNode('eee', eee))

        f.merge()

        e = f.get_entry('ddd')
        assert e == ddd, e
        assert e.name == 'ddd', e.name

        e = f.get_entry('eee')
        assert e == eee, e
        assert e.name == 'eee', e.name
        assert not hasattr(e, 'arg'), e

        f.store_info('eee', DummyNode('eee', fff))

        e = f.get_entry('eee')
        assert e == eee, e
        assert e.name == 'eee', e.name
        assert not hasattr(e, 'arg'), e

        f.merge()

        e = f.get_entry('eee')
        assert e.name == 'fff', e.name
        assert e.arg == 'fff arg', e.arg

class SConsignDBTestCase(SConsignTestCase):

    def test_SConsignDB(self):
        save_DataBase = SCons.SConsign.DataBase
        SCons.SConsign.DataBase = {}
        try:
            d1 = SCons.SConsign.DB(DummyNode('dir1'))
            d1.set_entry('aaa', DummySConsignEntry('aaa name'))
            d1.set_entry('bbb', DummySConsignEntry('bbb name'))

            aaa = d1.get_entry('aaa')
            assert aaa.name == 'aaa name'
            bbb = d1.get_entry('bbb')
            assert bbb.name == 'bbb name'

            d2 = SCons.SConsign.DB(DummyNode('dir2'))
            d2.set_entry('ccc', DummySConsignEntry('ccc name'))
            d2.set_entry('ddd', DummySConsignEntry('ddd name'))

            ccc = d2.get_entry('ccc')
            assert ccc.name == 'ccc name'
            ddd = d2.get_entry('ddd')
            assert ddd.name == 'ddd name'

            d31 = SCons.SConsign.DB(DummyNode('dir3/sub1'))
            d31.set_entry('eee', DummySConsignEntry('eee name'))
            d31.set_entry('fff', DummySConsignEntry('fff name'))

            eee = d31.get_entry('eee')
            assert eee.name == 'eee name'
            fff = d31.get_entry('fff')
            assert fff.name == 'fff name'

            d32 = SCons.SConsign.DB(DummyNode('dir3%ssub2' % os.sep))
            d32.set_entry('ggg', DummySConsignEntry('ggg name'))
            d32.set_entry('hhh', DummySConsignEntry('hhh name'))

            ggg = d32.get_entry('ggg')
            assert ggg.name == 'ggg name'
            hhh = d32.get_entry('hhh')
            assert hhh.name == 'hhh name'

        finally:

            SCons.SConsign.DataBase = save_DataBase

class SConsignDirFileTestCase(SConsignTestCase):

    def test_SConsignDirFile(self):
        bi_foo = DummySConsignEntry('foo')
        bi_bar = DummySConsignEntry('bar')

        f = SCons.SConsign.DirFile(DummyNode())
        f.set_entry('foo', bi_foo)
        f.set_entry('bar', bi_bar)

        e = f.get_entry('foo')
        assert e == bi_foo, e
        assert e.name == 'foo', e.name

        e = f.get_entry('bar')
        assert e == bi_bar, e
        assert e.name == 'bar', e.name
        assert not hasattr(e, 'arg'), e

        bbb = DummySConsignEntry('bbb')
        bbb.arg = 'bbb arg'

        f.set_entry('bar', bbb)

        e = f.get_entry('bar')
        assert e.name == 'bbb', e.name
        assert e.arg == 'bbb arg', e.arg


class SConsignFileTestCase(SConsignTestCase):

    def test_SConsignFile(self):
        test = self.test
        file = test.workpath('sconsign_file')

        assert SCons.SConsign.DataBase == {}, SCons.SConsign.DataBase
        if get_hash_format() is None and get_current_hash_algorithm_used() == 'md5':
            assert SCons.SConsign.DB_Name == ".sconsign", SCons.SConsign.DB_Name
        else:
            assert SCons.SConsign.DB_Name == ".sconsign_{}".format(get_current_hash_algorithm_used()), SCons.SConsign.DB_Name
        assert SCons.SConsign.DB_Module is SCons.dblite, SCons.SConsign.DB_Module

        SCons.SConsign.File(file)

        assert SCons.SConsign.DataBase == {}, SCons.SConsign.DataBase
        assert SCons.SConsign.DB_Name is file, SCons.SConsign.DB_Name
        assert SCons.SConsign.DB_Module is SCons.dblite, SCons.SConsign.DB_Module

        SCons.SConsign.File(None)

        assert SCons.SConsign.DataBase == {}, SCons.SConsign.DataBase
        assert SCons.SConsign.DB_Name is file, SCons.SConsign.DB_Name
        assert SCons.SConsign.DB_Module is None, SCons.SConsign.DB_Module

        class Fake_DBM:
            def open(self, name, mode):
                self.name = name
                self.mode = mode
                return self
            def __getitem__(self, key):
                pass
            def __setitem__(self, key, value):
                pass

        fake_dbm = Fake_DBM()

        SCons.SConsign.File(file, fake_dbm)

        assert SCons.SConsign.DataBase == {}, SCons.SConsign.DataBase
        assert SCons.SConsign.DB_Name is file, SCons.SConsign.DB_Name
        assert SCons.SConsign.DB_Module is fake_dbm, SCons.SConsign.DB_Module
        assert not hasattr(fake_dbm, 'name'), fake_dbm
        assert not hasattr(fake_dbm, 'mode'), fake_dbm

        SCons.SConsign.ForDirectory(DummyNode(test.workpath('dir')))

        assert SCons.SConsign.DataBase is not None, SCons.SConsign.DataBase
        assert fake_dbm.name == file, fake_dbm.name
        assert fake_dbm.mode == "c", fake_dbm.mode


class writeTestCase(SConsignTestCase):

    def test_write(self):

        test = self.test
        file = test.workpath('sconsign_file')

        class Fake_DBM:
            def __getitem__(self, key):
                return None
            def __setitem__(self, key, value):
                pass
            def open(self, name, mode):
                self.sync_count = 0
                return self
            def sync(self):
                self.sync_count = self.sync_count + 1

        fake_dbm = Fake_DBM()

        SCons.SConsign.DataBase = {}
        SCons.SConsign.File(file, fake_dbm)

        f = SCons.SConsign.DB(DummyNode())

        bi_foo = DummySConsignEntry('foo')
        bi_bar = DummySConsignEntry('bar')
        f.set_entry('foo', bi_foo)
        f.set_entry('bar', bi_bar)

        SCons.SConsign.write()

        assert bi_foo.c_to_s, bi_foo.c_to_s
        assert bi_bar.c_to_s, bi_bar.c_to_s

        assert fake_dbm.sync_count == 1, fake_dbm.sync_count



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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/Subst.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""SCons string substitution."""

import collections
import re
from inspect import signature, Parameter

import SCons.Errors
from SCons.Util import is_String, is_Sequence

# Indexed by the SUBST_* constants below.
_strconv = [
    SCons.Util.to_String_for_subst,
    SCons.Util.to_String_for_subst,
    SCons.Util.to_String_for_signature,
]

AllowableExceptions = (IndexError, NameError)


def SetAllowableExceptions(*excepts):
    global AllowableExceptions
    AllowableExceptions = [_f for _f in excepts if _f]


def raise_exception(exception, target, s):
    name = exception.__class__.__name__
    msg = "%s `%s' trying to evaluate `%s'" % (name, exception, s)
    if target:
        raise SCons.Errors.BuildError(target[0], msg)
    else:
        raise SCons.Errors.UserError(msg)


class Literal:
    """A wrapper for a string.  If you use this object wrapped
    around a string, then it will be interpreted as literal.
    When passed to the command interpreter, all special
    characters will be escaped."""
    def __init__(self, lstr):
        self.lstr = lstr

    def __str__(self):
        return self.lstr

    def escape(self, escape_func):
        return escape_func(self.lstr)

    def for_signature(self):
        return self.lstr

    def is_literal(self):
        return 1

    def __eq__(self, other):
        if not isinstance(other, Literal):
            return False
        return self.lstr == other.lstr

    def __neq__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash(self.lstr)

class SpecialAttrWrapper:
    """This is a wrapper for what we call a 'Node special attribute.'
    This is any of the attributes of a Node that we can reference from
    Environment variable substitution, such as $TARGET.abspath or
    $SOURCES[1].filebase.  We implement the same methods as Literal
    so we can handle special characters, plus a for_signature method,
    such that we can return some canonical string during signature
    calculation to avoid unnecessary rebuilds."""

    def __init__(self, lstr, for_signature=None):
        """The for_signature parameter, if supplied, will be the
        canonical string we return from for_signature().  Else
        we will simply return lstr."""
        self.lstr = lstr
        if for_signature:
            self.forsig = for_signature
        else:
            self.forsig = lstr

    def __str__(self):
        return self.lstr

    def escape(self, escape_func):
        return escape_func(self.lstr)

    def for_signature(self):
        return self.forsig

    def is_literal(self):
        return 1

def quote_spaces(arg):
    """Generic function for putting double quotes around any string that
    has white space in it."""
    if ' ' in arg or '\t' in arg:
        return '"%s"' % arg
    else:
        return str(arg)

class CmdStringHolder(collections.UserString):
    """This is a special class used to hold strings generated by
    scons_subst() and scons_subst_list().  It defines a special method
    escape().  When passed a function with an escape algorithm for a
    particular platform, it will return the contained string with the
    proper escape sequences inserted.
    """
    def __init__(self, cmd, literal=None):
        super().__init__(cmd)
        self.literal = literal

    def is_literal(self):
        return self.literal

    def escape(self, escape_func, quote_func=quote_spaces):
        """Escape the string with the supplied function.  The
        function is expected to take an arbitrary string, then
        return it with all special characters escaped and ready
        for passing to the command interpreter.

        After calling this function, the next call to str() will
        return the escaped string.
        """

        if self.is_literal():
            return escape_func(self.data)
        elif ' ' in self.data or '\t' in self.data:
            return quote_func(self.data)
        else:
            return self.data

def escape_list(mylist, escape_func):
    """Escape a list of arguments by running the specified escape_func
    on every object in the list that has an escape() method."""
    def escape(obj, escape_func=escape_func):
        try:
            e = obj.escape
        except AttributeError:
            return obj
        else:
            return e(escape_func)
    return list(map(escape, mylist))

class NLWrapper:
    """A wrapper class that delays turning a list of sources or targets
    into a NodeList until it's needed.  The specified function supplied
    when the object is initialized is responsible for turning raw nodes
    into proxies that implement the special attributes like .abspath,
    .source, etc.  This way, we avoid creating those proxies just
    "in case" someone is going to use $TARGET or the like, and only
    go through the trouble if we really have to.

    In practice, this might be a wash performance-wise, but it's a little
    cleaner conceptually...
    """

    def __init__(self, list, func):
        self.list = list
        self.func = func
    def _return_nodelist(self):
        return self.nodelist
    def _gen_nodelist(self):
        mylist = self.list
        if mylist is None:
            mylist = []
        elif not is_Sequence(mylist):
            mylist = [mylist]
        # The map(self.func) call is what actually turns
        # a list into appropriate proxies.
        self.nodelist = SCons.Util.NodeList(list(map(self.func, mylist)))
        self._create_nodelist = self._return_nodelist
        return self.nodelist
    _create_nodelist = _gen_nodelist


class Targets_or_Sources(collections.UserList):
    """A class that implements $TARGETS or $SOURCES expansions by in turn
    wrapping a NLWrapper.  This class handles the different methods used
    to access the list, calling the NLWrapper to create proxies on demand.

    Note that we subclass collections.UserList purely so that the
    is_Sequence() function will identify an object of this class as
    a list during variable expansion.  We're not really using any
    collections.UserList methods in practice.
    """
    def __init__(self, nl):
        self.nl = nl
    def __getattr__(self, attr):
        nl = self.nl._create_nodelist()
        return getattr(nl, attr)
    def __getitem__(self, i):
        nl = self.nl._create_nodelist()
        return nl[i]
    def __str__(self):
        nl = self.nl._create_nodelist()
        return str(nl)
    def __repr__(self):
        nl = self.nl._create_nodelist()
        return repr(nl)

class Target_or_Source:
    """A class that implements $TARGET or $SOURCE expansions by in turn
    wrapping a NLWrapper.  This class handles the different methods used
    to access an individual proxy Node, calling the NLWrapper to create
    a proxy on demand.
    """
    def __init__(self, nl):
        self.nl = nl
    def __getattr__(self, attr):
        nl = self.nl._create_nodelist()
        try:
            nl0 = nl[0]
        except IndexError:
            # If there is nothing in the list, then we have no attributes to
            # pass through, so raise AttributeError for everything.
            raise AttributeError("NodeList has no attribute: %s" % attr)
        return getattr(nl0, attr)
    def __str__(self):
        nl = self.nl._create_nodelist()
        if nl:
            return str(nl[0])
        return ''
    def __repr__(self):
        nl = self.nl._create_nodelist()
        if nl:
            return repr(nl[0])
        return ''

class NullNodeList(SCons.Util.NullSeq):
  def __call__(self, *args, **kwargs): return ''
  def __str__(self): return ''

NullNodesList = NullNodeList()

def subst_dict(target, source):
    """Create a dictionary for substitution of special
    construction variables.

    This translates the following special arguments:

    target - the target (object or array of objects),
             used to generate the TARGET and TARGETS
             construction variables

    source - the source (object or array of objects),
             used to generate the SOURCES and SOURCE
             construction variables
    """
    dict = {}

    if target:
        def get_tgt_subst_proxy(thing):
            try:
                subst_proxy = thing.get_subst_proxy()
            except AttributeError:
                subst_proxy = thing # probably a string, just return it
            return subst_proxy
        tnl = NLWrapper(target, get_tgt_subst_proxy)
        dict['TARGETS'] = Targets_or_Sources(tnl)
        dict['TARGET'] = Target_or_Source(tnl)

        # This is a total cheat, but hopefully this dictionary goes
        # away soon anyway.  We just let these expand to $TARGETS
        # because that's "good enough" for the use of ToolSurrogates
        # (see test/ToolSurrogate.py) to generate documentation.
        dict['CHANGED_TARGETS'] = '$TARGETS'
        dict['UNCHANGED_TARGETS'] = '$TARGETS'
    else:
        dict['TARGETS'] = NullNodesList
        dict['TARGET'] = NullNodesList

    if source:
        def get_src_subst_proxy(node):
            try:
                rfile = node.rfile
            except AttributeError:
                pass
            else:
                node = rfile()
            try:
                return node.get_subst_proxy()
            except AttributeError:
                return node     # probably a String, just return it
        snl = NLWrapper(source, get_src_subst_proxy)
        dict['SOURCES'] = Targets_or_Sources(snl)
        dict['SOURCE'] = Target_or_Source(snl)

        # This is a total cheat, but hopefully this dictionary goes
        # away soon anyway.  We just let these expand to $TARGETS
        # because that's "good enough" for the use of ToolSurrogates
        # (see test/ToolSurrogate.py) to generate documentation.
        dict['CHANGED_SOURCES'] = '$SOURCES'
        dict['UNCHANGED_SOURCES'] = '$SOURCES'
    else:
        dict['SOURCES'] = NullNodesList
        dict['SOURCE'] = NullNodesList

    return dict


_callable_args_set = {'target', 'source', 'env', 'for_signature'}

class StringSubber:
    """A class to construct the results of a scons_subst() call.

    This binds a specific construction environment, mode, target and
    source with two methods (substitute() and expand()) that handle
    the expansion.
    """


    def __init__(self, env, mode, conv, gvars):
        self.env = env
        self.mode = mode
        self.conv = conv
        self.gvars = gvars

    def expand(self, s, lvars):
        """Expand a single "token" as necessary, returning an
        appropriate string containing the expansion.

        This handles expanding different types of things (strings,
        lists, callables) appropriately.  It calls the wrapper
        substitute() method to re-expand things as necessary, so that
        the results of expansions of side-by-side strings still get
        re-evaluated separately, not smushed together.
        """
        if is_String(s):
            try:
                s0, s1 = s[:2]
            except (IndexError, ValueError):
                return s
            if s0 != '$':
                return s
            if s1 == '$':
                # In this case keep the double $'s which we'll later
                # swap for a single dollar sign as we need to retain
                # this information to properly avoid matching "$("" when
                # the actual text was "$$(""  (or "$)"" when "$$)"" )
                return '$$'
            elif s1 in '()':
                return s
            else:
                key = s[1:]
                if key[0] == '{' or '.' in key:
                    if key[0] == '{':
                        key = key[1:-1]

                # Store for error messages if we fail to expand the
                # value
                old_s = s
                s = None
                if key in lvars:
                     s = lvars[key]
                elif key in self.gvars:
                     s = self.gvars[key]
                else:
                     try:
                          s = eval(key, self.gvars, lvars)
                     except KeyboardInterrupt:
                          raise
                     except Exception as e:
                          if e.__class__ in AllowableExceptions:
                               return ''
                          raise_exception(e, lvars['TARGETS'], old_s)

                if s is None and NameError not in AllowableExceptions:
                     raise_exception(NameError(key), lvars['TARGETS'], old_s)
                elif s is None:
                     return ''

                # Before re-expanding the result, handle
                # recursive expansion by copying the local
                # variable dictionary and overwriting a null
                # string for the value of the variable name
                # we just expanded.
                #
                # This could potentially be optimized by only
                # copying lvars when s contains more expansions,
                # but lvars is usually supposed to be pretty
                # small, and deeply nested variable expansions
                # are probably more the exception than the norm,
                # so it should be tolerable for now.
                lv = lvars.copy()
                var = key.split('.')[0]
                lv[var] = ''
                return self.substitute(s, lv)
        elif is_Sequence(s):
            def func(l, conv=self.conv, substitute=self.substitute, lvars=lvars):
                return conv(substitute(l, lvars))
            return list(map(func, s))
        elif callable(s):

            # SCons has the unusual Null class where any __getattr__ call returns it's self, 
            # which does not work the signature module, and the Null class returns an empty
            # string if called on, so we make an exception in this condition for Null class
            # Also allow callables where the only non default valued args match the expected defaults
            # this should also allow functools.partial's to work.
            if isinstance(s, SCons.Util.Null) or {k for k, v in signature(s).parameters.items() if
                                                  k in _callable_args_set or v.default == Parameter.empty} == _callable_args_set:

                s = s(target=lvars['TARGETS'],
                     source=lvars['SOURCES'],
                     env=self.env,
                     for_signature=(self.mode == SUBST_SIG))
            else:
                # This probably indicates that it's a callable
                # object that doesn't match our calling arguments
                # (like an Action).
                if self.mode == SUBST_RAW:
                    return s
                s = self.conv(s)
            return self.substitute(s, lvars)
        elif s is None:
            return ''
        else:
            return s

    def substitute(self, args, lvars):
        """Substitute expansions in an argument or list of arguments.

        This serves as a wrapper for splitting up a string into
        separate tokens.
        """
        if is_String(args) and not isinstance(args, CmdStringHolder):
            args = str(args)        # In case it's a UserString.
            try:
                def sub_match(match):
                    return self.conv(self.expand(match.group(1), lvars))
                result = _dollar_exps.sub(sub_match, args)
            except TypeError:
                # If the internal conversion routine doesn't return
                # strings (it could be overridden to return Nodes, for
                # example), then the 1.5.2 re module will throw this
                # exception.  Back off to a slower, general-purpose
                # algorithm that works for all data types.
                args = _separate_args.findall(args)
                result = []
                for a in args:
                    result.append(self.conv(self.expand(a, lvars)))
                if len(result) == 1:
                    result = result[0]
                else:
                    result = ''.join(map(str, result))
            return result
        else:
            return self.expand(args, lvars)


class ListSubber(collections.UserList):
    """A class to construct the results of a scons_subst_list() call.

    Like StringSubber, this class binds a specific construction
    environment, mode, target and source with two methods
    (substitute() and expand()) that handle the expansion.

    In addition, however, this class is used to track the state of
    the result(s) we're gathering so we can do the appropriate thing
    whenever we have to append another word to the result--start a new
    line, start a new word, append to the current word, etc.  We do
    this by setting the "append" attribute to the right method so
    that our wrapper methods only need ever call ListSubber.append(),
    and the rest of the object takes care of doing the right thing
    internally.
    """
    def __init__(self, env, mode, conv, gvars):
        super().__init__([])
        self.env = env
        self.mode = mode
        self.conv = conv
        self.gvars = gvars

        if self.mode == SUBST_RAW:
            self.add_strip = lambda x: self.append(x)
        else:
            self.add_strip = lambda x: None
        self.in_strip = None
        self.next_line()

    def expanded(self, s):
        """Determines if the string s requires further expansion.

        Due to the implementation of ListSubber expand will call
        itself 2 additional times for an already expanded string. This
        method is used to determine if a string is already fully
        expanded and if so exit the loop early to prevent these
        recursive calls.
        """
        if not is_String(s) or isinstance(s, CmdStringHolder):
            return False

        s = str(s)  # in case it's a UserString
        return _separate_args.findall(s) is None

    def expand(self, s, lvars, within_list):
        """Expand a single "token" as necessary, appending the
        expansion to the current result.

        This handles expanding different types of things (strings,
        lists, callables) appropriately.  It calls the wrapper
        substitute() method to re-expand things as necessary, so that
        the results of expansions of side-by-side strings still get
        re-evaluated separately, not smushed together.
        """

        if is_String(s):
            try:
                s0, s1 = s[:2]
            except (IndexError, ValueError):
                self.append(s)
                return
            if s0 != '$':
                self.append(s)
                return
            if s1 == '$':
                self.append('$')
            elif s1 == '(':
                self.open_strip('$(')
            elif s1 == ')':
                self.close_strip('$)')
            else:
                key = s[1:]
                if key[0] == '{' or key.find('.') >= 0:
                    if key[0] == '{':
                        key = key[1:-1]

                # Store for error messages if we fail to expand the
                # value
                old_s = s
                s = None
                if key in lvars:
                     s = lvars[key]
                elif key in self.gvars:
                     s = self.gvars[key]
                else:
                     try:
                         s = eval(key, self.gvars, lvars)
                     except KeyboardInterrupt:
                         raise
                     except Exception as e:
                         if e.__class__ in AllowableExceptions:
                             return
                         raise_exception(e, lvars['TARGETS'], old_s)

                if s is None and NameError not in AllowableExceptions:
                     raise_exception(NameError(), lvars['TARGETS'], old_s)
                elif s is None:
                     return

                # If the string is already full expanded there's no
                # need to continue recursion.
                if self.expanded(s):
                    self.append(s)
                    return

                # Before re-expanding the result, handle
                # recursive expansion by copying the local
                # variable dictionary and overwriting a null
                # string for the value of the variable name
                # we just expanded.
                lv = lvars.copy()
                var = key.split('.')[0]
                lv[var] = ''
                self.substitute(s, lv, 0)
                self.this_word()
        elif is_Sequence(s):
            for a in s:
                self.substitute(a, lvars, 1)
                self.next_word()
        elif callable(s):
            # SCons has the unusual Null class where any __getattr__ call returns it's self, 
            # which does not work the signature module, and the Null class returns an empty
            # string if called on, so we make an exception in this condition for Null class
            # Also allow callables where the only non default valued args match the expected defaults
            # this should also allow functools.partial's to work.
            if isinstance(s, SCons.Util.Null) or {k for k, v in signature(s).parameters.items() if
                                                  k in _callable_args_set or v.default == Parameter.empty} == _callable_args_set:

                s = s(target=lvars['TARGETS'],
                     source=lvars['SOURCES'],
                     env=self.env,
                     for_signature=(self.mode != SUBST_CMD))
            else:
                # This probably indicates that it's a callable
                # object that doesn't match our calling arguments
                # (like an Action).
                if self.mode == SUBST_RAW:
                    self.append(s)
                    return
                s = self.conv(s)
            self.substitute(s, lvars, within_list)
        elif s is None:
            self.this_word()
        else:
            self.append(s)

    def substitute(self, args, lvars, within_list):
        """Substitute expansions in an argument or list of arguments.

        This serves as a wrapper for splitting up a string into
        separate tokens.
        """

        if is_String(args) and not isinstance(args, CmdStringHolder):
            args = str(args)        # In case it's a UserString.
            args = _separate_args.findall(args)
            for a in args:
                if a[0] in ' \t\n\r\f\v':
                    if '\n' in a:
                        self.next_line()
                    elif within_list:
                        self.append(a)
                    else:
                        self.next_word()
                else:
                    self.expand(a, lvars, within_list)
        else:
            self.expand(args, lvars, within_list)

    def next_line(self):
        """Arrange for the next word to start a new line.  This
        is like starting a new word, except that we have to append
        another line to the result."""
        collections.UserList.append(self, [])
        self.next_word()

    def this_word(self):
        """Arrange for the next word to append to the end of the
        current last word in the result."""
        self.append = self.add_to_current_word

    def next_word(self):
        """Arrange for the next word to start a new word."""
        self.append = self.add_new_word

    def add_to_current_word(self, x):
        """Append the string x to the end of the current last word
        in the result.  If that is not possible, then just add
        it as a new word.  Make sure the entire concatenated string
        inherits the object attributes of x (in particular, the
        escape function) by wrapping it as CmdStringHolder."""

        if not self.in_strip or self.mode != SUBST_SIG:
            try:
                current_word = self[-1][-1]
            except IndexError:
                self.add_new_word(x)
            else:
                # All right, this is a hack and it should probably
                # be refactored out of existence in the future.
                # The issue is that we want to smoosh words together
                # and make one file name that gets escaped if
                # we're expanding something like foo$EXTENSION,
                # but we don't want to smoosh them together if
                # it's something like >$TARGET, because then we'll
                # treat the '>' like it's part of the file name.
                # So for now, just hard-code looking for the special
                # command-line redirection characters...
                try:
                    last_char = str(current_word)[-1]
                except IndexError:
                    last_char = '\0'
                if last_char in '<>|':
                    self.add_new_word(x)
                else:
                    y = current_word + x

                    # We used to treat a word appended to a literal
                    # as a literal itself, but this caused problems
                    # with interpreting quotes around space-separated
                    # targets on command lines.  Removing this makes
                    # none of the "substantive" end-to-end tests fail,
                    # so we'll take this out but leave it commented
                    # for now in case there's a problem not covered
                    # by the test cases and we need to resurrect this.
                    #literal1 = self.literal(self[-1][-1])
                    #literal2 = self.literal(x)
                    y = self.conv(y)
                    if is_String(y):
                        #y = CmdStringHolder(y, literal1 or literal2)
                        y = CmdStringHolder(y, None)
                    self[-1][-1] = y

    def add_new_word(self, x):
        if not self.in_strip or self.mode != SUBST_SIG:
            literal = self.literal(x)
            x = self.conv(x)
            if is_String(x):
                x = CmdStringHolder(x, literal)
            self[-1].append(x)
        self.append = self.add_to_current_word

    def literal(self, x):
        try:
            l = x.is_literal
        except AttributeError:
            return None
        else:
            return l()

    def open_strip(self, x):
        """Handle the "open strip" $( token."""
        self.add_strip(x)
        self.in_strip = 1

    def close_strip(self, x):
        """Handle the "close strip" $) token."""
        self.add_strip(x)
        self.in_strip = None


# Constants for the "mode" parameter to scons_subst_list() and
# scons_subst().  SUBST_RAW gives the raw command line.  SUBST_CMD
# gives a command line suitable for passing to a shell.  SUBST_SIG
# gives a command line appropriate for calculating the signature
# of a command line...if this changes, we should rebuild.
SUBST_CMD = 0
SUBST_RAW = 1
SUBST_SIG = 2

_rm = re.compile(r'\$[()]')

# Note the pattern below only matches $( or $) when there is no
# preceeding $. (Thus the (?<!\$))
_rm_split = re.compile(r'(?<!\$)(\$[()])')

# Indexed by the SUBST_* constants above.
_regex_remove = [ _rm, None, _rm_split ]

def _rm_list(list):
    return [l for l in list if l not in ('$(', '$)')]

def _remove_list(list):
    result = []
    depth = 0
    for l in list:
        if l == '$(':
            depth += 1
        elif l == '$)':
            depth -= 1
            if depth < 0:
                break
        elif depth == 0:
            result.append(l)
    if depth != 0:
        return None
    return result

# Indexed by the SUBST_* constants above.
_list_remove = [ _rm_list, None, _remove_list ]

# Regular expressions for splitting strings and handling substitutions,
# for use by the scons_subst() and scons_subst_list() functions:
#
# The first expression compiled matches all of the $-introduced tokens
# that we need to process in some way, and is used for substitutions.
# The expressions it matches are:
#
#       "$$"
#       "$("
#       "$)"
#       "$variable"             [must begin with alphabetic or underscore]
#       "${any stuff}"
#
# The second expression compiled is used for splitting strings into tokens
# to be processed, and it matches all of the tokens listed above, plus
# the following that affect how arguments do or don't get joined together:
#
#       "   "                   [white space]
#       "non-white-space"       [without any dollar signs]
#       "$"                     [single dollar sign]
#
_dollar_exps_str = r'\$[\$\(\)]|\$[_a-zA-Z][\.\w]*|\${[^}]*}'
_dollar_exps = re.compile(r'(%s)' % _dollar_exps_str)
_separate_args = re.compile(r'(%s|\s+|[^\s$]+|\$)' % _dollar_exps_str)

# This regular expression is used to replace strings of multiple white
# space characters in the string result from the scons_subst() function.
_space_sep = re.compile(r'[\t ]+(?![^{]*})')

def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None):
    """Expand a string or list containing construction variable
    substitutions.

    This is the work-horse function for substitutions in file names
    and the like.  The companion scons_subst_list() function (below)
    handles separating command lines into lists of arguments, so see
    that function if that's what you're looking for.
    """
    if (isinstance(strSubst, str) and '$' not in strSubst) or isinstance(strSubst, CmdStringHolder):
        return strSubst

    if conv is None:
        conv = _strconv[mode]

    # Doing this every time is a bit of a waste, since the Executor
    # has typically already populated the OverrideEnvironment with
    # $TARGET/$SOURCE variables.  We're keeping this (for now), though,
    # because it supports existing behavior that allows us to call
    # an Action directly with an arbitrary target+source pair, which
    # we use in Tool/tex.py to handle calling $BIBTEX when necessary.
    # If we dropped that behavior (or found another way to cover it),
    # we could get rid of this call completely and just rely on the
    # Executor setting the variables.
    if 'TARGET' not in lvars:
        d = subst_dict(target, source)
        if d:
            lvars = lvars.copy()
            lvars.update(d)

    # We're (most likely) going to eval() things.  If Python doesn't
    # find a __builtins__ value in the global dictionary used for eval(),
    # it copies the current global values for you.  Avoid this by
    # setting it explicitly and then deleting, so we don't pollute the
    # construction environment Dictionary(ies) that are typically used
    # for expansion.
    gvars['__builtins__'] = __builtins__

    ss = StringSubber(env, mode, conv, gvars)
    result = ss.substitute(strSubst, lvars)

    try:
        del gvars['__builtins__']
    except KeyError:
        pass

    res = result
    if is_String(result):
        # Remove $(-$) pairs and any stuff in between,
        # if that's appropriate.
        remove = _regex_remove[mode]
        if remove:
            if mode == SUBST_SIG:
                result = _list_remove[mode](remove.split(result))
                if result is None:
                    raise SCons.Errors.UserError("Unbalanced $(/$) in: " + res)
                result = ' '.join(result)
            else:
                result = remove.sub('', result)
        if mode != SUBST_RAW:
            # Compress strings of white space characters into
            # a single space.
            result = _space_sep.sub(' ', result).strip()

        # Now replace escaped $'s currently "$$"
        # This is needed because we now retain $$ instead of
        # replacing them during substition to avoid
        # improperly trying to escape "$$(" as being "$("
        result = result.replace('$$','$')
    elif is_Sequence(result):
        remove = _list_remove[mode]
        if remove:
            result = remove(result)
            if result is None:
                raise SCons.Errors.UserError("Unbalanced $(/$) in: " + str(res))

    return result

def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None):
    """Substitute construction variables in a string (or list or other
    object) and separate the arguments into a command list.

    The companion scons_subst() function (above) handles basic
    substitutions within strings, so see that function instead
    if that's what you're looking for.
    """ 
    if conv is None:
        conv = _strconv[mode]

    # Doing this every time is a bit of a waste, since the Executor
    # has typically already populated the OverrideEnvironment with
    # $TARGET/$SOURCE variables.  We're keeping this (for now), though,
    # because it supports existing behavior that allows us to call
    # an Action directly with an arbitrary target+source pair, which
    # we use in Tool/tex.py to handle calling $BIBTEX when necessary.
    # If we dropped that behavior (or found another way to cover it),
    # we could get rid of this call completely and just rely on the
    # Executor setting the variables.
    if 'TARGET' not in lvars:
        d = subst_dict(target, source)
        if d:
            lvars = lvars.copy()
            lvars.update(d)

    # We're (most likely) going to eval() things.  If Python doesn't
    # find a __builtins__ value in the global dictionary used for eval(),
    # it copies the current global values for you.  Avoid this by
    # setting it explicitly and then deleting, so we don't pollute the
    # construction environment Dictionary(ies) that are typically used
    # for expansion.
    gvars['__builtins__'] = __builtins__

    ls = ListSubber(env, mode, conv, gvars)
    ls.substitute(strSubst, lvars, 0)

    try:
        del gvars['__builtins__']
    except KeyError:
        pass

    return ls.data

def scons_subst_once(strSubst, env, key):
    """Perform single (non-recursive) substitution of a single
    construction variable keyword.

    This is used when setting a variable when copying or overriding values
    in an Environment.  We want to capture (expand) the old value before
    we override it, so people can do things like:

        env2 = env.Clone(CCFLAGS = '$CCFLAGS -g')

    We do this with some straightforward, brute-force code here...
    """
    if isinstance(strSubst, str) and strSubst.find('$') < 0:
        return strSubst

    matchlist = ['$' + key, '${' + key + '}']
    val = env.get(key, '')
    def sub_match(match, val=val, matchlist=matchlist):
        a = match.group(1)
        if a in matchlist:
            a = val
        if is_Sequence(a):
            return ' '.join(map(str, a))
        else:
            return str(a)

    if is_Sequence(strSubst):
        result = []
        for arg in strSubst:
            if is_String(arg):
                if arg in matchlist:
                    arg = val
                    if is_Sequence(arg):
                        result.extend(arg)
                    else:
                        result.append(arg)
                else:
                    result.append(_dollar_exps.sub(sub_match, arg))
            else:
                result.append(arg)
        return result
    elif is_String(strSubst):
        return _dollar_exps.sub(sub_match, strSubst)
    else:
        return strSubst

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/SubstTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import SCons.compat

import os
import unittest
from functools import partial


import SCons.Errors

from SCons.Subst import (Literal, SUBST_CMD, SUBST_RAW, SUBST_SIG, SpecialAttrWrapper, collections,
                         escape_list, quote_spaces, scons_subst, scons_subst_list, scons_subst_once,
                         subst_dict)

class DummyNode:
    """Simple node work-alike."""
    def __init__(self, name):
        self.name = os.path.normpath(name)
    def __str__(self):
        return self.name
    def is_literal(self):
        return 1
    def rfile(self):
        return self
    def get_subst_proxy(self):
        return self

class DummyEnv:
    def __init__(self, dict={}):
        self.dict = dict

    def Dictionary(self, key = None):
        if not key:
            return self.dict
        return self.dict[key]

    def __getitem__(self, key):
        return self.dict[key]

    def get(self, key, default):
        return self.dict.get(key, default)

    def sig_dict(self):
        dict = self.dict.copy()
        dict["TARGETS"] = 'tsig'
        dict["SOURCES"] = 'ssig'
        return dict

def cs(target=None, source=None, env=None, for_signature=None):
    return 'cs'

def cl(target=None, source=None, env=None, for_signature=None):
    return ['cl']

def CmdGen1(target, source, env, for_signature):
    # Nifty trick...since Environment references are interpolated,
    # instantiate an instance of a callable class with this one,
    # which will then get evaluated.
    assert str(target) == 't', target
    assert str(source) == 's', source
    return "${CMDGEN2('foo', %d)}" % for_signature

class CmdGen2:
    def __init__(self, mystr, forsig):
        self.mystr = mystr
        self.expect_for_signature = forsig

    def __call__(self, target, source, env, for_signature):
        assert str(target) == 't', target
        assert str(source) == 's', source
        assert for_signature == self.expect_for_signature, for_signature
        return [ self.mystr, env.Dictionary('BAR') ]


def CallableWithDefault(target, source, env, for_signature, other_value="default"):
    assert str(target) == 't', target
    assert str(source) == 's', source
    return "CallableWithDefault: %s"%other_value

PartialCallable = partial(CallableWithDefault, other_value="partial")

def CallableWithNoDefault(target, source, env, for_signature, other_value):
    assert str(target) == 't', target
    assert str(source) == 's', source
    return "CallableWithNoDefault: %s"%other_value

PartialCallableNoDefault = partial(CallableWithNoDefault, other_value="partialNoDefault")



if os.sep == '/':
    def cvt(str):
        return str
else:
    def cvt(str):
        return str.replace('/', os.sep)

class SubstTestCase(unittest.TestCase):
    class MyNode(DummyNode):
        """Simple node work-alike with some extra stuff for testing."""
        def __init__(self, name):
            super().__init__(name)
            class Attribute:
                pass
            self.attribute = Attribute()
            self.attribute.attr1 = 'attr$1-' + os.path.basename(name)
            self.attribute.attr2 = 'attr$2-' + os.path.basename(name)
        def get_stuff(self, extra):
            return self.name + extra
        foo = 1

    class TestLiteral:
        def __init__(self, literal):
            self.literal = literal
        def __str__(self):
            return self.literal
        def is_literal(self):
            return 1

    class TestCallable:
        def __init__(self, value):
            self.value = value
        def __call__(self):
            pass
        def __str__(self):
            return self.value

    # only use of this is currently commented out below
    #def function_foo(arg):
    #    pass

    target = [ MyNode("./foo/bar.exe"),
               MyNode("/bar/baz with spaces.obj"),
               MyNode("../foo/baz.obj") ]
    source = [ MyNode("./foo/blah with spaces.cpp"),
               MyNode("/bar/ack.cpp"),
               MyNode("../foo/ack.c") ]

    callable_object_1 = TestCallable('callable-1')
    callable_object_2 = TestCallable('callable-2')

    def _defines(defs):
        l = []
        for d in defs:
            if SCons.Util.is_List(d) or isinstance(d, tuple):
                l.append(str(d[0]) + '=' + str(d[1]))
            else:
                l.append(str(d))
        return l

    loc = {
        'xxx'       : None,
        'NEWLINE'   : 'before\nafter',

        'null'      : '',
        'zero'      : 0,
        'one'       : 1,
        'BAZ'       : 'baz',
        'ONE'       : '$TWO',
        'TWO'       : '$THREE',
        'THREE'     : 'four',

        'AAA'       : 'a',
        'BBB'       : 'b',
        'CCC'       : 'c',

        'DO'        : DummyNode('do something'),
        'FOO'       : DummyNode('foo.in'),
        'BAR'       : DummyNode('bar with spaces.out'),
        'CRAZY'     : DummyNode('crazy\nfile.in'),

        # $XXX$HHH should expand to GGGIII, not BADNEWS.
        'XXX'       : '$FFF',
        'FFF'       : 'GGG',
        'HHH'       : 'III',
        'FFFIII'    : 'BADNEWS',

        'THING1'    : "$(STUFF$)",
        'THING2'    : "$THING1",

        'LITERAL'   : TestLiteral("$XXX"),

        # Test that we can expand to and return a function.
        #'FUNCTION'  : function_foo,

        'CMDGEN1'   : CmdGen1,
        'CMDGEN2'   : CmdGen2,

        'CallableWithDefault': CallableWithDefault,
        'PartialCallable' : PartialCallable,
        'PartialCallableNoDefault' : PartialCallableNoDefault,

        'LITERALS'  : [ Literal('foo\nwith\nnewlines'),
                        Literal('bar\nwith\nnewlines') ],

        'NOTHING'   : "",
        'NONE'      : None,

        # Test various combinations of strings, lists and functions.
        'N'         : None,
        'X'         : 'x',
        'Y'         : '$X',
        'R'         : '$R',
        'S'         : 'x y',
        'LS'        : ['x y'],
        'L'         : ['x', 'y'],
        'TS'        : ('x y',),
        'T'         : ('x', 'y'),
        'CS'        : cs,
        'CL'        : cl,
        'US'        : collections.UserString('us'),

        # Test function calls within ${}.
        'FUNCCALL'  : '${FUNC1("$AAA $FUNC2 $BBB")}',
        'FUNC1'     : lambda x: x,
        'FUNC2'     : lambda target, source, env, for_signature: ['x$CCC'],

        # Various tests refactored from ActionTests.py.
        'LIST'      : [["This", "is", "$(", "$a", "$)", "test"]],

        # Test recursion.
        'RECURSE'   : 'foo $RECURSE bar',
        'RRR'       : 'foo $SSS bar',
        'SSS'       : '$RRR',

        # Test callables that don't match the calling arguments.
        'CALLABLE1' : callable_object_1,
        'CALLABLE2' : callable_object_2,

        '_defines'  : _defines,
        'DEFS'      : [ ('Q1', '"q1"'), ('Q2', '"$AAA"') ],
    }

    def basic_comparisons(self, function, convert):
        env = DummyEnv(self.loc)
        cases = self.basic_cases[:]
        kwargs = {'target' : self.target, 'source' : self.source,
                  'gvars' : env.Dictionary()}

        failed = 0
        case_count = 0
        while cases:
            input, expect = cases[:2]
            expect = convert(expect)
            try:
                result = function(input, env, **kwargs)
            except Exception as e:
                fmt = "    input %s generated %s (%s)"
                print(fmt % (repr(input), e.__class__.__name__, repr(e)))
                failed = failed + 1
            else:
                if result != expect:
                    if failed == 0: print()
                    print("[%4d]    input %s => \n%s did not match \n%s" % (case_count, repr(input), repr(result), repr(expect)))
                    failed = failed + 1
            del cases[:2]
            case_count += 1
        fmt = "%d %s() cases failed"
        assert failed == 0, fmt % (failed, function.__name__)


class scons_subst_TestCase(SubstTestCase):

    # Basic tests of substitution functionality.
    basic_cases = [
        # Basics:  strings without expansions are left alone, and
        # the simplest possible expansion to a null-string value.
        "test",                 "test",
        "$null",                "",

        # Test expansion of integer values.
        "test $zero",           "test 0",
        "test $one",            "test 1",

        # Test multiple re-expansion of values.
        "test $ONE",            "test four",

        # Test a whole bunch of $TARGET[S] and $SOURCE[S] expansions.
        "test $TARGETS $SOURCES",
        "test foo/bar.exe /bar/baz with spaces.obj ../foo/baz.obj foo/blah with spaces.cpp /bar/ack.cpp ../foo/ack.c",

        "test ${TARGETS[:]} ${SOURCES[0]}",
        "test foo/bar.exe /bar/baz with spaces.obj ../foo/baz.obj foo/blah with spaces.cpp",

        "test ${TARGETS[1:]}v",
        "test /bar/baz with spaces.obj ../foo/baz.objv",

        "test $TARGET",
        "test foo/bar.exe",

        "test $TARGET$NO_SUCH_VAR[0]",
        "test foo/bar.exe[0]",

        "test $TARGETS.foo",
        "test 1 1 1",

        "test ${SOURCES[0:2].foo}",
        "test 1 1",

        "test $SOURCE.foo",
        "test 1",

        "test ${TARGET.get_stuff('blah')}",
        "test foo/bar.exeblah",

        "test ${SOURCES.get_stuff('blah')}",
        "test foo/blah with spaces.cppblah /bar/ack.cppblah ../foo/ack.cblah",

        "test ${SOURCES[0:2].get_stuff('blah')}",
        "test foo/blah with spaces.cppblah /bar/ack.cppblah",

        "test ${SOURCES[0:2].get_stuff('blah')}",
        "test foo/blah with spaces.cppblah /bar/ack.cppblah",

        "test ${SOURCES.attribute.attr1}",
        "test attr$1-blah with spaces.cpp attr$1-ack.cpp attr$1-ack.c",

        "test ${SOURCES.attribute.attr2}",
        "test attr$2-blah with spaces.cpp attr$2-ack.cpp attr$2-ack.c",

        # Test adjacent expansions.
        "foo$BAZ",
        "foobaz",

        "foo${BAZ}",
        "foobaz",

        # Test that adjacent expansions don't get re-interpreted
        # together.  The correct disambiguated expansion should be:
        #   $XXX$HHH => ${FFF}III => GGGIII
        # not:
        #   $XXX$HHH => ${FFFIII} => BADNEWS
        "$XXX$HHH",             "GGGIII",

        # Test double-dollar-sign behavior.
        "$$FFF$HHH",            "$FFFIII",

        # Test double-dollar-sign before open paren. It's not meant
        # to be signature escaping
        'echo $$(pwd) > XYZ',   'echo $(pwd) > XYZ',

        # Test that a Literal will stop dollar-sign substitution.
        "$XXX $LITERAL $FFF",   "GGG $XXX GGG",

        # Test that we don't blow up even if they subscript
        # something in ways they "can't."
        "${FFF[0]}",            "G",
        "${FFF[7]}",            "",
        "${NOTHING[1]}",        "",

        # Test various combinations of strings and lists.
        #None,                   '',
        '',                     '',
        'x',                    'x',
        'x y',                  'x y',
        '$N',                   '',
        '$X',                   'x',
        '$Y',                   'x',
        '$R',                   '',
        '$S',                   'x y',
        '$LS',                  'x y',
        '$L',                   'x y',
        '$TS',                  'x y',
        '$T',                   'x y',
        '$S z',                 'x y z',
        '$LS z',                'x y z',
        '$L z',                 'x y z',
        '$TS z',                'x y z',
        '$T z',                 'x y z',
        #cs,                     'cs',
        #cl,                     'cl',
        '$CS',                  'cs',
        '$CL',                  'cl',

        # Various uses of UserString.
        collections.UserString('x'),         'x',
        collections.UserString('$X'),        'x',
        collections.UserString('$US'),       'us',
        '$US',                              'us',

        # Test function calls within ${}.
        '$FUNCCALL',            'a xc b',

        # Bug reported by Christoph Wiedemann.
        cvt('$xxx/bin'),        '/bin',

        # Tests callables that don't match our calling arguments.
        '$CALLABLE1',            'callable-1',

        # Test handling of quotes.
        'aaa "bbb ccc" ddd',    'aaa "bbb ccc" ddd',
    ]

    def test_scons_subst(self):
        """Test scons_subst():  basic substitution"""
        return self.basic_comparisons(scons_subst, cvt)

    subst_cases = [
        "test $xxx",
            "test ",
            "test",
            "test",

        "test $($xxx$)",
            "test $($)",
            "test",
            "test",

        "test $( $xxx $)",
            "test $(  $)",
            "test",
            "test",

        "test $( $THING2 $)",
            "test $( $(STUFF$) $)",
            "test STUFF",
            "test",

        "$AAA ${AAA}A $BBBB $BBB",
            "a aA  b",
            "a aA b",
            "a aA b",

        "$RECURSE",
           "foo  bar",
           "foo bar",
           "foo bar",

        "$RRR",
           "foo  bar",
           "foo bar",
           "foo bar",

        # Verify what happens with no target or source nodes.
        "$TARGET $SOURCES",
            " ",
            "",
            "",

        "$TARGETS $SOURCE",
            " ",
            "",
            "",

        # Various tests refactored from ActionTests.py.
        "${LIST}",
           "This is $(  $) test",
           "This is test",
           "This is test",

        ["|", "$(", "$AAA", "|", "$BBB", "$)", "|", "$CCC", 1],
            ["|", "$(", "a", "|", "b", "$)", "|", "c", "1"],
            ["|", "a", "|", "b", "|", "c", "1"],
            ["|", "|", "c", "1"],
    ]

    def test_subst_env(self):
        """Test scons_subst():  expansion dictionary"""
        # The expansion dictionary no longer comes from the construction
        # environment automatically.
        env = DummyEnv(self.loc)
        s = scons_subst('$AAA', env)
        assert s == '', s

    def test_subst_SUBST_modes(self):
        """Test scons_subst():  SUBST_* modes"""
        env = DummyEnv(self.loc)
        subst_cases = self.subst_cases[:]

        gvars = env.Dictionary()

        failed = 0
        while subst_cases:
            input, eraw, ecmd, esig = subst_cases[:4]
            result = scons_subst(input, env, mode=SUBST_RAW, gvars=gvars)
            if result != eraw:
                if failed == 0: print()
                print("    input %s => RAW %s did not match %s" % (repr(input), repr(result), repr(eraw)))
                failed = failed + 1
            result = scons_subst(input, env, mode=SUBST_CMD, gvars=gvars)
            if result != ecmd:
                if failed == 0: print()
                print("    input %s => CMD %s did not match %s" % (repr(input), repr(result), repr(ecmd)))
                failed = failed + 1
            result = scons_subst(input, env, mode=SUBST_SIG, gvars=gvars)
            if result != esig:
                if failed == 0: print()
                print("    input %s => SIG %s did not match %s" % (repr(input), repr(result), repr(esig)))
                failed = failed + 1
            del subst_cases[:4]
        assert failed == 0, "%d subst() mode cases failed" % failed

    def test_subst_target_source(self):
        """Test scons_subst():  target= and source= arguments"""
        env = DummyEnv(self.loc)
        t1 = self.MyNode('t1')
        t2 = self.MyNode('t2')
        s1 = self.MyNode('s1')
        s2 = self.MyNode('s2')
        result = scons_subst("$TARGET $SOURCES", env,
                                  target=[t1, t2],
                                  source=[s1, s2])
        assert result == "t1 s1 s2", result
        result = scons_subst("$TARGET $SOURCES", env,
                                  target=[t1, t2],
                                  source=[s1, s2],
                                  gvars={})
        assert result == "t1 s1 s2", result

        result = scons_subst("$TARGET $SOURCES", env, target=[], source=[])
        assert result == " ", result
        result = scons_subst("$TARGETS $SOURCE", env, target=[], source=[])
        assert result == " ", result

    def test_subst_callable_expansion(self):
        """Test scons_subst():  expanding a callable"""
        env = DummyEnv(self.loc)
        gvars = env.Dictionary()
        newcom = scons_subst("test $CMDGEN1 $SOURCES $TARGETS", env,
                             target=self.MyNode('t'), source=self.MyNode('s'),
                             gvars=gvars)
        assert newcom == "test foo bar with spaces.out s t", newcom

    def test_subst_callable_with_default_expansion(self):
        """Test scons_subst():  expanding a callable with a default value arg"""
        env = DummyEnv(self.loc)
        gvars = env.Dictionary()
        newcom = scons_subst("test $CallableWithDefault $SOURCES $TARGETS", env,
                             target=self.MyNode('t'), source=self.MyNode('s'),
                             gvars=gvars)
        assert newcom == "test CallableWithDefault: default s t", newcom

    def test_subst_partial_callable_with_default_expansion(self):
        """Test scons_subst():  expanding a functools.partial callable which sets
           the default value in the callable"""
        env = DummyEnv(self.loc)
        gvars = env.Dictionary()
        newcom = scons_subst("test $PartialCallable $SOURCES $TARGETS", env,
                             target=self.MyNode('t'), source=self.MyNode('s'),
                             gvars=gvars)
        assert newcom == "test CallableWithDefault: partial s t", newcom

    def test_subst_partial_callable_with_no_default_expansion(self):
        """Test scons_subst():  expanding a functools.partial callable which sets
           the value for extraneous function argument"""
        env = DummyEnv(self.loc)
        gvars = env.Dictionary()
        newcom = scons_subst("test $PartialCallableNoDefault $SOURCES $TARGETS", env,
                             target=self.MyNode('t'), source=self.MyNode('s'),
                             gvars=gvars)
        assert newcom == "test CallableWithNoDefault: partialNoDefault s t", newcom

    def test_subst_attribute_errors(self):
        """Test scons_subst():  handling attribute errors"""
        env = DummyEnv(self.loc)
        try:
            class Foo:
                pass
            scons_subst('${foo.bar}', env, gvars={'foo':Foo()})
        except SCons.Errors.UserError as e:
            expect = [
                "AttributeError `bar' trying to evaluate `${foo.bar}'",
                "AttributeError `Foo instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
                "AttributeError `'Foo' instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
                "AttributeError `'Foo' object has no attribute 'bar'' trying to evaluate `${foo.bar}'",
            ]
            assert str(e) in expect, e
        else:
            raise AssertionError("did not catch expected UserError")

    def test_subst_syntax_errors(self):
        """Test scons_subst():  handling syntax errors"""
        env = DummyEnv(self.loc)
        try:
            scons_subst('$foo.bar.3.0', env)
        except SCons.Errors.UserError as e:
            expect = [
                # Python 2.5 to 3.9
                "SyntaxError `invalid syntax (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
                # Python 3.10 and later
                "SyntaxError `invalid syntax. Perhaps you forgot a comma? (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
            ]
            assert str(e) in expect, e
        else:
            raise AssertionError("did not catch expected UserError")

    def test_subst_balance_errors(self):
        """Test scons_subst():  handling syntax errors"""
        env = DummyEnv(self.loc)
        try:
            scons_subst('$(', env, mode=SUBST_SIG)
        except SCons.Errors.UserError as e:
            assert str(e) == "Unbalanced $(/$) in: $(", str(e)
        else:
            raise AssertionError("did not catch expected UserError")

        try:
            scons_subst('$)', env, mode=SUBST_SIG)
        except SCons.Errors.UserError as e:
            assert str(e) == "Unbalanced $(/$) in: $)", str(e)
        else:
            raise AssertionError("did not catch expected UserError")

    def test_subst_type_errors(self):
        """Test scons_subst():  handling type errors"""
        env = DummyEnv(self.loc)
        try:
            scons_subst("${NONE[2]}", env, gvars={'NONE':None})
        except SCons.Errors.UserError as e:
            expect = [
                # Python 2.7 and later
                "TypeError `'NoneType' object is not subscriptable' trying to evaluate `${NONE[2]}'",
                # Python 2.7 and later under Fedora
                "TypeError `'NoneType' object has no attribute '__getitem__'' trying to evaluate `${NONE[2]}'",
            ]
            assert str(e) in expect, e
        else:
            raise AssertionError("did not catch expected UserError")

        try:
            def func(a, b, c):
                pass
            scons_subst("${func(1)}", env, gvars={'func':func})
        except SCons.Errors.UserError as e:
            expect = [
                # Python 3.5 (and 3.x?)
                "TypeError `func() missing 2 required positional arguments: 'b' and 'c'' trying to evaluate `${func(1)}'",
                # Python 3.10
                "TypeError `scons_subst_TestCase.test_subst_type_errors.<locals>.func() missing 2 required positional arguments: 'b' and 'c'' trying to evaluate `${func(1)}'",
            ]
            assert str(e) in expect, repr(str(e))
        else:
            raise AssertionError("did not catch expected UserError")

    def test_subst_raw_function(self):
        """Test scons_subst():  fetch function with SUBST_RAW plus conv"""
        # Test that the combination of SUBST_RAW plus a pass-through
        # conversion routine allows us to fetch a function through the
        # dictionary.  CommandAction uses this to allow delayed evaluation
        # of $SPAWN variables.
        env = DummyEnv(self.loc)
        gvars = env.Dictionary()
        x = lambda x: x
        r = scons_subst("$CALLABLE1", env, mode=SUBST_RAW, conv=x, gvars=gvars)
        assert r is self.callable_object_1, repr(r)
        r = scons_subst("$CALLABLE1", env, mode=SUBST_RAW, gvars=gvars)
        assert r == 'callable-1', repr(r)

        # Test how we handle overriding the internal conversion routines.
        def s(obj):
            return obj

        n1 = self.MyNode('n1')
        env = DummyEnv({'NODE' : n1})
        gvars = env.Dictionary()
        node = scons_subst("$NODE", env, mode=SUBST_RAW, conv=s, gvars=gvars)
        assert node is n1, node
        node = scons_subst("$NODE", env, mode=SUBST_CMD, conv=s, gvars=gvars)
        assert node is n1, node
        node = scons_subst("$NODE", env, mode=SUBST_SIG, conv=s, gvars=gvars)
        assert node is n1, node

    def test_subst_overriding_gvars(self):
        """Test scons_subst():  supplying an overriding gvars dictionary"""
        env = DummyEnv({'XXX' : 'xxx'})
        result = scons_subst('$XXX', env, gvars=env.Dictionary())
        assert result == 'xxx', result
        result = scons_subst('$XXX', env, gvars={'XXX' : 'yyy'})
        assert result == 'yyy', result

class CLVar_TestCase(unittest.TestCase):
    def test_CLVar(self):
        """Test scons_subst() and scons_subst_list() with CLVar objects"""

        loc = {}
        loc['FOO'] = 'foo'
        loc['BAR'] = SCons.Util.CLVar('bar')
        loc['CALL'] = lambda target, source, env, for_signature: 'call'
        env = DummyEnv(loc)

        cmd = SCons.Util.CLVar("test $FOO $BAR $CALL test")

        newcmd = scons_subst(cmd, env, gvars=env.Dictionary())
        assert newcmd == ['test', 'foo', 'bar', 'call', 'test'], newcmd

        cmd_list = scons_subst_list(cmd, env, gvars=env.Dictionary())
        assert len(cmd_list) == 1, cmd_list
        assert cmd_list[0][0] == "test", cmd_list[0][0]
        assert cmd_list[0][1] == "foo", cmd_list[0][1]
        assert cmd_list[0][2] == "bar", cmd_list[0][2]
        assert cmd_list[0][3] == "call", cmd_list[0][3]
        assert cmd_list[0][4] == "test", cmd_list[0][4]

class scons_subst_list_TestCase(SubstTestCase):

    basic_cases = [
        "$TARGETS",
        [
            ["foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj"],
        ],

        "$SOURCES $NEWLINE $TARGETS",
        [
            ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.c", "before"],
            ["after", "foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj"],
        ],

        "$SOURCES$NEWLINE",
        [
            ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.cbefore"],
            ["after"],
        ],

        "foo$FFF",
        [
            ["fooGGG"],
        ],

        "foo${FFF}",
        [
            ["fooGGG"],
        ],

        "test ${SOURCES.attribute.attr1}",
        [
            ["test", "attr$1-blah with spaces.cpp", "attr$1-ack.cpp", "attr$1-ack.c"],
        ],

        "test ${SOURCES.attribute.attr2}",
        [
            ["test", "attr$2-blah with spaces.cpp", "attr$2-ack.cpp", "attr$2-ack.c"],
        ],

        "$DO --in=$FOO --out=$BAR",
        [
            ["do something", "--in=foo.in", "--out=bar with spaces.out"],
        ],

        # This test is now fixed, and works like it should.
        "$DO --in=$CRAZY --out=$BAR",
        [
            ["do something", "--in=crazy\nfile.in", "--out=bar with spaces.out"],
        ],

        # Try passing a list to scons_subst_list().
        [ "$SOURCES$NEWLINE", "$TARGETS", "This is a test"],
        [
            ["foo/blah with spaces.cpp", "/bar/ack.cpp", "../foo/ack.cbefore"],
            ["after", "foo/bar.exe", "/bar/baz with spaces.obj", "../foo/baz.obj", "This is a test"],
        ],

        # Test against a former bug in scons_subst_list().
        "$XXX$HHH",
        [
            ["GGGIII"],
        ],

        # Test double-dollar-sign behavior.
        "$$FFF$HHH",
        [
            ["$FFFIII"],
        ],

        # Test various combinations of strings, lists and functions.
        None,                   [[]],
        [None],                 [[]],
        '',                     [[]],
        [''],                   [[]],
        'x',                    [['x']],
        ['x'],                  [['x']],
        'x y',                  [['x', 'y']],
        ['x y'],                [['x y']],
        ['x', 'y'],             [['x', 'y']],
        '$N',                   [[]],
        ['$N'],                 [[]],
        '$X',                   [['x']],
        ['$X'],                 [['x']],
        '$Y',                   [['x']],
        ['$Y'],                 [['x']],
        #'$R',                   [[]],
        #['$R'],                 [[]],
        '$S',                   [['x', 'y']],
        '$S z',                 [['x', 'y', 'z']],
        ['$S'],                 [['x', 'y']],
        ['$S z'],               [['x', 'y z']],     # XXX - IS THIS BEST?
        ['$S', 'z'],            [['x', 'y', 'z']],
        '$LS',                  [['x y']],
        '$LS z',                [['x y', 'z']],
        ['$LS'],                [['x y']],
        ['$LS z'],              [['x y z']],
        ['$LS', 'z'],           [['x y', 'z']],
        '$L',                   [['x', 'y']],
        '$L z',                 [['x', 'y', 'z']],
        ['$L'],                 [['x', 'y']],
        ['$L z'],               [['x', 'y z']],     # XXX - IS THIS BEST?
        ['$L', 'z'],            [['x', 'y', 'z']],
        cs,                     [['cs']],
        [cs],                   [['cs']],
        cl,                     [['cl']],
        [cl],                   [['cl']],
        '$CS',                  [['cs']],
        ['$CS'],                [['cs']],
        '$CL',                  [['cl']],
        ['$CL'],                [['cl']],

        # Various uses of UserString.
        collections.UserString('x'),         [['x']],
        [collections.UserString('x')],       [['x']],
        collections.UserString('$X'),        [['x']],
        [collections.UserString('$X')],      [['x']],
        collections.UserString('$US'),       [['us']],
        [collections.UserString('$US')],     [['us']],
        '$US',                              [['us']],
        ['$US'],                            [['us']],

        # Test function calls within ${}.
        '$FUNCCALL',            [['a', 'xc', 'b']],

        # Test handling of newlines in white space.
        'foo\nbar',             [['foo'], ['bar']],
        'foo\n\nbar',           [['foo'], ['bar']],
        'foo \n \n bar',        [['foo'], ['bar']],
        'foo \nmiddle\n bar',   [['foo'], ['middle'], ['bar']],

        # Bug reported by Christoph Wiedemann.
        cvt('$xxx/bin'),        [['/bin']],

        # Test variables smooshed together with different prefixes.
        'foo$AAA',              [['fooa']],
        '<$AAA',                [['<', 'a']],
        '>$AAA',                [['>', 'a']],
        '|$AAA',                [['|', 'a']],

        # Test callables that don't match our calling arguments.
        '$CALLABLE2',            [['callable-2']],

        # Test handling of quotes.
        # XXX Find a way to handle this in the future.
        #'aaa "bbb ccc" ddd',    [['aaa', 'bbb ccc', 'ddd']],

        '${_defines(DEFS)}',     [['Q1="q1"', 'Q2="a"']],
    ]

    def test_scons_subst_list(self):
        """Test scons_subst_list():  basic substitution"""
        def convert_lists(expect):
            return [list(map(cvt, l)) for l in expect]
        return self.basic_comparisons(scons_subst_list, convert_lists)

    subst_list_cases = [
        "test $xxx",
            [["test"]],
            [["test"]],
            [["test"]],

        "test $($xxx$)",
            [["test", "$($)"]],
            [["test"]],
            [["test"]],

        "test $( $xxx $)",
            [["test", "$(", "$)"]],
            [["test"]],
            [["test"]],

        "$AAA ${AAA}A $BBBB $BBB",
            [["a", "aA", "b"]],
            [["a", "aA", "b"]],
            [["a", "aA", "b"]],

        "$RECURSE",
            [["foo", "bar"]],
            [["foo", "bar"]],
            [["foo", "bar"]],

        "$RRR",
            [["foo", "bar"]],
            [["foo", "bar"]],
            [["foo", "bar"]],

        # Verify what happens with no target or source nodes.
        "$TARGET $SOURCES",
            [[]],
            [[]],
            [[]],

        "$TARGETS $SOURCE",
            [[]],
            [[]],
            [[]],

        # Various test refactored from ActionTests.py
        "${LIST}",
            [['This', 'is', '$(', '$)', 'test']],
            [['This', 'is', 'test']],
            [['This', 'is', 'test']],

        ["|", "$(", "$AAA", "|", "$BBB", "$)", "|", "$CCC", 1],
            [["|", "$(", "a", "|", "b", "$)", "|", "c", "1"]],
            [["|", "a", "|", "b", "|", "c", "1"]],
            [["|", "|", "c", "1"]],
    ]

    def test_subst_env(self):
        """Test scons_subst_list():  expansion dictionary"""
        # The expansion dictionary no longer comes from the construction
        # environment automatically.
        env = DummyEnv()
        s = scons_subst_list('$AAA', env)
        assert s == [[]], s

    def test_subst_target_source(self):
        """Test scons_subst_list():  target= and source= arguments"""
        env = DummyEnv(self.loc)
        gvars = env.Dictionary()
        t1 = self.MyNode('t1')
        t2 = self.MyNode('t2')
        s1 = self.MyNode('s1')
        s2 = self.MyNode('s2')
        result = scons_subst_list("$TARGET $SOURCES", env,
                                  target=[t1, t2],
                                  source=[s1, s2],
                                  gvars=gvars)
        assert result == [['t1', 's1', 's2']], result
        result = scons_subst_list("$TARGET $SOURCES", env,
                                  target=[t1, t2],
                                  source=[s1, s2],
                                  gvars={})
        assert result == [['t1', 's1', 's2']], result

        # Test interpolating a callable.
        _t = DummyNode('t')
        _s = DummyNode('s')
        cmd_list = scons_subst_list("testing $CMDGEN1 $TARGETS $SOURCES",
                                    env, target=_t, source=_s,
                                    gvars=gvars)
        assert cmd_list == [['testing', 'foo', 'bar with spaces.out', 't', 's']], cmd_list

    def test_subst_escape(self):
        """Test scons_subst_list():  escape functionality"""
        env = DummyEnv(self.loc)
        gvars = env.Dictionary()
        def escape_func(foo):
            return '**' + foo + '**'
        cmd_list = scons_subst_list("abc $LITERALS xyz", env, gvars=gvars)
        assert cmd_list == [['abc',
                             'foo\nwith\nnewlines',
                             'bar\nwith\nnewlines',
                             'xyz']], cmd_list
        c = cmd_list[0][0].escape(escape_func)
        assert c == 'abc', c
        c = cmd_list[0][1].escape(escape_func)
        assert c == '**foo\nwith\nnewlines**', c
        c = cmd_list[0][2].escape(escape_func)
        assert c == '**bar\nwith\nnewlines**', c
        c = cmd_list[0][3].escape(escape_func)
        assert c == 'xyz', c

        # We used to treat literals smooshed together like the whole
        # thing was literal and escape it as a unit.  The commented-out
        # asserts below are in case we ever have to find a way to
        # resurrect that functionality in some way.
        cmd_list = scons_subst_list("abc${LITERALS}xyz", env, gvars=gvars)
        c = cmd_list[0][0].escape(escape_func)
        #assert c == '**abcfoo\nwith\nnewlines**', c
        assert c == 'abcfoo\nwith\nnewlines', c
        c = cmd_list[0][1].escape(escape_func)
        #assert c == '**bar\nwith\nnewlinesxyz**', c
        assert c == 'bar\nwith\nnewlinesxyz', c

        _t = DummyNode('t')

        cmd_list = scons_subst_list('echo "target: $TARGET"', env,
                                    target=_t, gvars=gvars)
        c = cmd_list[0][0].escape(escape_func)
        assert c == 'echo', c
        c = cmd_list[0][1].escape(escape_func)
        assert c == '"target:', c
        c = cmd_list[0][2].escape(escape_func)
        assert c == 't"', c

    def test_subst_SUBST_modes(self):
        """Test scons_subst_list():  SUBST_* modes"""
        env = DummyEnv(self.loc)
        subst_list_cases = self.subst_list_cases[:]
        gvars = env.Dictionary()

        r = scons_subst_list("$TARGET $SOURCES", env, mode=SUBST_RAW, gvars=gvars)
        assert r == [[]], r

        failed = 0
        while subst_list_cases:
            input, eraw, ecmd, esig = subst_list_cases[:4]
            result = scons_subst_list(input, env, mode=SUBST_RAW, gvars=gvars)
            if result != eraw:
                if failed == 0: print()
                print("    input %s => RAW %s did not match %s" % (repr(input), repr(result), repr(eraw)))
                failed = failed + 1
            result = scons_subst_list(input, env, mode=SUBST_CMD, gvars=gvars)
            if result != ecmd:
                if failed == 0: print()
                print("    input %s => CMD %s did not match %s" % (repr(input), repr(result), repr(ecmd)))
                failed = failed + 1
            result = scons_subst_list(input, env, mode=SUBST_SIG, gvars=gvars)
            if result != esig:
                if failed == 0: print()
                print("    input %s => SIG %s did not match %s" % (repr(input), repr(result), repr(esig)))
                failed = failed + 1
            del subst_list_cases[:4]
        assert failed == 0, "%d subst() mode cases failed" % failed

    def test_subst_attribute_errors(self):
        """Test scons_subst_list():  handling attribute errors"""
        env = DummyEnv()
        try:
            class Foo:
                pass
            scons_subst_list('${foo.bar}', env, gvars={'foo':Foo()})
        except SCons.Errors.UserError as e:
            expect = [
                "AttributeError `bar' trying to evaluate `${foo.bar}'",
                "AttributeError `Foo instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
                "AttributeError `'Foo' instance has no attribute 'bar'' trying to evaluate `${foo.bar}'",
                "AttributeError `'Foo' object has no attribute 'bar'' trying to evaluate `${foo.bar}'",
            ]
            assert str(e) in expect, e
        else:
            raise AssertionError("did not catch expected UserError")

    def test_subst_syntax_errors(self):
        """Test scons_subst_list():  handling syntax errors"""
        env = DummyEnv()
        try:
            scons_subst_list('$foo.bar.3.0', env)
        except SCons.Errors.UserError as e:
            expect = [
                # Python 2.5 to 3.9
                "SyntaxError `invalid syntax (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
                # Python 3.10 and later
                "SyntaxError `invalid syntax. Perhaps you forgot a comma? (<string>, line 1)' trying to evaluate `$foo.bar.3.0'",
            ]
            assert str(e) in expect, e
        else:
            raise AssertionError("did not catch expected SyntaxError")

    def test_subst_raw_function(self):
        """Test scons_subst_list():  fetch function with SUBST_RAW plus conv"""
        # Test that the combination of SUBST_RAW plus a pass-through
        # conversion routine allows us to fetch a function through the
        # dictionary.
        env = DummyEnv(self.loc)
        gvars = env.Dictionary()
        x = lambda x: x
        r = scons_subst_list("$CALLABLE2", env, mode=SUBST_RAW, conv=x, gvars=gvars)
        assert r == [[self.callable_object_2]], repr(r)
        r = scons_subst_list("$CALLABLE2", env, mode=SUBST_RAW, gvars=gvars)
        assert r == [['callable-2']], repr(r)

    def test_subst_list_overriding_gvars(self):
        """Test scons_subst_list():  overriding conv()"""
        env = DummyEnv()
        def s(obj):
            return obj

        n1 = self.MyNode('n1')
        env = DummyEnv({'NODE' : n1})
        gvars=env.Dictionary()
        node = scons_subst_list("$NODE", env, mode=SUBST_RAW, conv=s, gvars=gvars)
        assert node == [[n1]], node
        node = scons_subst_list("$NODE", env, mode=SUBST_CMD, conv=s, gvars=gvars)
        assert node == [[n1]], node
        node = scons_subst_list("$NODE", env, mode=SUBST_SIG, conv=s, gvars=gvars)
        assert node == [[n1]], node

    def test_subst_list_overriding_gvars2(self):
        """Test scons_subst_list():  supplying an overriding gvars dictionary"""
        env = DummyEnv({'XXX' : 'xxx'})
        result = scons_subst_list('$XXX', env, gvars=env.Dictionary())
        assert result == [['xxx']], result
        result = scons_subst_list('$XXX', env, gvars={'XXX' : 'yyy'})
        assert result == [['yyy']], result

class scons_subst_once_TestCase(unittest.TestCase):

    loc = {
        'CCFLAGS'           : '-DFOO',
        'ONE'               : 1,
        'RECURSE'           : 'r $RECURSE r',
        'LIST'              : ['a', 'b', 'c'],
    }

    basic_cases = [
        '$CCFLAGS -DBAR',
        'OTHER_KEY',
        '$CCFLAGS -DBAR',

        '$CCFLAGS -DBAR',
        'CCFLAGS',
        '-DFOO -DBAR',

        'x $ONE y',
        'ONE',
        'x 1 y',

        'x $RECURSE y',
        'RECURSE',
        'x r $RECURSE r y',

        '$LIST',
        'LIST',
        'a b c',

        ['$LIST'],
        'LIST',
        ['a', 'b', 'c'],

        ['x', '$LIST', 'y'],
        'LIST',
        ['x', 'a', 'b', 'c', 'y'],

        ['x', 'x $LIST y', 'y'],
        'LIST',
        ['x', 'x a b c y', 'y'],

        ['x', 'x $CCFLAGS y', 'y'],
        'LIST',
        ['x', 'x $CCFLAGS y', 'y'],

        ['x', 'x $RECURSE y', 'y'],
        'LIST',
        ['x', 'x $RECURSE y', 'y'],
    ]

    def test_subst_once(self):
        """Test the scons_subst_once() function"""
        env = DummyEnv(self.loc)
        cases = self.basic_cases[:]

        failed = 0
        while cases:
            input, key, expect = cases[:3]
            result = scons_subst_once(input, env, key)
            if result != expect:
                if failed == 0: print()
                print("    input %s (%s) => %s did not match %s" % (repr(input), repr(key), repr(result), repr(expect)))
                failed = failed + 1
            del cases[:3]
        assert failed == 0, "%d subst() cases failed" % failed

class quote_spaces_TestCase(unittest.TestCase):
    def test_quote_spaces(self):
        """Test the quote_spaces() method..."""
        q = quote_spaces('x')
        assert q == 'x', q

        q = quote_spaces('x x')
        assert q == '"x x"', q

        q = quote_spaces('x\tx')
        assert q == '"x\tx"', q

    class Node:
        def __init__(self, name, children=[]):
            self.children = children
            self.name = name
        def __str__(self):
            return self.name
        def exists(self):
            return 1
        def rexists(self):
            return 1
        def has_builder(self):
            return 1
        def has_explicit_builder(self):
            return 1
        def side_effect(self):
            return 1
        def precious(self):
            return 1
        def always_build(self):
            return 1
        def current(self):
            return 1

class LiteralTestCase(unittest.TestCase):
    def test_Literal(self):
        """Test the Literal() function."""
        input_list = [ '$FOO', Literal('$BAR') ]
        gvars = { 'FOO' : 'BAZ', 'BAR' : 'BLAT' }

        def escape_func(cmd):
            return '**' + cmd + '**'

        cmd_list = scons_subst_list(input_list, None, gvars=gvars)
        cmd_list = escape_list(cmd_list[0], escape_func)
        assert cmd_list == ['BAZ', '**$BAR**'], cmd_list

    def test_LiteralEqualsTest(self):
        """Test that Literals compare for equality properly"""
        assert Literal('a literal') == Literal('a literal')
        assert Literal('a literal') != Literal('b literal')

class SpecialAttrWrapperTestCase(unittest.TestCase):
    def test_SpecialAttrWrapper(self):
        """Test the SpecialAttrWrapper() function."""
        input_list = [ '$FOO', SpecialAttrWrapper('$BAR', 'BLEH') ]
        gvars = { 'FOO' : 'BAZ', 'BAR' : 'BLAT' }

        def escape_func(cmd):
            return '**' + cmd + '**'

        cmd_list = scons_subst_list(input_list, None, gvars=gvars)
        cmd_list = escape_list(cmd_list[0], escape_func)
        assert cmd_list == ['BAZ', '**$BAR**'], cmd_list

        cmd_list = scons_subst_list(input_list, None, mode=SUBST_SIG, gvars=gvars)
        cmd_list = escape_list(cmd_list[0], escape_func)
        assert cmd_list == ['BAZ', '**BLEH**'], cmd_list

class subst_dict_TestCase(unittest.TestCase):
    def test_subst_dict(self):
        """Test substituting dictionary values in an Action
        """
        t = DummyNode('t')
        s = DummyNode('s')
        d = subst_dict(target=t, source=s)
        assert str(d['TARGETS'][0]) == 't', d['TARGETS']
        assert str(d['TARGET']) == 't', d['TARGET']
        assert str(d['SOURCES'][0]) == 's', d['SOURCES']
        assert str(d['SOURCE']) == 's', d['SOURCE']

        t1 = DummyNode('t1')
        t2 = DummyNode('t2')
        s1 = DummyNode('s1')
        s2 = DummyNode('s2')
        d = subst_dict(target=[t1, t2], source=[s1, s2])
        TARGETS = sorted([str(x) for x in d['TARGETS']])
        assert TARGETS == ['t1', 't2'], d['TARGETS']
        assert str(d['TARGET']) == 't1', d['TARGET']
        SOURCES = sorted([str(x) for x in d['SOURCES']])
        assert SOURCES == ['s1', 's2'], d['SOURCES']
        assert str(d['SOURCE']) == 's1', d['SOURCE']

        class V:
            # Fake Value node with no rfile() method.
            def __init__(self, name):
                self.name = name
            def __str__(self):
                return 'v-'+self.name
            def get_subst_proxy(self):
                return self

        class N(V):
            def rfile(self):
                return self.__class__('rstr-' + self.name)

        t3 = N('t3')
        t4 = DummyNode('t4')
        t5 = V('t5')
        s3 = DummyNode('s3')
        s4 = N('s4')
        s5 = V('s5')
        d = subst_dict(target=[t3, t4, t5], source=[s3, s4, s5])
        TARGETS = sorted([str(x) for x in d['TARGETS']])
        assert TARGETS == ['t4', 'v-t3', 'v-t5'], TARGETS
        SOURCES = sorted([str(x) for x in d['SOURCES']])
        assert SOURCES == ['s3', 'v-rstr-s4', 'v-s5'], SOURCES

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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/UtilTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import functools
import io
import os
import sys
import unittest
import unittest.mock
import hashlib
import warnings
from collections import UserDict, UserList, UserString, namedtuple

import TestCmd

import SCons.Errors
import SCons.compat
from SCons.Util import (
    ALLOWED_HASH_FORMATS,
    AddPathIfNotExists,
    AppendPath,
    CLVar,
    LogicalLines,
    NodeList,
    PrependPath,
    Proxy,
    Selector,
    WhereIs,
    _attempt_init_of_python_3_9_hash_object,
    _attempt_get_hash_function,
    _get_hash_object,
    _set_allowed_viable_default_hashes,
    adjustixes,
    containsAll,
    containsAny,
    containsOnly,
    dictify,
    display,
    flatten,
    get_env_bool,
    get_environment_var,
    get_native_path,
    get_os_env_bool,
    hash_collect,
    hash_signature,
    is_Dict,
    is_List,
    is_String,
    is_Tuple,
    print_tree,
    render_tree,
    set_hash_format,
    silent_intern,
    splitext,
    to_String,
    to_bytes,
    to_str,
)

# These Util classes have no unit tests. Some don't make sense to test?
# DisplayEngine, Delegate, MethodWrapper, UniqueList, Unbuffered, Null, NullSeq


class OutBuffer:
    def __init__(self):
        self.buffer = ""

    def write(self, str):
        self.buffer = self.buffer + str


class dictifyTestCase(unittest.TestCase):
    def test_dictify(self):
        """Test the dictify() function"""
        r = dictify(['a', 'b', 'c'], [1, 2, 3])
        assert r == {'a': 1, 'b': 2, 'c': 3}, r

        r = {}
        dictify(['a'], [1], r)
        dictify(['b'], [2], r)
        dictify(['c'], [3], r)
        assert r == {'a': 1, 'b': 2, 'c': 3}, r


class UtilTestCase(unittest.TestCase):
    def test_splitext(self):
        assert splitext('foo') == ('foo', '')
        assert splitext('foo.bar') == ('foo', '.bar')
        assert splitext(os.path.join('foo.bar', 'blat')) == (os.path.join('foo.bar', 'blat'), '')

    class Node:
        def __init__(self, name, children=[]):
            self.children = children
            self.name = name
            self.nocache = None

        def __str__(self):
            return self.name

        def exists(self):
            return 1

        def rexists(self):
            return 1

        def has_builder(self):
            return 1

        def has_explicit_builder(self):
            return 1

        def side_effect(self):
            return 1

        def precious(self):
            return 1

        def always_build(self):
            return 1

        def is_up_to_date(self):
            return 1

        def noclean(self):
            return 1

    def tree_case_1(self):
        """Fixture for the render_tree() and print_tree() tests."""
        windows_h = self.Node("windows.h")
        stdlib_h = self.Node("stdlib.h")
        stdio_h = self.Node("stdio.h")
        bar_c = self.Node("bar.c", [stdlib_h, windows_h])
        bar_o = self.Node("bar.o", [bar_c])
        foo_c = self.Node("foo.c", [stdio_h])
        foo_o = self.Node("foo.o", [foo_c])
        foo = self.Node("foo", [foo_o, bar_o])

        expect = """\
+-foo
  +-foo.o
  | +-foo.c
  |   +-stdio.h
  +-bar.o
    +-bar.c
      +-stdlib.h
      +-windows.h
"""

        lines = expect.split('\n')[:-1]
        lines = ['[E BSPACN ]' + l for l in lines]
        withtags = '\n'.join(lines) + '\n'

        return foo, expect, withtags

    def tree_case_2(self, prune=1):
        """Fixture for the render_tree() and print_tree() tests."""

        types_h = self.Node('types.h')
        malloc_h = self.Node('malloc.h')
        stdlib_h = self.Node('stdlib.h', [types_h, malloc_h])
        bar_h = self.Node('bar.h', [stdlib_h])
        blat_h = self.Node('blat.h', [stdlib_h])
        blat_c = self.Node('blat.c', [blat_h, bar_h])
        blat_o = self.Node('blat.o', [blat_c])

        expect = """\
+-blat.o
  +-blat.c
    +-blat.h
    | +-stdlib.h
    |   +-types.h
    |   +-malloc.h
    +-bar.h
"""
        if prune:
            expect += """      +-[stdlib.h]
"""
        else:
            expect += """      +-stdlib.h
        +-types.h
        +-malloc.h
"""

        lines = expect.split('\n')[:-1]
        lines = ['[E BSPACN ]' + l for l in lines]
        withtags = '\n'.join(lines) + '\n'

        return blat_o, expect, withtags

    def test_render_tree(self):
        """Test the render_tree() function"""

        def get_children(node):
            return node.children

        node, expect, withtags = self.tree_case_1()
        actual = render_tree(node, get_children)
        assert expect == actual, (expect, actual)

        node, expect, withtags = self.tree_case_2()
        actual = render_tree(node, get_children, 1)
        assert expect == actual, (expect, actual)

        # Ensure that we can call render_tree on the same Node
        # again. This wasn't possible in version 2.4.1 and earlier
        # due to a bug in render_tree (visited was set to {} as default
        # parameter)
        actual = render_tree(node, get_children, 1)
        assert expect == actual, (expect, actual)

    def test_print_tree(self):
        """Test the print_tree() function"""

        def get_children(node):
            return node.children

        save_stdout = sys.stdout

        try:
            node, expect, withtags = self.tree_case_1()

            IOStream = io.StringIO
            sys.stdout = IOStream()
            print_tree(node, get_children)
            actual = sys.stdout.getvalue()
            assert expect == actual, (expect, actual)

            sys.stdout = IOStream()
            print_tree(node, get_children, showtags=1)
            actual = sys.stdout.getvalue()
            assert withtags == actual, (withtags, actual)

            # Test that explicitly setting prune to zero works
            # the same as the default (see above)
            node, expect, withtags = self.tree_case_2(prune=0)

            sys.stdout = IOStream()
            print_tree(node, get_children, 0)
            actual = sys.stdout.getvalue()
            assert expect == actual, (expect, actual)

            sys.stdout = IOStream()
            print_tree(node, get_children, 0, showtags=1)
            actual = sys.stdout.getvalue()
            assert withtags == actual, (withtags, actual)

            # Test output with prune=1
            node, expect, withtags = self.tree_case_2(prune=1)

            sys.stdout = IOStream()
            print_tree(node, get_children, 1)
            actual = sys.stdout.getvalue()
            assert expect == actual, (expect, actual)

            # Ensure that we can call print_tree on the same Node
            # again. This wasn't possible in version 2.4.1 and earlier
            # due to a bug in print_tree (visited was set to {} as default
            # parameter)
            sys.stdout = IOStream()
            print_tree(node, get_children, 1)
            actual = sys.stdout.getvalue()
            assert expect == actual, (expect, actual)

            sys.stdout = IOStream()
            print_tree(node, get_children, 1, showtags=1)
            actual = sys.stdout.getvalue()
            assert withtags == actual, (withtags, actual)
        finally:
            sys.stdout = save_stdout

    def test_is_Dict(self):
        assert is_Dict({})
        assert is_Dict(UserDict())
        try:
            class mydict(dict):
                pass
        except TypeError:
            pass
        else:
            assert is_Dict(mydict({}))
        assert not is_Dict([])
        assert not is_Dict(())
        assert not is_Dict("")


    def test_is_List(self):
        assert is_List([])
        assert is_List(UserList())
        try:
            class mylist(list):
                pass
        except TypeError:
            pass
        else:
            assert is_List(mylist([]))
        assert not is_List(())
        assert not is_List({})
        assert not is_List("")

    def test_is_String(self):
        assert is_String("")
        assert is_String(UserString(''))
        try:
            class mystr(str):
                pass
        except TypeError:
            pass
        else:
            assert is_String(mystr(''))
        assert not is_String({})
        assert not is_String([])
        assert not is_String(())

    def test_is_Tuple(self):
        assert is_Tuple(())
        try:
            class mytuple(tuple):
                pass
        except TypeError:
            pass
        else:
            assert is_Tuple(mytuple(()))
        assert not is_Tuple([])
        assert not is_Tuple({})
        assert not is_Tuple("")

    def test_to_Bytes(self):
        """ Test the to_Bytes method"""
        self.assertEqual(to_bytes('Hello'),
                         bytearray('Hello', 'utf-8'),
                         "Check that to_bytes creates byte array when presented with non byte string.")

    def test_to_String(self):
        """Test the to_String() method."""
        assert to_String(1) == "1", to_String(1)
        assert to_String([1, 2, 3]) == str([1, 2, 3]), to_String([1, 2, 3])
        assert to_String("foo") == "foo", to_String("foo")
        assert to_String(None) == 'None'
        # test low level string converters too
        assert to_str(None) == 'None'
        assert to_bytes(None) == b'None'

        s1 = UserString('blah')
        assert to_String(s1) == s1, s1
        assert to_String(s1) == 'blah', s1

        class Derived(UserString):
            pass

        s2 = Derived('foo')
        assert to_String(s2) == s2, s2
        assert to_String(s2) == 'foo', s2


    def test_WhereIs(self):
        test = TestCmd.TestCmd(workdir='')

        sub1_xxx_exe = test.workpath('sub1', 'xxx.exe')
        sub2_xxx_exe = test.workpath('sub2', 'xxx.exe')
        sub3_xxx_exe = test.workpath('sub3', 'xxx.exe')
        sub4_xxx_exe = test.workpath('sub4', 'xxx.exe')

        test.subdir('subdir', 'sub1', 'sub2', 'sub3', 'sub4')

        if sys.platform != 'win32':
            test.write(sub1_xxx_exe, "\n")

        os.mkdir(sub2_xxx_exe)

        test.write(sub3_xxx_exe, "\n")
        os.chmod(sub3_xxx_exe, 0o777)

        test.write(sub4_xxx_exe, "\n")
        os.chmod(sub4_xxx_exe, 0o777)

        env_path = os.environ['PATH']

        try:
            pathdirs_1234 = [test.workpath('sub1'),
                             test.workpath('sub2'),
                             test.workpath('sub3'),
                             test.workpath('sub4'),
                             ] + env_path.split(os.pathsep)

            pathdirs_1243 = [test.workpath('sub1'),
                             test.workpath('sub2'),
                             test.workpath('sub4'),
                             test.workpath('sub3'),
                             ] + env_path.split(os.pathsep)

            os.environ['PATH'] = os.pathsep.join(pathdirs_1234)
            wi = WhereIs('xxx.exe')
            assert wi == test.workpath(sub3_xxx_exe), wi
            wi = WhereIs('xxx.exe', pathdirs_1243)
            assert wi == test.workpath(sub4_xxx_exe), wi
            wi = WhereIs('xxx.exe', os.pathsep.join(pathdirs_1243))
            assert wi == test.workpath(sub4_xxx_exe), wi

            wi = WhereIs('xxx.exe', reject=sub3_xxx_exe)
            assert wi == test.workpath(sub4_xxx_exe), wi
            wi = WhereIs('xxx.exe', pathdirs_1243, reject=sub3_xxx_exe)
            assert wi == test.workpath(sub4_xxx_exe), wi

            os.environ['PATH'] = os.pathsep.join(pathdirs_1243)
            wi = WhereIs('xxx.exe')
            assert wi == test.workpath(sub4_xxx_exe), wi
            wi = WhereIs('xxx.exe', pathdirs_1234)
            assert wi == test.workpath(sub3_xxx_exe), wi
            wi = WhereIs('xxx.exe', os.pathsep.join(pathdirs_1234))
            assert wi == test.workpath(sub3_xxx_exe), wi

            if sys.platform == 'win32':
                wi = WhereIs('xxx', pathext='')
                assert wi is None, wi

                wi = WhereIs('xxx', pathext='.exe')
                assert wi == test.workpath(sub4_xxx_exe), wi

                wi = WhereIs('xxx', path=pathdirs_1234, pathext='.BAT;.EXE')
                assert wi.lower() == test.workpath(sub3_xxx_exe).lower(), wi

                # Test that we return a normalized path even when
                # the path contains forward slashes.
                forward_slash = test.workpath('') + '/sub3'
                wi = WhereIs('xxx', path=forward_slash, pathext='.EXE')
                assert wi.lower() == test.workpath(sub3_xxx_exe).lower(), wi

            del os.environ['PATH']
            wi = WhereIs('xxx.exe')
            assert wi is None, wi

        finally:
            os.environ['PATH'] = env_path

    def test_get_env_var(self):
        """Testing get_environment_var()."""
        assert get_environment_var("$FOO") == "FOO", get_environment_var("$FOO")
        assert get_environment_var("${BAR}") == "BAR", get_environment_var("${BAR}")
        assert get_environment_var("$FOO_BAR1234") == "FOO_BAR1234", get_environment_var("$FOO_BAR1234")
        assert get_environment_var("${BAR_FOO1234}") == "BAR_FOO1234", get_environment_var("${BAR_FOO1234}")
        assert get_environment_var("${BAR}FOO") is None, get_environment_var("${BAR}FOO")
        assert get_environment_var("$BAR ") is None, get_environment_var("$BAR ")
        assert get_environment_var("FOO$BAR") is None, get_environment_var("FOO$BAR")
        assert get_environment_var("$FOO[0]") is None, get_environment_var("$FOO[0]")
        assert get_environment_var("${some('complex expression')}") is None, get_environment_var(
            "${some('complex expression')}")

    def test_Proxy(self):
        """Test generic Proxy class."""

        class Subject:
            def foo(self):
                return 1

            def bar(self):
                return 2

        s = Subject()
        s.baz = 3

        class ProxyTest(Proxy):
            def bar(self):
                return 4

        p = ProxyTest(s)

        assert p.foo() == 1, p.foo()
        assert p.bar() == 4, p.bar()
        assert p.baz == 3, p.baz

        p.baz = 5
        s.baz = 6

        assert p.baz == 5, p.baz
        assert p.get() == s, p.get()

    def test_display(self):
        old_stdout = sys.stdout
        sys.stdout = OutBuffer()
        display("line1")
        display.set_mode(0)
        display("line2")
        display.set_mode(1)
        display("line3")
        display("line4\n", append_newline=0)
        display.set_mode(0)
        display("dont print1")
        display("dont print2\n", append_newline=0)
        display.set_mode(1)
        assert sys.stdout.buffer == "line1\nline3\nline4\n"
        sys.stdout = old_stdout

    def test_get_native_path(self):
        """Test the get_native_path() function."""
        import tempfile
        f, filename = tempfile.mkstemp(text=True)
        os.close(f)
        data = '1234567890 ' + filename
        try:
            with open(filename, 'w') as f:
                f.write(data)
            with open(get_native_path(filename), 'r') as f:
                assert f.read() == data
        finally:
            try:
                os.unlink(filename)
            except OSError:
                pass

    def test_PrependPath(self):
        """Test prepending to a path"""
        p1 = r'C:\dir\num\one;C:\dir\num\two'
        p2 = r'C:\mydir\num\one;C:\mydir\num\two'
        # have to include the pathsep here so that the test will work on UNIX too.
        p1 = PrependPath(p1, r'C:\dir\num\two', sep=';')
        p1 = PrependPath(p1, r'C:\dir\num\three', sep=';')
        assert p1 == r'C:\dir\num\three;C:\dir\num\two;C:\dir\num\one', p1

        p2 = PrependPath(p2, r'C:\mydir\num\three', sep=';')
        p2 = PrependPath(p2, r'C:\mydir\num\one', sep=';')
        assert p2 == r'C:\mydir\num\one;C:\mydir\num\three;C:\mydir\num\two', p2

        # check (only) first one is kept if there are dupes in new
        p3 = r'C:\dir\num\one'
        p3 = PrependPath(p3, r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\two', sep=';')
        assert p3 == r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\one', p3

    def test_AppendPath(self):
        """Test appending to a path."""
        p1 = r'C:\dir\num\one;C:\dir\num\two'
        p2 = r'C:\mydir\num\one;C:\mydir\num\two'
        # have to include the pathsep here so that the test will work on UNIX too.
        p1 = AppendPath(p1, r'C:\dir\num\two', sep=';')
        p1 = AppendPath(p1, r'C:\dir\num\three', sep=';')
        assert p1 == r'C:\dir\num\one;C:\dir\num\two;C:\dir\num\three', p1

        p2 = AppendPath(p2, r'C:\mydir\num\three', sep=';')
        p2 = AppendPath(p2, r'C:\mydir\num\one', sep=';')
        assert p2 == r'C:\mydir\num\two;C:\mydir\num\three;C:\mydir\num\one', p2

        # check (only) last one is kept if there are dupes in new
        p3 = r'C:\dir\num\one'
        p3 = AppendPath(p3, r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\two', sep=';')
        assert p3 == r'C:\dir\num\one;C:\dir\num\three;C:\dir\num\two', p3

    def test_PrependPathPreserveOld(self):
        """Test prepending to a path while preserving old paths"""
        p1 = r'C:\dir\num\one;C:\dir\num\two'
        # have to include the pathsep here so that the test will work on UNIX too.
        p1 = PrependPath(p1, r'C:\dir\num\two', sep=';', delete_existing=0)
        p1 = PrependPath(p1, r'C:\dir\num\three', sep=';')
        assert p1 == r'C:\dir\num\three;C:\dir\num\one;C:\dir\num\two', p1

    def test_AppendPathPreserveOld(self):
        """Test appending to a path while preserving old paths"""
        p1 = r'C:\dir\num\one;C:\dir\num\two'
        # have to include the pathsep here so that the test will work on UNIX too.
        p1 = AppendPath(p1, r'C:\dir\num\one', sep=';', delete_existing=0)
        p1 = AppendPath(p1, r'C:\dir\num\three', sep=';')
        assert p1 == r'C:\dir\num\one;C:\dir\num\two;C:\dir\num\three', p1

    def test_addPathIfNotExists(self):
        """Test the AddPathIfNotExists() function"""
        env_dict = {'FOO': os.path.normpath('/foo/bar') + os.pathsep + \
                           os.path.normpath('/baz/blat'),
                    'BAR': os.path.normpath('/foo/bar') + os.pathsep + \
                           os.path.normpath('/baz/blat'),
                    'BLAT': [os.path.normpath('/foo/bar'),
                             os.path.normpath('/baz/blat')]}
        AddPathIfNotExists(env_dict, 'FOO', os.path.normpath('/foo/bar'))
        AddPathIfNotExists(env_dict, 'BAR', os.path.normpath('/bar/foo'))
        AddPathIfNotExists(env_dict, 'BAZ', os.path.normpath('/foo/baz'))
        AddPathIfNotExists(env_dict, 'BLAT', os.path.normpath('/baz/blat'))
        AddPathIfNotExists(env_dict, 'BLAT', os.path.normpath('/baz/foo'))

        assert env_dict['FOO'] == os.path.normpath('/foo/bar') + os.pathsep + \
               os.path.normpath('/baz/blat'), env_dict['FOO']
        assert env_dict['BAR'] == os.path.normpath('/bar/foo') + os.pathsep + \
               os.path.normpath('/foo/bar') + os.pathsep + \
               os.path.normpath('/baz/blat'), env_dict['BAR']
        assert env_dict['BAZ'] == os.path.normpath('/foo/baz'), env_dict['BAZ']
        assert env_dict['BLAT'] == [os.path.normpath('/baz/foo'),
                                    os.path.normpath('/foo/bar'),
                                    os.path.normpath('/baz/blat')], env_dict['BLAT']

    def test_CLVar(self):
        """Test the command-line construction variable class"""

        # the default value should be an empty list
        d = CLVar()
        assert isinstance(d, CLVar), type(d)
        assert d.data == [], d.data
        assert str(d) == '', str(d)

        # input to CLVar is a string - should be split
        f = CLVar('aa bb')

        r = f + 'cc dd'
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', 'cc', 'dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + ' cc dd'
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', 'cc', 'dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + ['cc dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', 'cc dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + [' cc dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', ' cc dd'], r.data
        assert str(r) == 'aa bb  cc dd', str(r)

        r = f + ['cc', 'dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', 'cc', 'dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + [' cc', 'dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', ' cc', 'dd'], r.data
        assert str(r) == 'aa bb  cc dd', str(r)

        # input to CLVar is a list of one string, should not be split
        f = CLVar(['aa bb'])

        r = f + 'cc dd'
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa bb', 'cc', 'dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + ' cc dd'
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa bb', 'cc', 'dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + ['cc dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa bb', 'cc dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + [' cc dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa bb', ' cc dd'], r.data
        assert str(r) == 'aa bb  cc dd', str(r)

        r = f + ['cc', 'dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa bb', 'cc', 'dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + [' cc', 'dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa bb', ' cc', 'dd'], r.data
        assert str(r) == 'aa bb  cc dd', str(r)

        # input to CLVar is a list of strings
        f = CLVar(['aa', 'bb'])

        r = f + 'cc dd'
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', 'cc', 'dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + ' cc dd'
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', 'cc', 'dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + ['cc dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', 'cc dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + [' cc dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', ' cc dd'], r.data
        assert str(r) == 'aa bb  cc dd', str(r)

        r = f + ['cc', 'dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', 'cc', 'dd'], r.data
        assert str(r) == 'aa bb cc dd', str(r)

        r = f + [' cc', 'dd']
        assert isinstance(r, CLVar), type(r)
        assert r.data == ['aa', 'bb', ' cc', 'dd'], r.data
        assert str(r) == 'aa bb  cc dd', str(r)

        # make sure inplace adding a string works as well (issue 2399)
        # UserList would convert the string to a list of chars
        f = CLVar(['aa', 'bb'])
        f += 'cc dd'
        assert isinstance(f, CLVar), type(f)
        assert f.data == ['aa', 'bb', 'cc', 'dd'], f.data
        assert str(f) == 'aa bb cc dd', str(f)

        f = CLVar(['aa', 'bb'])
        f += ' cc dd'
        assert isinstance(f, CLVar), type(f)
        assert f.data == ['aa', 'bb', 'cc', 'dd'], f.data
        assert str(f) == 'aa bb cc dd', str(f)


    def test_Selector(self):
        """Test the Selector class"""

        class MyNode:
            def __init__(self, name):
                self.name = name

            def __str__(self):
                return self.name

            def get_suffix(self):
                return os.path.splitext(self.name)[1]

        s = Selector({'a': 'AAA', 'b': 'BBB'})
        assert s['a'] == 'AAA', s['a']
        assert s['b'] == 'BBB', s['b']
        exc_caught = None
        try:
            x = s['c']
        except KeyError:
            exc_caught = 1
        assert exc_caught, "should have caught a KeyError"
        s['c'] = 'CCC'
        assert s['c'] == 'CCC', s['c']

        class DummyEnv(UserDict):
            def subst(self, key):
                if key[0] == '$':
                    return self[key[1:]]
                return key

        env = DummyEnv()

        s = Selector({'.d': 'DDD', '.e': 'EEE'})
        ret = s(env, [])
        assert ret is None, ret
        ret = s(env, [MyNode('foo.d')])
        assert ret == 'DDD', ret
        ret = s(env, [MyNode('bar.e')])
        assert ret == 'EEE', ret
        ret = s(env, [MyNode('bar.x')])
        assert ret is None, ret
        s[None] = 'XXX'
        ret = s(env, [MyNode('bar.x')])
        assert ret == 'XXX', ret

        env = DummyEnv({'FSUFF': '.f', 'GSUFF': '.g'})

        s = Selector({'$FSUFF': 'FFF', '$GSUFF': 'GGG'})
        ret = s(env, [MyNode('foo.f')])
        assert ret == 'FFF', ret
        ret = s(env, [MyNode('bar.g')])
        assert ret == 'GGG', ret

    def test_adjustixes(self):
        """Test the adjustixes() function"""
        r = adjustixes('file', 'pre-', '-suf')
        assert r == 'pre-file-suf', r
        r = adjustixes('pre-file', 'pre-', '-suf')
        assert r == 'pre-file-suf', r
        r = adjustixes('file-suf', 'pre-', '-suf')
        assert r == 'pre-file-suf', r
        r = adjustixes('pre-file-suf', 'pre-', '-suf')
        assert r == 'pre-file-suf', r
        r = adjustixes('pre-file.xxx', 'pre-', '-suf')
        assert r == 'pre-file.xxx', r
        r = adjustixes('dir/file', 'pre-', '-suf')
        assert r == os.path.join('dir', 'pre-file-suf'), r

        # Verify that the odd case when library name is specified as 'lib'
        # doesn't yield lib.so, but yields the expected liblib.so
        r = adjustixes('PREFIX', 'PREFIX', 'SUFFIX')
        assert r == 'PREFIXPREFIXSUFFIX', "Failed handling when filename = PREFIX [r='%s']" % r

    def test_containsAny(self):
        """Test the containsAny() function"""
        assert containsAny('*.py', '*?[]')
        assert not containsAny('file.txt', '*?[]')

    def test_containsAll(self):
        """Test the containsAll() function"""
        assert containsAll('43221', '123')
        assert not containsAll('134', '123')

    def test_containsOnly(self):
        """Test the containsOnly() function"""
        assert containsOnly('.83', '0123456789.')
        assert not containsOnly('43221', '123')

    def test_LogicalLines(self):
        """Test the LogicalLines class"""
        content = """
foo \\
bar \\
baz
foo
bling \\
bling \\ bling
bling
"""
        fobj = io.StringIO(content)
        lines = LogicalLines(fobj).readlines()
        assert lines == [
            '\n',
            'foo bar baz\n',
            'foo\n',
            'bling bling \\ bling\n',
            'bling\n',
        ], lines

    def test_intern(self):
        s1 = silent_intern("spam")
        s3 = silent_intern(42)
        s4 = silent_intern("spam")
        assert id(s1) == id(s4)


class HashTestCase(unittest.TestCase):

    def test_collect(self):
        """Test collecting a list of signatures into a new signature value
        """
        for algorithm, expected in {
            'md5': ('698d51a19d8a121ce581499d7b701668',
                    '8980c988edc2c78cc43ccb718c06efd5',
                    '53fd88c84ff8a285eb6e0a687e55b8c7'),
            'sha1': ('6216f8a75fd5bb3d5f22b6f9958cdede3fc086c2',
                     '42eda1b5dcb3586bccfb1c69f22f923145271d97',
                     '2eb2f7be4e883ebe52034281d818c91e1cf16256'),
            'sha256': ('f6e0a1e2ac41945a9aa7ff8a8aaa0cebc12a3bcc981a929ad5cf810a090e11ae',
                       '25235f0fcab8767b7b5ac6568786fbc4f7d5d83468f0626bf07c3dbeed391a7a',
                       'f8d3d0729bf2427e2e81007588356332e7e8c4133fae4bceb173b93f33411d17'),
        }.items():
            # if the current platform does not support the algorithm we're looking at,
            # skip the test steps for that algorithm, but display a warning to the user
            if algorithm not in ALLOWED_HASH_FORMATS:
                warnings.warn("Missing hash algorithm {} on this platform, cannot test with it".format(algorithm), ResourceWarning)
            else:
                hs = functools.partial(hash_signature, hash_format=algorithm)
                s = list(map(hs, ('111', '222', '333')))

                assert expected[0] == hash_collect(s[0:1], hash_format=algorithm)
                assert expected[1] == hash_collect(s[0:2], hash_format=algorithm)
                assert expected[2] == hash_collect(s, hash_format=algorithm)

    def test_MD5signature(self):
        """Test generating a signature"""
        for algorithm, expected in {
            'md5': ('698d51a19d8a121ce581499d7b701668',
                    'bcbe3365e6ac95ea2c0343a2395834dd'),
            'sha1': ('6216f8a75fd5bb3d5f22b6f9958cdede3fc086c2',
                     '1c6637a8f2e1f75e06ff9984894d6bd16a3a36a9'),
            'sha256': ('f6e0a1e2ac41945a9aa7ff8a8aaa0cebc12a3bcc981a929ad5cf810a090e11ae',
                       '9b871512327c09ce91dd649b3f96a63b7408ef267c8cc5710114e629730cb61f'),
        }.items():
            # if the current platform does not support the algorithm we're looking at,
            # skip the test steps for that algorithm, but display a warning to the user
            if algorithm not in ALLOWED_HASH_FORMATS:
                warnings.warn("Missing hash algorithm {} on this platform, cannot test with it".format(algorithm), ResourceWarning)
            else:
                s = hash_signature('111', hash_format=algorithm)
                assert expected[0] == s, s

                s = hash_signature('222', hash_format=algorithm)
                assert expected[1] == s, s

# this uses mocking out, which is platform specific, however, the FIPS
# behavior this is testing is also platform-specific, and only would be
# visible in hosts running Linux with the fips_mode kernel flag along
# with using OpenSSL.

class FIPSHashTestCase(unittest.TestCase):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        ###############################
        # algorithm mocks, can check if we called with usedforsecurity=False for python >= 3.9
        self.fake_md5=lambda usedforsecurity=True: (usedforsecurity, 'md5')
        self.fake_sha1=lambda usedforsecurity=True: (usedforsecurity, 'sha1')
        self.fake_sha256=lambda usedforsecurity=True: (usedforsecurity, 'sha256')
        ###############################

        ###############################
        # hashlib mocks
        md5Available = unittest.mock.Mock(md5=self.fake_md5)
        del md5Available.sha1
        del md5Available.sha256
        self.md5Available=md5Available

        md5Default = unittest.mock.Mock(md5=self.fake_md5, sha1=self.fake_sha1)
        del md5Default.sha256
        self.md5Default=md5Default

        sha1Default = unittest.mock.Mock(sha1=self.fake_sha1, sha256=self.fake_sha256)
        del sha1Default.md5
        self.sha1Default=sha1Default

        sha256Default = unittest.mock.Mock(sha256=self.fake_sha256, **{'md5.side_effect': ValueError, 'sha1.side_effect': ValueError})
        self.sha256Default=sha256Default

        all_throw = unittest.mock.Mock(**{'md5.side_effect': ValueError, 'sha1.side_effect': ValueError, 'sha256.side_effect': ValueError})
        self.all_throw=all_throw
        
        no_algorithms = unittest.mock.Mock()
        del no_algorithms.md5
        del no_algorithms.sha1
        del no_algorithms.sha256
        del no_algorithms.nonexist
        self.no_algorithms=no_algorithms
        
        unsupported_algorithm = unittest.mock.Mock(unsupported=self.fake_sha256)
        del unsupported_algorithm.md5
        del unsupported_algorithm.sha1
        del unsupported_algorithm.sha256
        del unsupported_algorithm.unsupported
        self.unsupported_algorithm=unsupported_algorithm
        ###############################

        ###############################
        # system version mocks
        VersionInfo = namedtuple('VersionInfo', 'major minor micro releaselevel serial')
        v3_8 = VersionInfo(3, 8, 199, 'super-beta', 1337)
        v3_9 = VersionInfo(3, 9, 0, 'alpha', 0)
        v4_8 = VersionInfo(4, 8, 0, 'final', 0)

        self.sys_v3_8 = unittest.mock.Mock(version_info=v3_8)
        self.sys_v3_9 = unittest.mock.Mock(version_info=v3_9)
        self.sys_v4_8 = unittest.mock.Mock(version_info=v4_8)
        ###############################

    def test_basic_failover_bad_hashlib_hash_init(self):
        """Tests that if the hashing function is entirely missing from hashlib (hashlib returns None),
        the hash init function returns None"""
        assert _attempt_init_of_python_3_9_hash_object(None) is None

    def test_basic_failover_bad_hashlib_hash_get(self):
        """Tests that if the hashing function is entirely missing from hashlib (hashlib returns None),
        the hash get function returns None"""
        assert _attempt_get_hash_function("nonexist", self.no_algorithms) is None

    def test_usedforsecurity_flag_behavior(self):
        """Test usedforsecurity flag -> should be set to 'True' on older versions of python, and 'False' on Python >= 3.9"""
        for version, expected in {
            self.sys_v3_8: (True, 'md5'),
            self.sys_v3_9: (False, 'md5'),
            self.sys_v4_8: (False, 'md5'),
        }.items():
            assert _attempt_init_of_python_3_9_hash_object(self.fake_md5, version) == expected
    
    def test_automatic_default_to_md5(self):
        """Test automatic default to md5 even if sha1 available"""
        for version, expected in {
            self.sys_v3_8: (True, 'md5'),
            self.sys_v3_9: (False, 'md5'),
            self.sys_v4_8: (False, 'md5'),
        }.items():
            _set_allowed_viable_default_hashes(self.md5Default, version)
            set_hash_format(None, self.md5Default, version)
            assert _get_hash_object(None, self.md5Default, version) == expected

    def test_automatic_default_to_sha256(self):
        """Test automatic default to sha256 if other algorithms available but throw"""
        for version, expected in {
            self.sys_v3_8: (True, 'sha256'),
            self.sys_v3_9: (False, 'sha256'),
            self.sys_v4_8: (False, 'sha256'),
        }.items():
            _set_allowed_viable_default_hashes(self.sha256Default, version)
            set_hash_format(None, self.sha256Default, version)
            assert _get_hash_object(None, self.sha256Default, version) == expected

    def test_automatic_default_to_sha1(self):
        """Test automatic default to sha1 if md5 is missing from hashlib entirely"""
        for version, expected in {
            self.sys_v3_8: (True, 'sha1'),
            self.sys_v3_9: (False, 'sha1'),
            self.sys_v4_8: (False, 'sha1'),
        }.items():
            _set_allowed_viable_default_hashes(self.sha1Default, version)
            set_hash_format(None, self.sha1Default, version)
            assert _get_hash_object(None, self.sha1Default, version) == expected
    
    def test_no_available_algorithms(self):
        """expect exceptions on no available algorithms or when all algorithms throw"""
        self.assertRaises(SCons.Errors.SConsEnvironmentError, _set_allowed_viable_default_hashes, self.no_algorithms)
        self.assertRaises(SCons.Errors.SConsEnvironmentError, _set_allowed_viable_default_hashes, self.all_throw)
        self.assertRaises(SCons.Errors.SConsEnvironmentError, _set_allowed_viable_default_hashes, self.unsupported_algorithm)

    def test_bad_algorithm_set_attempt(self):
        """expect exceptions on user setting an unsupported algorithm selections, either by host or by SCons"""

        # nonexistant hash algorithm, not supported by SCons
        _set_allowed_viable_default_hashes(self.md5Available)
        self.assertRaises(SCons.Errors.UserError, set_hash_format, 'blah blah blah', hashlib_used=self.no_algorithms)
        
        # md5 is default-allowed, but in this case throws when we attempt to use it
        _set_allowed_viable_default_hashes(self.md5Available)
        self.assertRaises(SCons.Errors.UserError, set_hash_format, 'md5', hashlib_used=self.all_throw)

        # user attempts to use an algorithm that isn't supported by their current system but is supported by SCons
        _set_allowed_viable_default_hashes(self.sha1Default)
        self.assertRaises(SCons.Errors.UserError, set_hash_format, 'md5', hashlib_used=self.all_throw)
        
        # user attempts to use an algorithm that is supported by their current system but isn't supported by SCons
        _set_allowed_viable_default_hashes(self.sha1Default)
        self.assertRaises(SCons.Errors.UserError, set_hash_format, 'unsupported', hashlib_used=self.unsupported_algorithm)

    def tearDown(self):
        """Return SCons back to the normal global state for the hashing functions."""
        _set_allowed_viable_default_hashes(hashlib, sys)
        set_hash_format(None)


class NodeListTestCase(unittest.TestCase):
    def test_simple_attributes(self):
        """Test simple attributes of a NodeList class"""

        class TestClass:
            def __init__(self, name, child=None):
                self.child = child
                self.bar = name

        t1 = TestClass('t1', TestClass('t1child'))
        t2 = TestClass('t2', TestClass('t2child'))
        t3 = TestClass('t3')

        nl = NodeList([t1, t2, t3])
        assert nl.bar == ['t1', 't2', 't3'], nl.bar
        assert nl[0:2].child.bar == ['t1child', 't2child'], \
            nl[0:2].child.bar

    def test_callable_attributes(self):
        """Test callable attributes of a NodeList class"""

        class TestClass:
            def __init__(self, name, child=None):
                self.child = child
                self.bar = name

            def foo(self):
                return self.bar + "foo"

            def getself(self):
                return self

        t1 = TestClass('t1', TestClass('t1child'))
        t2 = TestClass('t2', TestClass('t2child'))
        t3 = TestClass('t3')

        nl = NodeList([t1, t2, t3])
        assert nl.foo() == ['t1foo', 't2foo', 't3foo'], nl.foo()
        assert nl.bar == ['t1', 't2', 't3'], nl.bar
        assert nl.getself().bar == ['t1', 't2', 't3'], nl.getself().bar
        assert nl[0:2].child.foo() == ['t1childfoo', 't2childfoo'], \
            nl[0:2].child.foo()
        assert nl[0:2].child.bar == ['t1child', 't2child'], \
            nl[0:2].child.bar

    def test_null(self):
        """Test a null NodeList"""
        nl = NodeList([])
        r = str(nl)
        assert r == '', r
        for node in nl:
            raise Exception("should not enter this loop")


class flattenTestCase(unittest.TestCase):

    def test_scalar(self):
        """Test flattening a scalar"""
        result = flatten('xyz')
        self.assertEqual(result, ['xyz'], result)

    def test_dictionary_values(self):
        """Test flattening the dictionary values"""
        items = {"a": 1, "b": 2, "c": 3}
        result = flatten(items.values())
        self.assertEqual(sorted(result), [1, 2, 3])


class OsEnviron:
    """Used to temporarily mock os.environ"""

    def __init__(self, environ):
        self._environ = environ

    def start(self):
        self._stored = os.environ
        os.environ = self._environ

    def stop(self):
        os.environ = self._stored
        del self._stored

    def __enter__(self):
        self.start()
        return os.environ

    def __exit__(self, *args):
        self.stop()


class get_env_boolTestCase(unittest.TestCase):
    def test_missing(self):
        env = dict()
        var = get_env_bool(env, 'FOO')
        assert var is False, "var should be False, not %s" % repr(var)
        env = {'FOO': '1'}
        var = get_env_bool(env, 'BAR')
        assert var is False, "var should be False, not %s" % repr(var)

    def test_true(self):
        for foo in ['TRUE', 'True', 'true',
                    'YES', 'Yes', 'yes',
                    'Y', 'y',
                    'ON', 'On', 'on',
                    '1', '20', '-1']:
            env = {'FOO': foo}
            var = get_env_bool(env, 'FOO')
            assert var is True, 'var should be True, not %s' % repr(var)

    def test_false(self):
        for foo in ['FALSE', 'False', 'false',
                    'NO', 'No', 'no',
                    'N', 'n',
                    'OFF', 'Off', 'off',
                    '0']:
            env = {'FOO': foo}
            var = get_env_bool(env, 'FOO', True)
            assert var is False, 'var should be True, not %s' % repr(var)

    def test_default(self):
        env = {'FOO': 'other'}
        var = get_env_bool(env, 'FOO', True)
        assert var is True, 'var should be True, not %s' % repr(var)
        var = get_env_bool(env, 'FOO', False)
        assert var is False, 'var should be False, not %s' % repr(var)


class get_os_env_boolTestCase(unittest.TestCase):
    def test_missing(self):
        with OsEnviron(dict()):
            var = get_os_env_bool('FOO')
            assert var is False, "var should be False, not %s" % repr(var)
        with OsEnviron({'FOO': '1'}):
            var = get_os_env_bool('BAR')
            assert var is False, "var should be False, not %s" % repr(var)

    def test_true(self):
        for foo in ['TRUE', 'True', 'true',
                    'YES', 'Yes', 'yes',
                    'Y', 'y',
                    'ON', 'On', 'on',
                    '1', '20', '-1']:
            with OsEnviron({'FOO': foo}):
                var = get_os_env_bool('FOO')
                assert var is True, 'var should be True, not %s' % repr(var)

    def test_false(self):
        for foo in ['FALSE', 'False', 'false',
                    'NO', 'No', 'no',
                    'N', 'n',
                    'OFF', 'Off', 'off',
                    '0']:
            with OsEnviron({'FOO': foo}):
                var = get_os_env_bool('FOO', True)
                assert var is False, 'var should be True, not %s' % repr(var)

    def test_default(self):
        with OsEnviron({'FOO': 'other'}):
            var = get_os_env_bool('FOO', True)
            assert var is True, 'var should be True, not %s' % repr(var)
            var = get_os_env_bool('FOO', False)
            assert var is False, 'var should be False, not %s' % repr(var)


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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/Warnings.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""The SCons warnings framework."""

import sys

import SCons.Errors

class SConsWarning(SCons.Errors.UserError):
    pass

class WarningOnByDefault(SConsWarning):
    pass


# NOTE:  If you add a new warning class, add it to the man page, too!
# Not all warnings are defined here, some are defined in the location of use

class TargetNotBuiltWarning(SConsWarning): # Should go to OnByDefault
    pass

class CacheVersionWarning(WarningOnByDefault):
    pass

class CacheWriteErrorWarning(SConsWarning):
    pass

class CacheCleanupErrorWarning(SConsWarning):
    pass

class CorruptSConsignWarning(WarningOnByDefault):
    pass

class DependencyWarning(SConsWarning):
    pass

class DevelopmentVersionWarning(WarningOnByDefault):
    pass

class DuplicateEnvironmentWarning(WarningOnByDefault):
    pass

class FutureReservedVariableWarning(WarningOnByDefault):
    pass

class LinkWarning(WarningOnByDefault):
    pass

class MisleadingKeywordsWarning(WarningOnByDefault):
    pass

class MissingSConscriptWarning(WarningOnByDefault):
    pass

class NoObjectCountWarning(WarningOnByDefault):
    pass

class NoParallelSupportWarning(WarningOnByDefault):
    pass

class ReservedVariableWarning(WarningOnByDefault):
    pass

class StackSizeWarning(WarningOnByDefault):
    pass

class VisualCMissingWarning(WarningOnByDefault):
    pass

# Used when MSVC_VERSION and MSVS_VERSION do not point to the
# same version (MSVS_VERSION is deprecated)
class VisualVersionMismatch(WarningOnByDefault):
    pass

class VisualStudioMissingWarning(SConsWarning):
    pass

class FortranCxxMixWarning(LinkWarning):
    pass


# Deprecation warnings

class FutureDeprecatedWarning(SConsWarning):
    pass

class DeprecatedWarning(SConsWarning):
    pass

class MandatoryDeprecatedWarning(DeprecatedWarning):
    pass


# Special case; base always stays DeprecatedWarning
class PythonVersionWarning(DeprecatedWarning):
    pass

class DeprecatedSourceCodeWarning(FutureDeprecatedWarning):
    pass

class TaskmasterNeedsExecuteWarning(DeprecatedWarning):
    pass

class DeprecatedOptionsWarning(MandatoryDeprecatedWarning):
    pass

class DeprecatedDebugOptionsWarning(MandatoryDeprecatedWarning):
    pass

class DeprecatedMissingSConscriptWarning(DeprecatedWarning):
    pass

class ToolQtDeprecatedWarning(FutureDeprecatedWarning):
    pass

# The below is a list of 2-tuples.  The first element is a class object.
# The second element is true if that class is enabled, false if it is disabled.
_enabled = []

# If set, raise the warning as an exception
_warningAsException = False

# If not None, a function to call with the warning
_warningOut = None

def suppressWarningClass(clazz):
    """Suppresses all warnings of type clazz or derived from clazz."""
    _enabled.insert(0, (clazz, False))

def enableWarningClass(clazz):
    """Enables all warnings of type clazz or derived from clazz."""
    _enabled.insert(0, (clazz, True))

def warningAsException(flag=True):
    """Set global _warningAsExeption flag.

    Args:
        flag: value to set warnings-as-exceptions to [default: True]

    Returns:
        The previous value.
    """
    global _warningAsException
    old = _warningAsException
    _warningAsException = flag
    return old

def warn(clazz, *args):
    """Issue a warning, accounting for SCons rules.

    Check if warnings for this class are enabled.
    If warnings are treated as exceptions, raise exception.
    Use the global warning-emitter _warningOut, which allows selecting
    different ways of presenting a traceback (see Script/Main.py)
    """
    warning = clazz(args)
    for cls, flag in _enabled:
        if isinstance(warning, cls):
            if flag:
                if _warningAsException:
                    raise warning

                if _warningOut:
                    _warningOut(warning)
            break

def process_warn_strings(arguments):
    """Process requests to enable/disable warnings.

    The requests are strings passed to the --warn option or the
    SetOption('warn') function.

    An argument to this option should be of the form "warning-class"
    or "no-warning-class".  The warning class is munged and has
    the suffix "Warning" added in order to get an actual class name
    from the classes above, which we need to pass to the
    {enable,disable}WarningClass() functions.

    For example, "deprecated" will enable the DeprecatedWarning class.
    "no-dependency" will disable the DependencyWarning class.

    As a special case, --warn=all and --warn=no-all will enable or
    disable (respectively) the base class of all SCons warnings.
    """

    def _classmunge(s):
        """Convert a warning argument to SConsCase.

        The result is CamelCase, except "Scons" is changed to "SCons"
        """
        s = s.replace("-", " ").title().replace(" ", "")
        return s.replace("Scons", "SCons")

    for arg in arguments:
        enable = True
        if arg.startswith("no-"):
            enable = False
            arg = arg[len("no-") :]
        if arg == 'all':
            class_name = "SConsWarning"
        else:
            class_name = _classmunge(arg) + 'Warning'
        try:
            clazz = globals()[class_name]
        except KeyError:
            sys.stderr.write("No warning type: '%s'\n" % arg)
        else:
            if enable:
                enableWarningClass(clazz)
            elif issubclass(clazz, MandatoryDeprecatedWarning):
                fmt = "Can not disable mandataory warning: '%s'\n"
                sys.stderr.write(fmt % arg)
            else:
                suppressWarningClass(clazz)

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/WarningsTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import unittest

import SCons.Warnings

class TestOutput:
    def __call__(self, x):
        args = x.args[0]
        if len(args) == 1:
            args = args[0]
        self.out = str(args)

class WarningsTestCase(unittest.TestCase):
    def test_Warning(self):
        """Test warn function."""

        # Reset global state
        SCons.Warnings._enabled = []
        SCons.Warnings._warningAsException = 0
        
        to = TestOutput()
        SCons.Warnings._warningOut=to
        SCons.Warnings.enableWarningClass(SCons.Warnings.SConsWarning)
        SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
                            "Foo")
        assert to.out == "Foo", to.out
        SCons.Warnings.warn(SCons.Warnings.DependencyWarning,
                            "Foo", 1)
        assert to.out == "('Foo', 1)", to.out

    def test_WarningAsExc(self):
        """Test warnings as exceptions."""
        
        # Reset global state
        SCons.Warnings._enabled = []
        SCons.Warnings._warningAsException = 0

        SCons.Warnings.enableWarningClass(SCons.Warnings.SConsWarning)
        old = SCons.Warnings.warningAsException()
        assert old == 0, old
        exc_caught = 0
        try:
            SCons.Warnings.warn(SCons.Warnings.SConsWarning, "Foo")
        except:
            exc_caught = 1
        assert exc_caught == 1

        old = SCons.Warnings.warningAsException(old)
        assert old == 1, old
        exc_caught = 0
        try:
            SCons.Warnings.warn(SCons.Warnings.SConsWarning, "Foo")
        except:
            exc_caught = 1
        assert exc_caught == 0

    def test_Disable(self):
        """Test disabling/enabling warnings."""

        # Reset global state
        SCons.Warnings._enabled = []
        SCons.Warnings._warningAsException = 0

        to = TestOutput()
        SCons.Warnings._warningOut=to
        to.out = None

        # No warnings by default
        SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
                            "Foo")
        assert to.out is None, to.out

        SCons.Warnings.enableWarningClass(SCons.Warnings.SConsWarning)
        SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
                            "Foo")
        assert to.out == "Foo", to.out

        to.out = None
        SCons.Warnings.suppressWarningClass(SCons.Warnings.DeprecatedWarning)
        SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
                            "Foo")
        assert to.out is None, to.out

        SCons.Warnings.warn(SCons.Warnings.MandatoryDeprecatedWarning,
                            "Foo")
        assert to.out is None, to.out

        # Dependency warnings should still be enabled though
        SCons.Warnings.enableWarningClass(SCons.Warnings.SConsWarning)
        SCons.Warnings.warn(SCons.Warnings.DependencyWarning,
                            "Foo")
        assert to.out == "Foo", to.out

        # Try reenabling all warnings...
        SCons.Warnings.enableWarningClass(SCons.Warnings.SConsWarning)

        SCons.Warnings.enableWarningClass(SCons.Warnings.SConsWarning)
        SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
                            "Foo")
        assert to.out == "Foo", to.out

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

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/__init__.py

__version__="4.4.1"
__copyright__="Copyright (c) 2001 - 2022 The SCons Foundation"
__developer__="bdbaddog"
__date__="Sun, 13 Nov 2022 11:31:29 -0400"
__buildsys__="M1DOG2021"
__revision__="15379ff3b369560c4bf13459b630387bf7349bb7"
__build__="15379ff3b369560c4bf13459b630387bf7349bb7"
# make sure compatibility is always in place
import SCons.compat # noqa

# SCons/__main__.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import SCons.Script
# this does all the work, and calls sys.exit
# with the proper exit status when done.
SCons.Script.main()


# SCons/cpp.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""SCons C Pre-Processor module"""

import os
import re

# First "subsystem" of regular expressions that we set up:
#
# Stuff to turn the C preprocessor directives in a file's contents into
# a list of tuples that we can process easily.
#
# A table of regular expressions that fetch the arguments from the rest of
# a C preprocessor line.  Different directives have different arguments
# that we want to fetch, using the regular expressions to which the lists
# of preprocessor directives map.
cpp_lines_dict = {
    # Fetch the rest of a #if/#elif as one argument,
    # with white space optional.
    ('if', 'elif')      : r'\s*(.+)',

    # Fetch the rest of a #ifdef/#ifndef as one argument,
    # separated from the keyword by white space.
    ('ifdef', 'ifndef',): r'\s+(.+)',

    # Fetch the rest of a #import/#include/#include_next line as one
    # argument, with white space optional.
    ('import', 'include', 'include_next',)
                        : r'\s*(.+)',

    # We don't care what comes after a #else or #endif line.
    ('else', 'endif',)  : '',

    # Fetch three arguments from a #define line:
    #   1) The #defined keyword.
    #   2) The optional parentheses and arguments (if it's a function-like
    #      macro, '' if it's not).
    #   3) The expansion value.
    ('define',)         : r'\s+([_A-Za-z][_A-Za-z0-9_]*)(\([^)]*\))?\s*(.*)',

    # Fetch the #undefed keyword from a #undef line.
    ('undef',)          : r'\s+([_A-Za-z][A-Za-z0-9_]*)',
}

# Create a table that maps each individual C preprocessor directive to
# the corresponding compiled regular expression that fetches the arguments
# we care about.
Table = {}
for op_list, expr in cpp_lines_dict.items():
    e = re.compile(expr)
    for op in op_list:
        Table[op] = e
del e
del op
del op_list

# Create a list of the expressions we'll use to match all of the
# preprocessor directives.  These are the same as the directives
# themselves *except* that we must use a negative lookahead assertion
# when matching "if" so it doesn't match the "if" in "ifdef" or "ifndef".
override = {
    'if'                        : 'if(?!n?def)',
}
l = [override.get(x, x) for x in Table.keys()]


# Turn the list of expressions into one big honkin' regular expression
# that will match all the preprocessor lines at once.  This will return
# a list of tuples, one for each preprocessor line.  The preprocessor
# directive will be the first element in each tuple, and the rest of
# the line will be the second element.
e = r'^\s*#\s*(' + '|'.join(l) + ')(.*)$'

# And last but not least, compile the expression.
CPP_Expression = re.compile(e, re.M)

# A list with RE to cleanup CPP Expressions (tuples)
# We should remove all comments and carriage returns (\r) before evaluating
CPP_Expression_Cleaner_List = [
    r"/\*.*\*/",
    r"/\*.*",
    r"//.*",
    r"\r"
]
CPP_Expression_Cleaner_RE = re.compile(
    r"\s*(" + "|".join(CPP_Expression_Cleaner_List) + ")")

def Cleanup_CPP_Expressions(ts):
    return [(t[0], CPP_Expression_Cleaner_RE.sub("", t[1])) for t in ts]

#
# Second "subsystem" of regular expressions that we set up:
#
# Stuff to translate a C preprocessor expression (as found on a #if or
# #elif line) into an equivalent Python expression that we can eval().
#

# A dictionary that maps the C representation of Boolean operators
# to their Python equivalents.
CPP_to_Python_Ops_Dict = {
    '!'         : ' not ',
    '!='        : ' != ',
    '&&'        : ' and ',
    '||'        : ' or ',
    '?'         : ' and ',
    ':'         : ' or ',
}

CPP_to_Python_Ops_Sub = lambda m: CPP_to_Python_Ops_Dict[m.group(0)]

# We have to sort the keys by length so that longer expressions
# come *before* shorter expressions--in particular, "!=" must
# come before "!" in the alternation.  Without this, the Python
# re module, as late as version 2.2.2, empirically matches the
# "!" in "!=" first, instead of finding the longest match.
# What's up with that?
l = sorted(list(CPP_to_Python_Ops_Dict.keys()), key=lambda a: len(a), reverse=True)

# Turn the list of keys into one regular expression that will allow us
# to substitute all of the operators at once.
expr = '|'.join(map(re.escape, l))

# ...and compile the expression.
CPP_to_Python_Ops_Expression = re.compile(expr)

# A separate list of expressions to be evaluated and substituted
# sequentially, not all at once.
CPP_to_Python_Eval_List = [
    [r'defined\s+(\w+)',                 '"\\1" in __dict__'],
    [r'defined\s*\((\w+)\)',             '"\\1" in __dict__'],
    [r'(0x[0-9A-Fa-f]+)(?:L|UL)?',  '\\1'],
    [r'(\d+)(?:L|UL)?',  '\\1'],
]

# Replace the string representations of the regular expressions in the
# list with compiled versions.
for l in CPP_to_Python_Eval_List:
    l[0] = re.compile(l[0])

# Wrap up all of the above into a handy function.
def CPP_to_Python(s):
    """
    Converts a C pre-processor expression into an equivalent
    Python expression that can be evaluated.
    """
    s = CPP_to_Python_Ops_Expression.sub(CPP_to_Python_Ops_Sub, s)
    for expr, repl in CPP_to_Python_Eval_List:
        s = re.sub(expr, repl, s)
    return s



del expr
del l
del override



class FunctionEvaluator:
    """Handles delayed evaluation of a #define function call."""

    def __init__(self, name, args, expansion):
        """
        Squirrels away the arguments and expansion value of a #define
        macro function for later evaluation when we must actually expand
        a value that uses it.
        """
        self.name = name
        self.args = function_arg_separator.split(args)
        try:
            expansion = expansion.split('##')
        except AttributeError:
            pass
        self.expansion = expansion

    def __call__(self, *values):
        """
        Evaluates the expansion of a #define macro function called
        with the specified values.
        """
        if len(self.args) != len(values):
            raise ValueError("Incorrect number of arguments to `%s'" % self.name)
        # Create a dictionary that maps the macro arguments to the
        # corresponding values in this "call."  We'll use this when we
        # eval() the expansion so that arguments will get expanded to
        # the right values.
        args = self.args
        localvars = {k: v for k, v in zip(args, values)}
        parts = [s if s in args else repr(s) for s in self.expansion]
        statement = ' + '.join(parts)

        return eval(statement, globals(), localvars)


# Find line continuations.
line_continuations = re.compile('\\\\\r?\n')

# Search for a "function call" macro on an expansion.  Returns the
# two-tuple of the "function" name itself, and a string containing the
# arguments within the call parentheses.
function_name = re.compile(r'(\S+)\(([^)]*)\)')

# Split a string containing comma-separated function call arguments into
# the separate arguments.
function_arg_separator = re.compile(r',\s*')



class PreProcessor:
    """The main workhorse class for handling C pre-processing."""

    def __init__(self, current=os.curdir, cpppath=(), dict={}, all=0, depth=-1):
        global Table

        cpppath = tuple(cpppath)

        self.searchpath = {
            '"': (current,) + cpppath,
            '<': cpppath + (current,),
        }

        # Initialize our C preprocessor namespace for tracking the
        # values of #defined keywords.  We use this namespace to look
        # for keywords on #ifdef/#ifndef lines, and to eval() the
        # expressions on #if/#elif lines (after massaging them from C to
        # Python).
        self.cpp_namespace = dict.copy()
        self.cpp_namespace['__dict__'] = self.cpp_namespace

        # Return all includes without resolving
        if all:
            self.do_include = self.all_include

        # Max depth of nested includes:
        # -1 = unlimited
        # 0 - disabled nesting
        # >0 - number of allowed nested includes
        self.depth = depth

        # For efficiency, a dispatch table maps each C preprocessor
        # directive (#if, #define, etc.) to the method that should be
        # called when we see it.  We accomodate state changes (#if,
        # #ifdef, #ifndef) by pushing the current dispatch table on a
        # stack and changing what method gets called for each relevant
        # directive we might see next at this level (#else, #elif).
        # #endif will simply pop the stack.
        d = {'scons_current_file': self.scons_current_file}
        for op in Table.keys():
            d[op] = getattr(self, 'do_' + op)
        self.default_table = d

    def __call__(self, file):
        """
        Pre-processes a file.

        This is the main public entry point.
        """
        self.current_file = file
        return self.process_file(file)

    def process_file(self, file):
        """
        Pre-processes a file.

        This is the main internal entry point.
        """
        return self._process_tuples(self.tupleize(self.read_file(file)), file)

    def process_contents(self, contents):
        """
        Pre-processes a file contents.

        Is used by tests
        """
        return self._process_tuples(self.tupleize(contents))

    def _process_tuples(self, tuples, file=None):
        self.stack = []
        self.dispatch_table = self.default_table.copy()
        self.current_file = file
        self.tuples = tuples

        self.initialize_result(file)
        while self.tuples:
            t = self.tuples.pop(0)
            # Uncomment to see the list of tuples being processed (e.g.,
            # to validate the CPP lines are being translated correctly).
            # print(t)
            self.dispatch_table[t[0]](t)
        return self.finalize_result(file)

    def tupleize(self, contents):
        """
        Turns the contents of a file into a list of easily-processed
        tuples describing the CPP lines in the file.

        The first element of each tuple is the line's preprocessor
        directive (#if, #include, #define, etc., minus the initial '#').
        The remaining elements are specific to the type of directive, as
        pulled apart by the regular expression.
        """
        return self._match_tuples(self._parse_tuples(contents))

    def _parse_tuples(self, contents):
        global CPP_Expression
        contents = line_continuations.sub('', contents)
        tuples = CPP_Expression.findall(contents)
        return Cleanup_CPP_Expressions(tuples)

    def _match_tuples(self, tuples):
        global Table
        result = []
        for t in tuples:
            m = Table[t[0]].match(t[1])
            if m:
                result.append((t[0],) + m.groups())
        return result

    # Dispatch table stack manipulation methods.

    def save(self):
        """
        Pushes the current dispatch table on the stack and re-initializes
        the current dispatch table to the default.
        """
        self.stack.append(self.dispatch_table)
        self.dispatch_table = self.default_table.copy()

    def restore(self):
        """
        Pops the previous dispatch table off the stack and makes it the
        current one.
        """
        try: self.dispatch_table = self.stack.pop()
        except IndexError: pass

    # Utility methods.

    def do_nothing(self, t):
        """
        Null method for when we explicitly want the action for a
        specific preprocessor directive to do nothing.
        """
        pass

    def scons_current_file(self, t):
        self.current_file = t[1]

    def eval_expression(self, t):
        """
        Evaluates a C preprocessor expression.

        This is done by converting it to a Python equivalent and
        eval()ing it in the C preprocessor namespace we use to
        track #define values.
        """
        t = CPP_to_Python(' '.join(t[1:]))
        try:
            return eval(t, self.cpp_namespace)
        except (NameError, TypeError, SyntaxError):
            return 0

    def initialize_result(self, fname):
        self.result = [fname]

    def finalize_result(self, fname):
        return self.result[1:]

    def find_include_file(self, t):
        """
        Finds the #include file for a given preprocessor tuple.
        """
        fname = t[2]
        for d in self.searchpath[t[1]]:
            if d == os.curdir:
                f = fname
            else:
                f = os.path.join(d, fname)
            if os.path.isfile(f):
                return f
        return None

    def read_file(self, file):
        with open(file) as f:
            return f.read()

    # Start and stop processing include lines.

    def start_handling_includes(self, t=None):
        """
        Causes the PreProcessor object to start processing #import,
        #include and #include_next lines.

        This method will be called when a #if, #ifdef, #ifndef or #elif
        evaluates True, or when we reach the #else in a #if, #ifdef,
        #ifndef or #elif block where a condition already evaluated
        False.

        """
        d = self.dispatch_table
        p = self.stack[-1] if self.stack else self.default_table

        for k in ('import', 'include', 'include_next', 'define', 'undef'):
            d[k] = p[k]

    def stop_handling_includes(self, t=None):
        """
        Causes the PreProcessor object to stop processing #import,
        #include and #include_next lines.

        This method will be called when a #if, #ifdef, #ifndef or #elif
        evaluates False, or when we reach the #else in a #if, #ifdef,
        #ifndef or #elif block where a condition already evaluated True.
        """
        d = self.dispatch_table
        d['import'] = self.do_nothing
        d['include'] =  self.do_nothing
        d['include_next'] =  self.do_nothing
        d['define'] =  self.do_nothing
        d['undef'] =  self.do_nothing

    # Default methods for handling all of the preprocessor directives.
    # (Note that what actually gets called for a given directive at any
    # point in time is really controlled by the dispatch_table.)

    def _do_if_else_condition(self, condition):
        """
        Common logic for evaluating the conditions on #if, #ifdef and
        #ifndef lines.
        """
        self.save()
        d = self.dispatch_table
        if condition:
            self.start_handling_includes()
            d['elif'] = self.stop_handling_includes
            d['else'] = self.stop_handling_includes
        else:
            self.stop_handling_includes()
            d['elif'] = self.do_elif
            d['else'] = self.start_handling_includes

    def do_ifdef(self, t):
        """
        Default handling of a #ifdef line.
        """
        self._do_if_else_condition(t[1] in self.cpp_namespace)

    def do_ifndef(self, t):
        """
        Default handling of a #ifndef line.
        """
        self._do_if_else_condition(t[1] not in self.cpp_namespace)

    def do_if(self, t):
        """
        Default handling of a #if line.
        """
        self._do_if_else_condition(self.eval_expression(t))

    def do_elif(self, t):
        """
        Default handling of a #elif line.
        """
        d = self.dispatch_table
        if self.eval_expression(t):
            self.start_handling_includes()
            d['elif'] = self.stop_handling_includes
            d['else'] = self.stop_handling_includes

    def do_else(self, t):
        """
        Default handling of a #else line.
        """
        pass

    def do_endif(self, t):
        """
        Default handling of a #endif line.
        """
        self.restore()

    def do_define(self, t):
        """
        Default handling of a #define line.
        """
        _, name, args, expansion = t
        try:
            expansion = int(expansion)
        except (TypeError, ValueError):
            # handle "defined" chain "! (defined (A) || defined (B)" ...
            if "defined " in expansion:
                self.cpp_namespace[name] = self.eval_expression(t[2:])
                return

        if args:
            evaluator = FunctionEvaluator(name, args[1:-1], expansion)
            self.cpp_namespace[name] = evaluator
        else:
            self.cpp_namespace[name] = expansion

    def do_undef(self, t):
        """
        Default handling of a #undef line.
        """
        try: del self.cpp_namespace[t[1]]
        except KeyError: pass

    def do_import(self, t):
        """
        Default handling of a #import line.
        """
        # XXX finish this -- maybe borrow/share logic from do_include()...?
        pass

    def do_include(self, t):
        """
        Default handling of a #include line.
        """
        t = self.resolve_include(t)
        if not t:
            return
        include_file = self.find_include_file(t)
        # avoid infinite recursion
        if not include_file or include_file in self.result:
            return
        self.result.append(include_file)
        # print include_file, len(self.tuples)

        # Handle maximum depth of nested includes
        if self.depth != -1:
            current_depth = 0
            for t in self.tuples:
                if t[0] == "scons_current_file":
                    current_depth += 1
            if current_depth >= self.depth:
                return

        new_tuples = [('scons_current_file', include_file)] + \
                      self.tupleize(self.read_file(include_file)) + \
                     [('scons_current_file', self.current_file)]
        self.tuples[:] = new_tuples + self.tuples

    # Date: Tue, 22 Nov 2005 20:26:09 -0500
    # From: Stefan Seefeld <seefeld@sympatico.ca>
    #
    # By the way, #include_next is not the same as #include. The difference
    # being that #include_next starts its search in the path following the
    # path that let to the including file. In other words, if your system
    # include paths are ['/foo', '/bar'], and you are looking at a header
    # '/foo/baz.h', it might issue an '#include_next <baz.h>' which would
    # correctly resolve to '/bar/baz.h' (if that exists), but *not* see
    # '/foo/baz.h' again. See http://www.delorie.com/gnu/docs/gcc/cpp_11.html
    # for more reasoning.
    #
    # I have no idea in what context 'import' might be used.

    # XXX is #include_next really the same as #include ?
    do_include_next = do_include

    # Utility methods for handling resolution of include files.

    def resolve_include(self, t):
        """Resolve a tuple-ized #include line.

        This handles recursive expansion of values without "" or <>
        surrounding the name until an initial " or < is found, to handle
        #include FILE where FILE is a #define somewhere else.
        """
        s = t[1].strip()
        while not s[0] in '<"':
            try:
                s = self.cpp_namespace[s]
                # strip backslashes from the computed include (-DFOO_H=\"foo.h\")
                for c in '<">':
                    s = s.replace(f"\\{c}", c)
            except KeyError:
                m = function_name.search(s)

                # Date: Mon, 28 Nov 2016 17:47:13 UTC
                # From: Ivan Kravets <ikravets@platformio.org>
                #
                # Ignore `#include` directive that depends on dynamic macro
                # which is not located in state TABLE
                # For example, `#include MYCONFIG_FILE`
                if not m:
                    return None

                s = self.cpp_namespace[m.group(1)]
                if callable(s):
                    args = function_arg_separator.split(m.group(2))
                    s = s(*args)
            if not s:
                return None
        return (t[0], s[0], s[1:-1])

    def all_include(self, t):
        """
        """
        self.result.append(self.resolve_include(t))


class DumbPreProcessor(PreProcessor):
    """A preprocessor that ignores all #if/#elif/#else/#endif directives
    and just reports back *all* of the #include files (like the classic
    SCons scanner did).

    This is functionally equivalent to using a regular expression to
    find all of the #include lines, only slower.  It exists mainly as
    an example of how the main PreProcessor class can be sub-classed
    to tailor its behavior.
    """
    def __init__(self, *args, **kw):
        PreProcessor.__init__(self, *args, **kw)
        d = self.default_table
        for func in ['if', 'elif', 'else', 'endif', 'ifdef', 'ifndef']:
            d[func] = d[func] = self.do_nothing

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/cppTests.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import atexit
import unittest

import TestUnit

import cpp


basic_input = """
#include "file1-yes"
#include <file2-yes>
"""


substitution_input = """
#define FILE3   "file3-yes"
#define FILE4   <file4-yes>

#include FILE3
#include FILE4

#define XXX_FILE5       YYY_FILE5
#define YYY_FILE5       ZZZ_FILE5
#define ZZZ_FILE5       FILE5

#define FILE5   "file5-yes"
#define FILE6   <file6-yes>

#define XXX_FILE6       YYY_FILE6
#define YYY_FILE6       ZZZ_FILE6
#define ZZZ_FILE6       FILE6

#include XXX_FILE5
#include XXX_FILE6

#include SHELL_ESCAPED_H
"""


ifdef_input = """
#define DEFINED 0

#ifdef	DEFINED /* multi-line comment */
#include "file7-yes"
#else
#include "file7-no"
#endif

#ifdef	NOT_DEFINED
#include <file8-no>
#else
#include <file8-yes>
#endif
"""


if_boolean_input = """
#define ZERO	0  // single-line comment
#define ONE	1

#if ZERO
#include "file9-no"
#else
#include "file9-yes"
#endif

#if ONE
#include <file10-yes>
#else
#include <file10-no>
#endif

#if ZERO
#include "file11-no-1"
#elif ZERO
#include "file11-no-2"
#else
#include "file11-yes"
#endif

#if ZERO
#include <file12-no-1>
#elif ONE
#include <file12-yes>
#else
#include <file12-no-2>
#endif

#if ONE
#include "file13-yes"
#elif ZERO
#include "file13-no-1"
#else
#include "file13-no-2"
#endif

#if ONE
#include <file14-yes>
#elif ONE
#include <file14-no-1>
#else
#include <file14-no-2>
#endif
"""


if_defined_input = """
#define DEFINED_A 0
#define DEFINED_B 0

#if	defined(DEFINED_A)
#include "file15-yes"
#endif

#if	! defined(DEFINED_A)
#include <file16-no>
#else
#include <file16-yes>
#endif

#if	defined DEFINED_A
#include "file17-yes"
#endif

#if	! defined DEFINED_A
#include <file18-no>
#else
#include <file18-yes>
#endif

#if ! (defined (DEFINED_A) || defined (DEFINED_B)
#include <file19-no>
#else
#include <file19-yes>
#endif

"""


expression_input = """
#define ZERO	0
#define ONE	1

#if	ZERO && ZERO
#include "file19-no"
#else
#include "file19-yes"
#endif

#if	ZERO && ONE
#include <file20-no>
#else
#include <file20-yes>
#endif

#if	ONE && ZERO
#include "file21-no"
#else
#include "file21-yes"
#endif

#if	ONE && ONE
#include <file22-yes>
#else
#include <file22-no>
#endif

#if	ZERO || ZERO
#include "file23-no"
#else
#include "file23-yes"
#endif

#if	ZERO || ONE
#include <file24-yes>
#else
#include <file24-no>
#endif

#if	ONE || ZERO
#include "file25-yes"
#else
#include "file25-no"
#endif

#if	ONE || ONE
#include <file26-yes>
#else
#include <file26-no>
#endif

#if	ONE == ONE
#include "file27-yes"
#else
#include "file27-no"
#endif

#if	ONE != ONE
#include <file28-no>
#else
#include <file28-yes>
#endif

#if	! (ONE == ONE)
#include "file29-no"
#else
#include "file29-yes"
#endif

#if ! (ONE != ONE)
#include <file30-yes>
#else
#include <file30-no>
#endif

#if	123456789UL || 0x13L
#include <file301-yes>
#else
#include <file301-no>
#endif
"""


undef_input = """
#define	UNDEFINE	0

#ifdef	UNDEFINE
#include "file31-yes"
#else
#include "file31-no"
#endif

#undef	UNDEFINE

#ifdef	UNDEFINE
#include <file32-no>
#else
#include <file32-yes>
#endif
"""


macro_function_input = """
#define ZERO	0
#define ONE	1

#define	FUNC33(x)	"file33-yes"
#define	FUNC34(x)	<file34-yes>

#include FUNC33(ZERO)
#include FUNC34(ZERO)

#define FILE35		"file35-yes"
#define FILE36		<file36-yes>

#define	FUNC35(x, y)	FILE35
#define	FUNC36(x, y)	FILE36

#include FUNC35(ZERO, ONE)
#include FUNC36(ZERO, ONE)

#define FILE37		"file37-yes"
#define FILE38		<file38-yes>

#define	FUNC37a(x, y)	FILE37
#define	FUNC38a(x, y)	FILE38

#define	FUNC37b(x, y)	FUNC37a(x, y)
#define	FUNC38b(x, y)	FUNC38a(x, y)

#define	FUNC37c(x, y)	FUNC37b(x, y)
#define	FUNC38c(x, y)	FUNC38b(x, y)

#include FUNC37c(ZERO, ONE)
#include FUNC38c(ZERO, ONE)

#define FILE39		"file39-yes"
#define FILE40		<file40-yes>

#define	FUNC39a(x0, y0)	FILE39
#define	FUNC40a(x0, y0)	FILE40

#define	FUNC39b(x1, y2)	FUNC39a(x1, y1)
#define	FUNC40b(x1, y2)	FUNC40a(x1, y1)

#define	FUNC39c(x2, y2)	FUNC39b(x2, y2)
#define	FUNC40c(x2, y2)	FUNC40b(x2, y2)

#include FUNC39c(ZERO, ONE)
#include FUNC40c(ZERO, ONE)

/* Make sure we don't die if the expansion isn't a string. */
#define FUNC_INTEGER(x)       1

/* Make sure one-character names are recognized. */
#define _(x)       translate(x)
#undef _
"""


token_pasting_input = """
#define PASTE_QUOTE(q, name)	q##name##-yes##q
#define PASTE_ANGLE(name)	<##name##-yes>

#define FUNC41	PASTE_QUOTE(", file41)
#define FUNC42	PASTE_ANGLE(file42)

#include FUNC41
#include FUNC42
"""


no_space_input = """
#include<file43-yes>
#include"file44-yes"
"""


nested_ifs_input = """
#define DEFINED

#ifdef	NOT_DEFINED
    #include "file7-no"
    #ifdef DEFINED
        #include "file8-no"
    #else
        #include "file9-no"
    #endif
#else
    #include "file7-yes"
#endif
"""



ifndef_input = """
#define DEFINED 0

#ifndef	DEFINED
#include "file45-no"
#else
#include "file45-yes"
#endif

#ifndef	NOT_DEFINED
#include <file46-yes>
#else
#include <file46-no>
#endif
"""


if_defined_no_space_input = """
#define DEFINED 0

#if(defined DEFINED)
#include "file47-yes"
#endif

#if(!defined DEFINED)
#include <file48-no>
#elif(!defined DEFINED)
#include <file49-no>
#else
#include <file50-yes>
#endif

#if!(defined DEFINED)
#include "file51-no"
#elif!(defined DEFINED)
#include <file52-no>
#else
#include "file53-yes"
#endif
"""

if_no_space_input = """
#define DEFINED 0

#if(DEFINED)
#include "file54-no"
#endif

#if!(DEFINED)
#include <file55-yes>
#elif!(DEFINED)
#include <file56-no>
#endif

#if(DEFINED)
#include "file57-no"
#elif(!DEFINED)
#include <file58-yes>
#endif

#if!( DEFINED)
#include "file59-yes"
#elif!( DEFINED)
#include <file60-no>
#endif

#if( DEFINED)
#include "file61-no"
#elif(! DEFINED)
#include <file62-yes>
#endif
"""


#    pp_class = PreProcessor
#    #pp_class = DumbPreProcessor

#    pp = pp_class(current = ".",
#                  cpppath = ['/usr/include'],
#                  print_all = 1)
#    #pp(open(sys.argv[1]).read())
#    pp(input)


class cppTestCase(unittest.TestCase):
    def setUp(self):
        self.cpp = self.cpp_class(current = ".",
                                  cpppath = ['/usr/include'],
                                  dict={"SHELL_ESCAPED_H": '\\"file-shell-computed-yes\\"'})

    def test_basic(self):
        """Test basic #include scanning"""
        expect = self.basic_expect
        result = self.cpp.process_contents(basic_input)
        assert expect == result, (expect, result)

    def test_substitution(self):
        """Test substitution of #include files using CPP variables"""
        expect = self.substitution_expect
        result = self.cpp.process_contents(substitution_input)
        assert expect == result, (expect, result)

    def test_ifdef(self):
        """Test basic #ifdef processing"""
        expect = self.ifdef_expect
        result = self.cpp.process_contents(ifdef_input)
        assert expect == result, (expect, result)

    def test_if_boolean(self):
        """Test #if with Boolean values"""
        expect = self.if_boolean_expect
        result = self.cpp.process_contents(if_boolean_input)
        assert expect == result, (expect, result)

    def test_if_defined(self):
        """Test #if defined() idioms"""
        expect = self.if_defined_expect
        result = self.cpp.process_contents(if_defined_input)
        assert expect == result, (expect, result)

    def test_expression(self):
        """Test #if with arithmetic expressions"""
        expect = self.expression_expect
        result = self.cpp.process_contents(expression_input)
        assert expect == result, (expect, result)

    def test_undef(self):
        """Test #undef handling"""
        expect = self.undef_expect
        result = self.cpp.process_contents(undef_input)
        assert expect == result, (expect, result)

    def test_macro_function(self):
        """Test using macro functions to express file names"""
        expect = self.macro_function_expect
        result = self.cpp.process_contents(macro_function_input)
        assert expect == result, (expect, result)

    def test_token_pasting(self):
        """Test token-pasting to construct file names"""
        expect = self.token_pasting_expect
        result = self.cpp.process_contents(token_pasting_input)
        assert expect == result, (expect, result)

    def test_no_space(self):
        """Test no space between #include and the quote"""
        expect = self.no_space_expect
        result = self.cpp.process_contents(no_space_input)
        assert expect == result, (expect, result)

    def test_nested_ifs(self):
        expect = self.nested_ifs_expect
        result = self.cpp.process_contents(nested_ifs_input)
        assert expect == result, (expect, result)

    def test_ifndef(self):
        """Test basic #ifndef processing"""
        expect = self.ifndef_expect
        result = self.cpp.process_contents(ifndef_input)
        assert expect == result, (expect, result)

    def test_if_defined_no_space(self):
        """Test #if(defined, i.e.without space but parenthesis"""
        expect = self.if_defined_no_space_expect
        result = self.cpp.process_contents(if_defined_no_space_input)
        assert expect == result, (expect, result)

    def test_if_no_space(self):
        """Test #if(, i.e. without space but parenthesis"""
        expect = self.if_no_space_expect
        result = self.cpp.process_contents(if_no_space_input)
        assert expect == result, (expect, result)


class cppAllTestCase(cppTestCase):
    def setUp(self):
        self.cpp = self.cpp_class(current = ".",
                                  cpppath = ['/usr/include'],
                                  dict={"SHELL_ESCAPED_H": '\\"file-shell-computed-yes\\"'},
                                  all=1)

class PreProcessorTestCase(cppAllTestCase):
    cpp_class = cpp.PreProcessor

    basic_expect = [
        ('include', '"', 'file1-yes'),
        ('include', '<', 'file2-yes'),
    ]

    substitution_expect = [
        ('include', '"', 'file3-yes'),
        ('include', '<', 'file4-yes'),
        ('include', '"', 'file5-yes'),
        ('include', '<', 'file6-yes'),
        ('include', '"', 'file-shell-computed-yes'),
    ]

    ifdef_expect = [
        ('include', '"', 'file7-yes'),
        ('include', '<', 'file8-yes'),
    ]

    if_boolean_expect = [
        ('include', '"', 'file9-yes'),
        ('include', '<', 'file10-yes'),
        ('include', '"', 'file11-yes'),
        ('include', '<', 'file12-yes'),
        ('include', '"', 'file13-yes'),
        ('include', '<', 'file14-yes'),
    ]

    if_defined_expect = [
        ('include', '"', 'file15-yes'),
        ('include', '<', 'file16-yes'),
        ('include', '"', 'file17-yes'),
        ('include', '<', 'file18-yes'),
        ('include', '<', 'file19-yes'),
    ]

    expression_expect = [
        ('include', '"', 'file19-yes'),
        ('include', '<', 'file20-yes'),
        ('include', '"', 'file21-yes'),
        ('include', '<', 'file22-yes'),
        ('include', '"', 'file23-yes'),
        ('include', '<', 'file24-yes'),
        ('include', '"', 'file25-yes'),
        ('include', '<', 'file26-yes'),
        ('include', '"', 'file27-yes'),
        ('include', '<', 'file28-yes'),
        ('include', '"', 'file29-yes'),
        ('include', '<', 'file30-yes'),
        ('include', '<', 'file301-yes'),
    ]

    undef_expect = [
        ('include', '"', 'file31-yes'),
        ('include', '<', 'file32-yes'),
    ]

    macro_function_expect = [
        ('include', '"', 'file33-yes'),
        ('include', '<', 'file34-yes'),
        ('include', '"', 'file35-yes'),
        ('include', '<', 'file36-yes'),
        ('include', '"', 'file37-yes'),
        ('include', '<', 'file38-yes'),
        ('include', '"', 'file39-yes'),
        ('include', '<', 'file40-yes'),
    ]

    token_pasting_expect = [
        ('include', '"', 'file41-yes'),
        ('include', '<', 'file42-yes'),
    ]

    no_space_expect = [
        ('include', '<', 'file43-yes'),
        ('include', '"', 'file44-yes'),
    ]

    nested_ifs_expect = [
        ('include', '"', 'file7-yes'),
    ]

    ifndef_expect = [
        ('include', '"', 'file45-yes'),
        ('include', '<', 'file46-yes'),
    ]

    if_defined_no_space_expect = [
        ('include', '"', 'file47-yes'),
        ('include', '<', 'file50-yes'),
        ('include', '"', 'file53-yes'),
    ]

    if_no_space_expect = [
        ('include', '<', 'file55-yes'),
        ('include', '<', 'file58-yes'),
        ('include', '"', 'file59-yes'),
        ('include', '<', 'file62-yes'),
    ]

class DumbPreProcessorTestCase(cppAllTestCase):
    cpp_class = cpp.DumbPreProcessor

    basic_expect = [
        ('include', '"', 'file1-yes'),
        ('include', '<', 'file2-yes'),
    ]

    substitution_expect = [
        ('include', '"', 'file3-yes'),
        ('include', '<', 'file4-yes'),
        ('include', '"', 'file5-yes'),
        ('include', '<', 'file6-yes'),
        ('include', '"', 'file-shell-computed-yes'),
    ]

    ifdef_expect = [
        ('include', '"', 'file7-yes'),
        ('include', '"', 'file7-no'),
        ('include', '<', 'file8-no'),
        ('include', '<', 'file8-yes'),
    ]

    if_boolean_expect = [
        ('include', '"', 'file9-no'),
        ('include', '"', 'file9-yes'),
        ('include', '<', 'file10-yes'),
        ('include', '<', 'file10-no'),
        ('include', '"', 'file11-no-1'),
        ('include', '"', 'file11-no-2'),
        ('include', '"', 'file11-yes'),
        ('include', '<', 'file12-no-1'),
        ('include', '<', 'file12-yes'),
        ('include', '<', 'file12-no-2'),
        ('include', '"', 'file13-yes'),
        ('include', '"', 'file13-no-1'),
        ('include', '"', 'file13-no-2'),
        ('include', '<', 'file14-yes'),
        ('include', '<', 'file14-no-1'),
        ('include', '<', 'file14-no-2'),
    ]

    if_defined_expect = [
        ('include', '"', 'file15-yes'),
        ('include', '<', 'file16-no'),
        ('include', '<', 'file16-yes'),
        ('include', '"', 'file17-yes'),
        ('include', '<', 'file18-no'),
        ('include', '<', 'file18-yes'),
        ('include', '<', 'file19-no'),
        ('include', '<', 'file19-yes'),
    ]

    expression_expect = [
        ('include', '"', 'file19-no'),
        ('include', '"', 'file19-yes'),
        ('include', '<', 'file20-no'),
        ('include', '<', 'file20-yes'),
        ('include', '"', 'file21-no'),
        ('include', '"', 'file21-yes'),
        ('include', '<', 'file22-yes'),
        ('include', '<', 'file22-no'),
        ('include', '"', 'file23-no'),
        ('include', '"', 'file23-yes'),
        ('include', '<', 'file24-yes'),
        ('include', '<', 'file24-no'),
        ('include', '"', 'file25-yes'),
        ('include', '"', 'file25-no'),
        ('include', '<', 'file26-yes'),
        ('include', '<', 'file26-no'),
        ('include', '"', 'file27-yes'),
        ('include', '"', 'file27-no'),
        ('include', '<', 'file28-no'),
        ('include', '<', 'file28-yes'),
        ('include', '"', 'file29-no'),
        ('include', '"', 'file29-yes'),
        ('include', '<', 'file30-yes'),
        ('include', '<', 'file30-no'),
        ('include', '<', 'file301-yes'),
        ('include', '<', 'file301-no'),
    ]

    undef_expect = [
        ('include', '"', 'file31-yes'),
        ('include', '"', 'file31-no'),
        ('include', '<', 'file32-no'),
        ('include', '<', 'file32-yes'),
    ]

    macro_function_expect = [
        ('include', '"', 'file33-yes'),
        ('include', '<', 'file34-yes'),
        ('include', '"', 'file35-yes'),
        ('include', '<', 'file36-yes'),
        ('include', '"', 'file37-yes'),
        ('include', '<', 'file38-yes'),
        ('include', '"', 'file39-yes'),
        ('include', '<', 'file40-yes'),
    ]

    token_pasting_expect = [
        ('include', '"', 'file41-yes'),
        ('include', '<', 'file42-yes'),
    ]

    no_space_expect = [
        ('include', '<', 'file43-yes'),
        ('include', '"', 'file44-yes'),
    ]

    nested_ifs_expect = [
        ('include', '"', 'file7-no'),
        ('include', '"', 'file8-no'),
        ('include', '"', 'file9-no'),
        ('include', '"', 'file7-yes')
    ]

    ifndef_expect = [
        ('include', '"', 'file45-no'),
        ('include', '"', 'file45-yes'),
        ('include', '<', 'file46-yes'),
        ('include', '<', 'file46-no'),
    ]

    if_defined_no_space_expect = [
        ('include', '"', 'file47-yes'),
        ('include', '<', 'file48-no'),
        ('include', '<', 'file49-no'),
        ('include', '<', 'file50-yes'),
        ('include', '"', 'file51-no'),
        ('include', '<', 'file52-no'),
        ('include', '"', 'file53-yes'),
    ]

    if_no_space_expect = [
        ('include', '"', 'file54-no'),
        ('include', '<', 'file55-yes'),
        ('include', '<', 'file56-no'),
        ('include', '"', 'file57-no'),
        ('include', '<', 'file58-yes'),
        ('include', '"', 'file59-yes'),
        ('include', '<', 'file60-no'),
        ('include', '"', 'file61-no'),
        ('include', '<', 'file62-yes'),
    ]

import os
import re
import shutil
import tempfile

_Cleanup = []

def _clean():
    for dir in _Cleanup:
        if os.path.exists(dir):
            shutil.rmtree(dir)

atexit.register(_clean)

if os.name in ('posix', 'nt'):
    tmpprefix = 'cppTests.' + str(os.getpid()) + '.'
else:
    tmpprefix = 'cppTests.'

class fileTestCase(unittest.TestCase):
    cpp_class = cpp.DumbPreProcessor

    def setUp(self):
        path = tempfile.mkdtemp(prefix=tmpprefix)
        _Cleanup.append(path)
        self.tempdir = path
        self.orig_cwd = os.getcwd()
        os.chdir(path)

    def tearDown(self):
        os.chdir(self.orig_cwd)
        shutil.rmtree(self.tempdir)
        _Cleanup.remove(self.tempdir)

    def strip_initial_spaces(self, s):
        lines = s.split('\n')
        spaces = re.match(' *', lines[0]).group(0)
        def strip_spaces(l, spaces=spaces):
            if l[:len(spaces)] == spaces:
                l = l[len(spaces):]
            return l
        return '\n'.join(map(strip_spaces, lines))

    def write(self, file, contents):
        with open(file, 'w') as f:
            f.write(self.strip_initial_spaces(contents))

    def test_basic(self):
        """Test basic file inclusion"""
        self.write('f1.h', """\
        #include "f2.h"
        """)
        self.write('f2.h', """\
        #include <f3.h>
        """)
        self.write('f3.h', """\
        """)
        p = cpp.DumbPreProcessor(current = os.curdir,
                                 cpppath = [os.curdir])
        result = p('f1.h')
        assert result == ['f2.h', 'f3.h'], result

    def test_current_file(self):
        """Test use of the .current_file attribute"""
        self.write('f1.h', """\
        #include <f2.h>
        """)
        self.write('f2.h', """\
        #include "f3.h"
        """)
        self.write('f3.h', """\
        """)
        class MyPreProcessor(cpp.DumbPreProcessor):
            def __init__(self, *args, **kw):
                super().__init__(*args, **kw)
                self.files = []
            def __call__(self, file):
                self.files.append(file)
                return cpp.DumbPreProcessor.__call__(self, file)
            def scons_current_file(self, t):
                r = cpp.DumbPreProcessor.scons_current_file(self, t)
                self.files.append(self.current_file)
                return r
        p = MyPreProcessor(current = os.curdir, cpppath = [os.curdir])
        result = p('f1.h')
        assert result == ['f2.h', 'f3.h'], result
        assert p.files == ['f1.h', 'f2.h', 'f3.h', 'f2.h', 'f1.h'], p.files



if __name__ == '__main__':
    suite = unittest.TestSuite()
    tclasses = [ PreProcessorTestCase,
                 DumbPreProcessorTestCase,
                 fileTestCase,
               ]
    for tclass in tclasses:
        loader = unittest.TestLoader()
        loader.testMethodPrefix = 'test_'
        names = loader.getTestCaseNames(tclass)
        try:
            names = sorted(set(names))
        except NameError:
            pass
        suite.addTests(list(map(tclass, names)))
    TestUnit.run(suite)

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/dblite.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
dblite.py module contributed by Ralf W. Grosse-Kunstleve.
Extended for Unicode by Steven Knight.
"""

import os
import pickle
import shutil
import time

from SCons.compat import PICKLE_PROTOCOL

KEEP_ALL_FILES = False
IGNORE_CORRUPT_DBFILES = False


def corruption_warning(filename):
    """Local warning for corrupt db.

    Used for self-tests. SCons overwrites this with a
    different warning function in SConsign.py.
    """
    print("Warning: Discarding corrupt database:", filename)

DBLITE_SUFFIX = '.dblite'
TMP_SUFFIX = '.tmp'


class dblite:
    """
    Squirrel away references to the functions in various modules
    that we'll use when our __del__() method calls our sync() method
    during shutdown.  We might get destroyed when Python is in the midst
    of tearing down the different modules we import in an essentially
    arbitrary order, and some of the various modules's global attributes
    may already be wiped out from under us.

    See the discussion at:
      http://mail.python.org/pipermail/python-bugs-list/2003-March/016877.html

    """

    _open = open
    _pickle_dump = staticmethod(pickle.dump)
    _pickle_protocol = PICKLE_PROTOCOL

    try:
        _os_chown = os.chown
    except AttributeError:
        _os_chown = None
    _os_replace = os.replace
    _os_chmod = os.chmod
    _shutil_copyfile = shutil.copyfile
    _time_time = time.time

    def __init__(self, file_base_name, flag, mode):
        assert flag in (None, "r", "w", "c", "n")
        if flag is None:
            flag = "r"

        base, ext = os.path.splitext(file_base_name)
        if ext == DBLITE_SUFFIX:
            # There's already a suffix on the file name, don't add one.
            self._file_name = file_base_name
            self._tmp_name = base + TMP_SUFFIX
        else:
            self._file_name = file_base_name + DBLITE_SUFFIX
            self._tmp_name = file_base_name + TMP_SUFFIX

        self._flag = flag
        self._mode = mode
        self._dict = {}
        self._needs_sync = False

        if self._os_chown is not None and (os.geteuid() == 0 or os.getuid() == 0):
            # running as root; chown back to current owner/group when done
            try:
                statinfo = os.stat(self._file_name)
                self._chown_to = statinfo.st_uid
                self._chgrp_to = statinfo.st_gid
            except OSError:
                # db file doesn't exist yet.
                # Check os.environ for SUDO_UID, use if set
                self._chown_to = int(os.environ.get('SUDO_UID', -1))
                self._chgrp_to = int(os.environ.get('SUDO_GID', -1))
        else:
            self._chown_to = -1  # don't chown
            self._chgrp_to = -1  # don't chgrp

        if self._flag == "n":
            with self._open(self._file_name, "wb", self._mode):
                pass  # just make sure it exists
        else:
            try:
                f = self._open(self._file_name, "rb")
            except IOError as e:
                if self._flag != "c":
                    raise e
                with self._open(self._file_name, "wb", self._mode):
                    pass  # just make sure it exists
            else:
                p = f.read()
                f.close()
                if len(p) > 0:
                    try:
                        self._dict = pickle.loads(p, encoding='bytes')
                    except (pickle.UnpicklingError, EOFError, KeyError):
                        # Note how we catch KeyErrors too here, which might happen
                        # when we don't have cPickle available (default pickle
                        # throws it).
                        if IGNORE_CORRUPT_DBFILES:
                            corruption_warning(self._file_name)
                        else:
                            raise

    def close(self):
        if self._needs_sync:
            self.sync()

    def __del__(self):
        self.close()

    def sync(self):
        self._check_writable()
        with self._open(self._tmp_name, "wb", self._mode) as f:
            self._pickle_dump(self._dict, f, self._pickle_protocol)

        try:
            self._os_replace(self._tmp_name, self._file_name)
        except PermissionError:
            # If we couldn't replace due to perms, try to change and retry.
            # This is mainly for Windows - on POSIX the file permissions
            # don't matter, the os.replace would have worked anyway.
            # We're giving up if the retry fails, just let the Python
            # exception abort us.
            try:
                self._os_chmod(self._file_name, 0o777)
            except PermissionError:
                pass
            self._os_replace(self._tmp_name, self._file_name)

        if self._os_chown is not None and self._chown_to > 0:  # don't chown to root or -1
            try:
                self._os_chown(self._file_name, self._chown_to, self._chgrp_to)
            except OSError:
                pass

        self._needs_sync = False
        if KEEP_ALL_FILES:
            self._shutil_copyfile(
                self._file_name,
                self._file_name + "_" + str(int(self._time_time()))
            )

    def _check_writable(self):
        if self._flag == "r":
            raise IOError("Read-only database: %s" % self._file_name)

    def __getitem__(self, key):
        return self._dict[key]

    def __setitem__(self, key, value):
        self._check_writable()

        if not isinstance(key, str):
            raise TypeError("key `%s' must be a string but is %s" % (key, type(key)))

        if not isinstance(value, bytes):
            raise TypeError("value `%s' must be a bytes but is %s" % (value, type(value)))

        self._dict[key] = value
        self._needs_sync = True

    def keys(self):
        return list(self._dict.keys())

    def __contains__(self, key):
        return key in self._dict

    def __iter__(self):
        return iter(self._dict)

    def __len__(self):
        return len(self._dict)


def open(file, flag=None, mode=0o666):
    return dblite(file, flag, mode)


def _exercise():
    db = open("tmp", "n")
    assert len(db) == 0
    db["foo"] = b"bar"
    assert db["foo"] == b"bar"
    db.sync()

    db = open("tmp", "c")
    assert len(db) == 1, len(db)
    assert db["foo"] == b"bar"
    db["bar"] = b"foo"
    assert db["bar"] == b"foo"
    db.sync()

    db = open("tmp", "r")
    assert len(db) == 2, len(db)
    assert db["foo"] == b"bar"
    assert db["bar"] == b"foo"
    try:
        db.sync()
    except IOError as e:
        assert str(e) == "Read-only database: tmp.dblite"
    else:
        raise RuntimeError("IOError expected.")
    db = open("tmp", "w")
    assert len(db) == 2, len(db)
    db["ping"] = b"pong"
    db.sync()

    try:
        db[(1, 2)] = "tuple"
    except TypeError as e:
        assert str(e) == "key `(1, 2)' must be a string but is <class 'tuple'>", str(e)
    else:
        raise RuntimeError("TypeError exception expected")

    try:
        db["list"] = [1, 2]
    except TypeError as e:
        assert str(e) == "value `[1, 2]' must be a bytes but is <class 'list'>", str(e)
    else:
        raise RuntimeError("TypeError exception expected")

    db = open("tmp", "r")
    assert len(db) == 3, len(db)

    db = open("tmp", "n")
    assert len(db) == 0, len(db)
    dblite._open("tmp.dblite", "w")

    db = open("tmp", "r")
    dblite._open("tmp.dblite", "w").write("x")
    try:
        db = open("tmp", "r")
    except pickle.UnpicklingError:
        pass
    else:
        raise RuntimeError("pickle exception expected.")

    global IGNORE_CORRUPT_DBFILES
    IGNORE_CORRUPT_DBFILES = True
    db = open("tmp", "r")
    assert len(db) == 0, len(db)
    os.unlink("tmp.dblite")
    try:
        db = open("tmp", "w")
    except IOError as e:
        assert str(e) == "[Errno 2] No such file or directory: 'tmp.dblite'", str(e)
    else:
        raise RuntimeError("IOError expected.")

    print("Completed _exercise()")


if __name__ == "__main__":
    _exercise()

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/exitfuncs.py

# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Register functions which are executed when SCons exits for any reason."""

# This is obsolete now

import atexit

_exithandlers = []
def _run_exitfuncs():
    """run any registered exit functions

    _exithandlers is traversed in reverse order so functions are executed
    last in, first out.
    """

    while _exithandlers:
        func, targs, kargs =  _exithandlers.pop()
        func(*targs, **kargs)

def register(func, *targs, **kargs):
    """register a function to be executed upon normal program termination

    func - function to be called at exit
    targs - optional arguments to pass to func
    kargs - optional keyword arguments to pass to func
    """
    _exithandlers.append((func, targs, kargs))


# make our exit function get run by python when it exits
atexit.register(_run_exitfuncs)

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:


# SCons/Util/hashes.py
# SPDX-License-Identifier: MIT
#
# Copyright The SCons Foundation

"""SCons utility functions

Routines for working with hash formats.
"""

import hashlib
import sys

from .types import to_bytes


# Default hash function and format. SCons-internal.
DEFAULT_HASH_FORMATS = ['md5', 'sha1', 'sha256']
ALLOWED_HASH_FORMATS = []
_HASH_FUNCTION = None
_HASH_FORMAT = None


def _attempt_init_of_python_3_9_hash_object(hash_function_object, sys_used=sys):
    """Initialize hash function with non-security indicator.

    In Python 3.9 and onwards, :mod:`hashlib` constructors accept a
    keyword argument *usedforsecurity*, which, if set to ``False``,
    lets us continue to use algorithms that have been deprecated either
    by FIPS or by Python itself, as the MD5 algorithm SCons prefers is
    not being used for security purposes as much as a short, 32 char
    hash that is resistant to accidental collisions.

    In prior versions of python, :mod:`hashlib` returns a native function
    wrapper, which errors out when it's queried for the optional
    parameter, so this function wraps that call.

    It can still throw a ValueError if the initialization fails due to
    FIPS compliance issues, but that is assumed to be the responsibility
    of the caller.
    """
    if hash_function_object is None:
        return None

    # https://stackoverflow.com/a/11887885 details how to check versions
    # with the "packaging" library. However, for our purposes, checking
    # the version is greater than or equal to 3.9 is good enough, as the API
    # is guaranteed to have support for the 'usedforsecurity' flag in 3.9. See
    # https://docs.python.org/3/library/hashlib.html#:~:text=usedforsecurity
    # for the version support notes.
    if (sys_used.version_info.major > 3) or (
        sys_used.version_info.major == 3 and sys_used.version_info.minor >= 9
    ):
        return hash_function_object(usedforsecurity=False)

    # Note that this can throw a ValueError in FIPS-enabled versions of
    # Linux prior to 3.9. The OpenSSL hashlib will throw on first init here,
    # but it is assumed to be responsibility of the caller to diagnose the
    # ValueError & potentially display the error to screen.
    return hash_function_object()


def _set_allowed_viable_default_hashes(hashlib_used, sys_used=sys) -> None:
    """Check if the default hash algorithms can be called.

    This util class is sometimes called prior to setting the
    user-selected hash algorithm, meaning that on FIPS-compliant systems
    the library would default-initialize MD5 and throw an exception in
    set_hash_format. A common case is using the SConf options, which can
    run prior to main, and thus ignore the options.hash_format variable.

    This function checks the DEFAULT_HASH_FORMATS and sets the
    ALLOWED_HASH_FORMATS to only the ones that can be called. In Python
    >= 3.9 this will always default to MD5 as in Python 3.9 there is an
    optional attribute "usedforsecurity" set for the method.

    Throws if no allowed hash formats are detected.
    """
    global ALLOWED_HASH_FORMATS
    _last_error = None
    # note: if you call this method repeatedly, example using timeout,
    # this is needed. Otherwise it keeps appending valid formats to the string.
    ALLOWED_HASH_FORMATS = []

    for test_algorithm in DEFAULT_HASH_FORMATS:
        _test_hash = getattr(hashlib_used, test_algorithm, None)
        # we know hashlib claims to support it... check to see if we can call it.
        if _test_hash is not None:
            # The hashing library will throw an exception on initialization
            # in FIPS mode, meaning if we call the default algorithm returned
            # with no parameters, it'll throw if it's a bad algorithm,
            # otherwise it will append it to the known good formats.
            try:
                _attempt_init_of_python_3_9_hash_object(_test_hash, sys_used)
                ALLOWED_HASH_FORMATS.append(test_algorithm)
            except ValueError as e:
                _last_error = e
                continue

    if len(ALLOWED_HASH_FORMATS) == 0:
        from SCons.Errors import (
            SConsEnvironmentError,
        )  # pylint: disable=import-outside-toplevel

        # chain the exception thrown with the most recent error from hashlib.
        raise SConsEnvironmentError(
            'No usable hash algorithms found.'
            'Most recent error from hashlib attached in trace.'
        ) from _last_error


_set_allowed_viable_default_hashes(hashlib)


def get_hash_format():
    """Retrieves the hash format or ``None`` if not overridden.

    A return value of ``None``
    does not guarantee that MD5 is being used; instead, it means that the
    default precedence order documented in :func:`SCons.Util.set_hash_format`
    is respected.
    """
    return _HASH_FORMAT


def _attempt_get_hash_function(hash_name, hashlib_used=hashlib, sys_used=sys):
    """Wrapper used to try to initialize a hash function given.

    If successful, returns the name of the hash function back to the user.

    Otherwise returns None.
    """
    try:
        _fetch_hash = getattr(hashlib_used, hash_name, None)
        if _fetch_hash is None:
            return None
        _attempt_init_of_python_3_9_hash_object(_fetch_hash, sys_used)
        return hash_name
    except ValueError:
        # If attempt_init_of_python_3_9 throws, this is typically due to FIPS
        # being enabled. However, if we get to this point, the viable hash
        # function check has either been bypassed or otherwise failed to
        # properly restrict the user to only the supported functions.
        # As such throw the UserError as an internal assertion-like error.
        return None


def set_hash_format(hash_format, hashlib_used=hashlib, sys_used=sys):
    """Sets the default hash format used by SCons.

    If `hash_format` is ``None`` or
    an empty string, the default is determined by this function.

    Currently the default behavior is to use the first available format of
    the following options: MD5, SHA1, SHA256.
    """
    global _HASH_FORMAT, _HASH_FUNCTION

    _HASH_FORMAT = hash_format
    if hash_format:
        hash_format_lower = hash_format.lower()
        if hash_format_lower not in ALLOWED_HASH_FORMATS:
            from SCons.Errors import (
                UserError,
            )  # pylint: disable=import-outside-toplevel

            # User can select something not supported by their OS but
            # normally supported by SCons, example, selecting MD5 in an
            # OS with FIPS-mode turned on. Therefore we first check if
            # SCons supports it, and then if their local OS supports it.
            if hash_format_lower in DEFAULT_HASH_FORMATS:
                raise UserError(
                    'While hash format "%s" is supported by SCons, the '
                    'local system indicates only the following hash '
                    'formats are supported by the hashlib library: %s'
                    % (hash_format_lower, ', '.join(ALLOWED_HASH_FORMATS))
                )

            # The hash format isn't supported by SCons in any case.
            # Warn the user, and if we detect that SCons supports more
            # algorithms than their local system supports,
            # warn the user about that too.
            if ALLOWED_HASH_FORMATS == DEFAULT_HASH_FORMATS:
                raise UserError(
                    'Hash format "%s" is not supported by SCons. Only '
                    'the following hash formats are supported: %s'
                    % (hash_format_lower, ', '.join(ALLOWED_HASH_FORMATS))
                )

            raise UserError(
                'Hash format "%s" is not supported by SCons. '
                'SCons supports more hash formats than your local system '
                'is reporting; SCons supports: %s. Your local system only '
                'supports: %s'
                % (
                    hash_format_lower,
                    ', '.join(DEFAULT_HASH_FORMATS),
                    ', '.join(ALLOWED_HASH_FORMATS),
                )
            )

        # This is not expected to fail. If this fails it means the
        # set_allowed_viable_default_hashes function did not throw,
        # or when it threw, the exception was caught and ignored, or
        # the global ALLOWED_HASH_FORMATS was changed by an external user.
        _HASH_FUNCTION = _attempt_get_hash_function(
            hash_format_lower, hashlib_used, sys_used
        )

        if _HASH_FUNCTION is None:
            from SCons.Errors import (
                UserError,
            )  # pylint: disable=import-outside-toplevel

            raise UserError(
                'Hash format "%s" is not available in your Python interpreter. '
                'Expected to be supported algorithm by set_allowed_viable_default_hashes, '
                'Assertion error in SCons.' % hash_format_lower
            )
    else:
        # Set the default hash format based on what is available, defaulting
        # to the first supported hash algorithm (usually md5) for backwards
        # compatibility. In FIPS-compliant systems this usually defaults to
        # SHA1, unless that too has been disabled.
        for choice in ALLOWED_HASH_FORMATS:
            _HASH_FUNCTION = _attempt_get_hash_function(choice, hashlib_used, sys_used)

            if _HASH_FUNCTION is not None:
                break
        else:
            # This is not expected to happen in practice.
            from SCons.Errors import (
                UserError,
            )  # pylint: disable=import-outside-toplevel

            raise UserError(
                'Your Python interpreter does not have MD5, SHA1, or SHA256. '
                'SCons requires at least one. Expected to support one or more '
                'during set_allowed_viable_default_hashes.'
            )


# Ensure that this is initialized in case either:
#    1. This code is running in a unit test.
#    2. This code is running in a consumer that does hash operations while
#       SConscript files are being loaded.
set_hash_format(None)


def get_current_hash_algorithm_used():
    """Returns the current hash algorithm name used.

    Where the python version >= 3.9, this is expected to return md5.
    If python's version is <= 3.8, this returns md5 on non-FIPS-mode platforms, and
    sha1 or sha256 on FIPS-mode Linux platforms.

    This function is primarily useful for testing, where one expects a value to be
    one of N distinct hashes, and therefore the test needs to know which hash to select.
    """
    return _HASH_FUNCTION


def _get_hash_object(hash_format, hashlib_used=hashlib, sys_used=sys):
    """Allocates a hash object using the requested hash format.

    Args:
        hash_format: Hash format to use.

    Returns:
        hashlib object.
    """
    if hash_format is None:
        if _HASH_FUNCTION is None:
            from SCons.Errors import (
                UserError,
            )  # pylint: disable=import-outside-toplevel

            raise UserError(
                'There is no default hash function. Did you call '
                'a hashing function before SCons was initialized?'
            )
        return _attempt_init_of_python_3_9_hash_object(
            getattr(hashlib_used, _HASH_FUNCTION, None), sys_used
        )

    if not hasattr(hashlib, hash_format):
        from SCons.Errors import UserError  # pylint: disable=import-outside-toplevel

        raise UserError(
            f'Hash format "{hash_format}" is not available in your Python interpreter.'
        )

    return _attempt_init_of_python_3_9_hash_object(
        getattr(hashlib, hash_format), sys_used
    )


def hash_signature(s, hash_format=None):
    """
    Generate hash signature of a string

    Args:
        s: either string or bytes. Normally should be bytes
        hash_format: Specify to override default hash format

    Returns:
        String of hex digits representing the signature
    """
    m = _get_hash_object(hash_format)
    try:
        m.update(to_bytes(s))
    except TypeError:
        m.update(to_bytes(str(s)))

    return m.hexdigest()


def hash_file_signature(fname, chunksize=65536, hash_format=None):
    """
    Generate the md5 signature of a file

    Args:
        fname: file to hash
        chunksize: chunk size to read
        hash_format: Specify to override default hash format

    Returns:
        String of Hex digits representing the signature
    """

    m = _get_hash_object(hash_format)
    with open(fname, "rb") as f:
        while True:
            blck = f.read(chunksize)
            if not blck:
                break
            m.update(to_bytes(blck))
    return m.hexdigest()


def hash_collect(signatures, hash_format=None):
    """
    Collects a list of signatures into an aggregate signature.

    Args:
        signatures: a list of signatures
        hash_format: Specify to override default hash format

    Returns:
        the aggregate signature
    """

    if len(signatures) == 1:
        return signatures[0]

    return hash_signature(', '.join(signatures), hash_format)


_MD5_WARNING_SHOWN = False


def _show_md5_warning(function_name):
    """Shows a deprecation warning for various MD5 functions."""

    global _MD5_WARNING_SHOWN

    if not _MD5_WARNING_SHOWN:
        import SCons.Warnings  # pylint: disable=import-outside-toplevel

        SCons.Warnings.warn(
            SCons.Warnings.DeprecatedWarning,
            f"Function {function_name} is deprecated",
        )
        _MD5_WARNING_SHOWN = True


def MD5signature(s):
    """Deprecated. Use :func:`hash_signature` instead."""

    _show_md5_warning("MD5signature")
    return hash_signature(s)


def MD5filesignature(fname, chunksize=65536):
    """Deprecated. Use :func:`hash_file_signature` instead."""

    _show_md5_warning("MD5filesignature")
    return hash_file_signature(fname, chunksize)


def MD5collect(signatures):
    """Deprecated. Use :func:`hash_collect` instead."""

    _show_md5_warning("MD5collect")
    return hash_collect(signatures)


# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:
