# 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 os.path
import sys
import time
import unittest
import shutil
import stat

from TestCmd import TestCmd, IS_WINDOWS

import SCons.Errors
import SCons.Node.FS
import SCons.Util
import SCons.Warnings
import SCons.Environment

built_it = None

scanner_count = 0


class Scanner:
    def __init__(self, node=None):
        global scanner_count
        scanner_count = scanner_count + 1
        self.hash = scanner_count
        self.node = node

    def path(self, env, dir, target=None, source=None):
        return ()

    def __call__(self, node, env, path):
        return [self.node]

    def __hash__(self):
        return self.hash

    def select(self, node):
        return self

    def recurse_nodes(self, nodes):
        return nodes


class Environment:
    def __init__(self):
        self.scanner = Scanner()

    def Dictionary(self, *args):
        return {}

    def autogenerate(self, **kw):
        return {}

    def get_scanner(self, skey):
        return self.scanner

    def Override(self, overrides):
        return self

    def _update(self, dict):
        pass


class Action:
    def __call__(self, targets, sources, env, **kw):
        global built_it
        if kw.get('execute', 1):
            built_it = 1
        return 0

    def show(self, string):
        pass

    def get_contents(self, target, source, env):
        return bytearray("", 'utf-8')

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

    def strfunction(self, targets, sources, env):
        return ""

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


class Builder:
    def __init__(self, factory, action=Action()):
        self.factory = factory
        self.env = Environment()
        self.overrides = {}
        self.action = action
        self.target_scanner = None
        self.source_scanner = None

    def targets(self, t):
        return [t]

    def source_factory(self, name):
        return self.factory(name)


class _tempdirTestCase(unittest.TestCase):
    def setUp(self):
        self.save_cwd = os.getcwd()
        self.test = TestCmd(workdir='')
        # FS doesn't like the cwd to be something other than its root.
        os.chdir(self.test.workpath(""))
        self.fs = SCons.Node.FS.FS()

    def tearDown(self):
        os.chdir(self.save_cwd)


class VariantDirTestCase(unittest.TestCase):
    def runTest(self):
        """Test variant dir functionality"""
        test = TestCmd(workdir='')

        fs = SCons.Node.FS.FS()
        f1 = fs.File('build/test1')
        fs.VariantDir('build', 'src')
        f2 = fs.File('build/test2')
        d1 = fs.Dir('build')
        assert f1.srcnode().get_internal_path() == os.path.normpath('src/test1'), f1.srcnode().get_internal_path()
        assert f2.srcnode().get_internal_path() == os.path.normpath('src/test2'), f2.srcnode().get_internal_path()
        assert d1.srcnode().get_internal_path() == 'src', d1.srcnode().get_internal_path()

        fs = SCons.Node.FS.FS()
        f1 = fs.File('build/test1')
        fs.VariantDir('build', '.')
        f2 = fs.File('build/test2')
        d1 = fs.Dir('build')
        assert f1.srcnode().get_internal_path() == 'test1', f1.srcnode().get_internal_path()
        assert f2.srcnode().get_internal_path() == 'test2', f2.srcnode().get_internal_path()
        assert d1.srcnode().get_internal_path() == '.', d1.srcnode().get_internal_path()

        fs = SCons.Node.FS.FS()
        fs.VariantDir('build/var1', 'src')
        fs.VariantDir('build/var2', 'src')
        f1 = fs.File('build/var1/test1')
        f2 = fs.File('build/var2/test1')
        assert f1.srcnode().get_internal_path() == os.path.normpath('src/test1'), f1.srcnode().get_internal_path()
        assert f2.srcnode().get_internal_path() == os.path.normpath('src/test1'), f2.srcnode().get_internal_path()

        fs = SCons.Node.FS.FS()
        fs.VariantDir('../var1', 'src')
        fs.VariantDir('../var2', 'src')
        f1 = fs.File('../var1/test1')
        f2 = fs.File('../var2/test1')
        assert f1.srcnode().get_internal_path() == os.path.normpath('src/test1'), f1.srcnode().get_internal_path()
        assert f2.srcnode().get_internal_path() == os.path.normpath('src/test1'), f2.srcnode().get_internal_path()

        # Set up some files
        test.subdir('work', ['work', 'src'])
        test.subdir(['work', 'build'], ['work', 'build', 'var1'])
        test.subdir(['work', 'build', 'var2'])
        test.subdir('rep1', ['rep1', 'src'])
        test.subdir(['rep1', 'build'], ['rep1', 'build', 'var1'])
        test.subdir(['rep1', 'build', 'var2'])

        # A source file in the source directory
        test.write(['work', 'src', 'test.in'], 'test.in')

        # A source file in a subdir of the source directory
        test.subdir(['work', 'src', 'new_dir'])
        test.write(['work', 'src', 'new_dir', 'test9.out'], 'test9.out\n')

        # A source file in the repository
        test.write(['rep1', 'src', 'test2.in'], 'test2.in')

        # Some source files in the variant directory
        test.write(['work', 'build', 'var2', 'test.in'], 'test.old')
        test.write(['work', 'build', 'var2', 'test2.in'], 'test2.old')

        # An old derived file in the variant directories
        test.write(['work', 'build', 'var1', 'test.out'], 'test.old')
        test.write(['work', 'build', 'var2', 'test.out'], 'test.old')

        # And just in case we are weird, a derived file in the source
        # dir.
        test.write(['work', 'src', 'test.out'], 'test.out.src')

        # A derived file in the repository
        test.write(['rep1', 'build', 'var1', 'test2.out'], 'test2.out_rep')
        test.write(['rep1', 'build', 'var2', 'test2.out'], 'test2.out_rep')

        os.chdir(test.workpath('work'))

        fs = SCons.Node.FS.FS(test.workpath('work'))
        fs.VariantDir('build/var1', 'src', duplicate=0)
        fs.VariantDir('build/var2', 'src')
        f1 = fs.File('build/var1/test.in')
        f1out = fs.File('build/var1/test.out')
        f1out.builder = 1
        f1out_2 = fs.File('build/var1/test2.out')
        f1out_2.builder = 1
        f2 = fs.File('build/var2/test.in')
        f2out = fs.File('build/var2/test.out')
        f2out.builder = 1
        f2out_2 = fs.File('build/var2/test2.out')
        f2out_2.builder = 1
        fs.Repository(test.workpath('rep1'))

        assert f1.srcnode().get_internal_path() == os.path.normpath('src/test.in'), \
            f1.srcnode().get_internal_path()
        # str(node) returns source path for duplicate = 0
        assert str(f1) == os.path.normpath('src/test.in'), str(f1)
        # Build path does not exist
        assert not f1.exists()
        # ...but the actual file is not there...
        assert not os.path.exists(f1.get_abspath())
        # And duplicate=0 should also work just like a Repository
        assert f1.rexists()
        # rfile() should point to the source path
        assert f1.rfile().get_internal_path() == os.path.normpath('src/test.in'), \
            f1.rfile().get_internal_path()

        assert f2.srcnode().get_internal_path() == os.path.normpath('src/test.in'), \
            f2.srcnode().get_internal_path()
        # str(node) returns build path for duplicate = 1
        assert str(f2) == os.path.normpath('build/var2/test.in'), str(f2)
        # Build path exists
        assert f2.exists()
        # ...and exists() should copy the file from src to build path
        assert test.read(['work', 'build', 'var2', 'test.in']) == bytearray('test.in', 'utf-8'), \
            test.read(['work', 'build', 'var2', 'test.in'])
        # Since exists() is true, so should rexists() be
        assert f2.rexists()

        f3 = fs.File('build/var1/test2.in')
        f4 = fs.File('build/var2/test2.in')

        assert f3.srcnode().get_internal_path() == os.path.normpath('src/test2.in'), \
            f3.srcnode().get_internal_path()
        # str(node) returns source path for duplicate = 0
        assert str(f3) == os.path.normpath('src/test2.in'), str(f3)
        # Build path does not exist
        assert not f3.exists()
        # Source path does not either
        assert not f3.srcnode().exists()
        # But we do have a file in the Repository
        assert f3.rexists()
        # rfile() should point to the source path
        assert f3.rfile().get_internal_path() == os.path.normpath(test.workpath('rep1/src/test2.in')), \
            f3.rfile().get_internal_path()

        assert f4.srcnode().get_internal_path() == os.path.normpath('src/test2.in'), \
            f4.srcnode().get_internal_path()
        # str(node) returns build path for duplicate = 1
        assert str(f4) == os.path.normpath('build/var2/test2.in'), str(f4)
        # Build path should exist
        assert f4.exists()
        # ...and copy over the file into the local build path
        assert test.read(['work', 'build', 'var2', 'test2.in']) == bytearray('test2.in', 'utf-8')
        # should exist in repository, since exists() is true
        assert f4.rexists()
        # rfile() should point to ourselves
        assert f4.rfile().get_internal_path() == os.path.normpath('build/var2/test2.in'), \
            f4.rfile().get_internal_path()

        f5 = fs.File('build/var1/test.out')
        f6 = fs.File('build/var2/test.out')

        assert f5.exists()
        # We should not copy the file from the source dir, since this is
        # a derived file.
        assert test.read(['work', 'build', 'var1', 'test.out']) == bytearray('test.old', 'utf-8')

        assert f6.exists()
        # We should not copy the file from the source dir, since this is
        # a derived file.
        assert test.read(['work', 'build', 'var2', 'test.out']) == bytearray('test.old', 'utf-8')

        f7 = fs.File('build/var1/test2.out')
        f8 = fs.File('build/var2/test2.out')

        assert not f7.exists()
        assert f7.rexists()
        r = f7.rfile().get_internal_path()
        expect = os.path.normpath(test.workpath('rep1/build/var1/test2.out'))
        assert r == expect, (repr(r), repr(expect))

        assert not f8.exists()
        assert f8.rexists()
        assert f8.rfile().get_internal_path() == os.path.normpath(test.workpath('rep1/build/var2/test2.out')), \
            f8.rfile().get_internal_path()

        # Verify the Mkdir and Link actions are called
        d9 = fs.Dir('build/var2/new_dir')
        f9 = fs.File('build/var2/new_dir/test9.out')

        class MkdirAction(Action):
            def __init__(self, dir_made):
                self.dir_made = dir_made

            def __call__(self, target, source, env, executor=None):
                if executor:
                    target = executor.get_all_targets()
                    source = executor.get_all_sources()
                self.dir_made.extend(target)

        save_Link = SCons.Node.FS.Link
        link_made = []

        def link_func(target, source, env, link_made=link_made):
            link_made.append(target)

        SCons.Node.FS.Link = link_func

        try:
            dir_made = []
            d9.builder = Builder(fs.Dir, action=MkdirAction(dir_made))
            d9.reset_executor()
            f9.exists()
            expect = os.path.join('build', 'var2', 'new_dir')
            assert dir_made[0].get_internal_path() == expect, dir_made[0].get_internal_path()
            expect = os.path.join('build', 'var2', 'new_dir', 'test9.out')
            assert link_made[0].get_internal_path() == expect, link_made[0].get_internal_path()
            assert f9.linked
        finally:
            SCons.Node.FS.Link = save_Link

        # Test for an interesting pathological case...we have a source
        # file in a build path, but not in a source path.  This can
        # happen if you switch from duplicate=1 to duplicate=0, then
        # delete a source file.  At one time, this would cause exists()
        # to return a 1 but get_contents() to throw.
        test.write(['work', 'build', 'var1', 'asourcefile'], 'stuff')
        f10 = fs.File('build/var1/asourcefile')
        assert f10.exists()
        assert f10.get_contents() == bytearray('stuff', 'utf-8'), f10.get_contents()

        f11 = fs.File('src/file11')
        t, m = f11.alter_targets()
        bdt = [n.get_internal_path() for n in t]
        var1_file11 = os.path.normpath('build/var1/file11')
        var2_file11 = os.path.normpath('build/var2/file11')
        assert bdt == [var1_file11, var2_file11], bdt

        f12 = fs.File('src/file12')
        f12.builder = 1
        bdt, m = f12.alter_targets()
        assert bdt == [], [n.get_internal_path() for n in bdt]

        d13 = fs.Dir('src/new_dir')
        t, m = d13.alter_targets()
        bdt = [n.get_internal_path() for n in t]
        var1_new_dir = os.path.normpath('build/var1/new_dir')
        var2_new_dir = os.path.normpath('build/var2/new_dir')
        assert bdt == [var1_new_dir, var2_new_dir], bdt

        # Test that an IOError trying to Link a src file
        # into a VariantDir ends up throwing a StopError.
        fIO = fs.File("build/var2/IOError")

        save_Link = SCons.Node.FS.Link

        def Link_IOError(target, source, env):
            raise IOError(17, "Link_IOError")

        SCons.Node.FS.Link = SCons.Action.Action(Link_IOError, None)

        test.write(['work', 'src', 'IOError'], "work/src/IOError\n")

        try:
            exc_caught = 0
            try:
                fIO.exists()
            except SCons.Errors.StopError:
                exc_caught = 1
            assert exc_caught, "Should have caught a StopError"

        finally:
            SCons.Node.FS.Link = save_Link

        # Test to see if Link() works...
        test.subdir('src', 'build')
        test.write('src/foo', 'src/foo\n')
        os.chmod(test.workpath('src/foo'), stat.S_IRUSR)
        SCons.Node.FS.Link(fs.File(test.workpath('build/foo')),
                           fs.File(test.workpath('src/foo')),
                           None)
        os.chmod(test.workpath('src/foo'), stat.S_IRUSR | stat.S_IWRITE)
        st = os.stat(test.workpath('build/foo'))
        assert (stat.S_IMODE(st.st_mode) & stat.S_IWRITE), \
            stat.S_IMODE(st.st_mode)

        # This used to generate a UserError when we forbid the source
        # directory from being outside the top-level SConstruct dir.
        fs = SCons.Node.FS.FS()
        fs.VariantDir('build', '/test/foo')

        exc_caught = 0
        try:
            try:
                fs = SCons.Node.FS.FS()
                fs.VariantDir('build', 'build/src')
            except SCons.Errors.UserError:
                exc_caught = 1
            assert exc_caught, "Should have caught a UserError."
        finally:
            test.unlink("src/foo")
            test.unlink("build/foo")

        fs = SCons.Node.FS.FS()
        fs.VariantDir('build', 'src1')

        # Calling the same VariantDir twice should work fine.
        fs.VariantDir('build', 'src1')

        # Trying to move a variant dir to a second source dir
        # should blow up
        try:
            fs.VariantDir('build', 'src2')
        except SCons.Errors.UserError:
            pass
        else:
            assert 0, "Should have caught a UserError."

        # Test against a former bug.  Make sure we can get a repository
        # path for the variant directory itself!
        fs = SCons.Node.FS.FS(test.workpath('work'))

        # not needed this subdir is created above near line 188
        # test.subdir('work')
        fs.VariantDir('build/var3', 'src', duplicate=0)
        d1 = fs.Dir('build/var3')
        r = d1.rdir()
        assert r == d1, "%s != %s" % (r, d1)

        # verify the link creation attempts in file_link()
        class LinkSimulator:
            """A class to intercept os.[sym]link() calls and track them."""

            def __init__(self, duplicate, link, symlink, copy):
                self.duplicate = duplicate
                self.have = {'hard': link, 'soft': symlink, 'copy': copy}

                self.links_to_be_called = []
                for link in self.duplicate.split('-'):
                    if self.have[link]:
                        self.links_to_be_called.append(link)

            def link_fail(self, src, dest):
                next_link = self.links_to_be_called.pop(0)
                assert next_link == "hard", \
                    "Wrong link order: expected %s to be called " \
                    "instead of hard" % next_link
                raise OSError("Simulating hard link creation error.")

            def symlink_fail(self, src, dest):
                next_link = self.links_to_be_called.pop(0)
                assert next_link == "soft", \
                    "Wrong link order: expected %s to be called " \
                    "instead of soft" % next_link
                raise OSError("Simulating symlink creation error.")

            def copy(self, src, dest):
                next_link = self.links_to_be_called.pop(0)
                assert next_link == "copy", \
                    "Wrong link order: expected %s to be called " \
                    "instead of copy" % next_link
                # copy succeeds, but use the real copy
                self.have['copy'](src, dest)

        # end class LinkSimulator

        try:
            SCons.Node.FS.set_duplicate("no-link-order")
            assert 0, "Expected exception when passing an invalid duplicate to set_duplicate"
        except SCons.Errors.InternalError:
            pass

        for duplicate in SCons.Node.FS.Valid_Duplicates:
            # save the real functions for later restoration
            try:
                real_link = os.link
            except AttributeError:
                real_link = None
            try:
                real_symlink = os.symlink
            except AttributeError:
                real_symlink = None

            # Disable symlink and link for now in win32.
            # We don't have a consistant plan to make these work as yet
            # They are only supported with PY3
            if sys.platform == 'win32':
                real_symlink = None
                real_link = None

            real_copy = shutil.copy2

            simulator = LinkSimulator(duplicate, real_link, real_symlink, real_copy)

            # override the real functions with our simulation
            os.link = simulator.link_fail
            os.symlink = simulator.symlink_fail
            shutil.copy2 = simulator.copy

            try:

                SCons.Node.FS.set_duplicate(duplicate)

                src_foo = test.workpath('src', 'foo')
                build_foo = test.workpath('build', 'foo')

                test.write(src_foo, 'src/foo\n')
                os.chmod(src_foo, stat.S_IRUSR)
                try:
                    SCons.Node.FS.Link(fs.File(build_foo),
                                       fs.File(src_foo),
                                       None)
                finally:
                    os.chmod(src_foo, stat.S_IRUSR | stat.S_IWRITE)
                    test.unlink(src_foo)
                    test.unlink(build_foo)

            finally:
                # restore the real functions
                if real_link:
                    os.link = real_link
                else:
                    delattr(os, 'link')
                if real_symlink:
                    os.symlink = real_symlink
                else:
                    delattr(os, 'symlink')
                shutil.copy2 = real_copy

        # Test VariantDir "reflection," where a same-named subdirectory
        # exists underneath a variant_dir.
        fs = SCons.Node.FS.FS()
        fs.VariantDir('work/src/b1/b2', 'work/src')

        dir_list = [
            'work/src',
            'work/src/b1',
            'work/src/b1/b2',
            'work/src/b1/b2/b1',
            'work/src/b1/b2/b1/b2',
            'work/src/b1/b2/b1/b2/b1',
            'work/src/b1/b2/b1/b2/b1/b2',
        ]

        srcnode_map = {
            'work/src/b1/b2': 'work/src',
            'work/src/b1/b2/f': 'work/src/f',
            'work/src/b1/b2/b1': 'work/src/b1/',
            'work/src/b1/b2/b1/f': 'work/src/b1/f',
            'work/src/b1/b2/b1/b2': 'work/src/b1/b2',
            'work/src/b1/b2/b1/b2/f': 'work/src/b1/b2/f',
            'work/src/b1/b2/b1/b2/b1': 'work/src/b1/b2/b1',
            'work/src/b1/b2/b1/b2/b1/f': 'work/src/b1/b2/b1/f',
            'work/src/b1/b2/b1/b2/b1/b2': 'work/src/b1/b2/b1/b2',
            'work/src/b1/b2/b1/b2/b1/b2/f': 'work/src/b1/b2/b1/b2/f',
        }

        alter_map = {
            'work/src': 'work/src/b1/b2',
            'work/src/f': 'work/src/b1/b2/f',
            'work/src/b1': 'work/src/b1/b2/b1',
            'work/src/b1/f': 'work/src/b1/b2/b1/f',
        }

        errors = 0

        for dir in dir_list:
            dnode = fs.Dir(dir)
            f = dir + '/f'
            fnode = fs.File(dir + '/f')

            dp = dnode.srcnode().get_internal_path()
            expect = os.path.normpath(srcnode_map.get(dir, dir))
            if dp != expect:
                print("Dir `%s' srcnode() `%s' != expected `%s'" % (dir, dp, expect))
                errors = errors + 1

            fp = fnode.srcnode().get_internal_path()
            expect = os.path.normpath(srcnode_map.get(f, f))
            if fp != expect:
                print("File `%s' srcnode() `%s' != expected `%s'" % (f, fp, expect))
                errors = errors + 1

        for dir in dir_list:
            dnode = fs.Dir(dir)
            f = dir + '/f'
            fnode = fs.File(dir + '/f')

            t, m = dnode.alter_targets()
            tp = t[0].get_internal_path()
            expect = os.path.normpath(alter_map.get(dir, dir))
            if tp != expect:
                print("Dir `%s' alter_targets() `%s' != expected `%s'" % (dir, tp, expect))
                errors = errors + 1

            t, m = fnode.alter_targets()
            tp = t[0].get_internal_path()
            expect = os.path.normpath(alter_map.get(f, f))
            if tp != expect:
                print("File `%s' alter_targets() `%s' != expected `%s'" % (f, tp, expect))
                errors = errors + 1

        self.assertFalse(errors)


class BaseTestCase(_tempdirTestCase):
    def test_stat(self):
        """Test the Base.stat() method"""
        test = self.test
        test.write("e1", "e1\n")
        fs = SCons.Node.FS.FS()

        e1 = fs.Entry('e1')
        s = e1.stat()
        assert s is not None, s

        e2 = fs.Entry('e2')
        s = e2.stat()
        assert s is None, s

    def test_getmtime(self):
        """Test the Base.getmtime() method"""
        test = self.test
        test.write("file", "file\n")
        fs = SCons.Node.FS.FS()

        file = fs.Entry('file')
        assert file.getmtime()

        file = fs.Entry('nonexistent')
        mtime = file.getmtime()
        assert mtime is None, mtime

    def test_getsize(self):
        """Test the Base.getsize() method"""
        test = self.test
        test.write("file", "file\n")
        fs = SCons.Node.FS.FS()

        file = fs.Entry('file')
        size = file.getsize()
        assert size == 5, size

        file = fs.Entry('nonexistent')
        size = file.getsize()
        assert size is None, size

    def test_isdir(self):
        """Test the Base.isdir() method"""
        test = self.test
        test.subdir('dir')
        test.write("file", "file\n")
        fs = SCons.Node.FS.FS()

        dir = fs.Entry('dir')
        assert dir.isdir()

        file = fs.Entry('file')
        assert not file.isdir()

        nonexistent = fs.Entry('nonexistent')
        assert not nonexistent.isdir()

    def test_isfile(self):
        """Test the Base.isfile() method"""
        test = self.test
        test.subdir('dir')
        test.write("file", "file\n")
        fs = SCons.Node.FS.FS()

        dir = fs.Entry('dir')
        assert not dir.isfile()

        file = fs.Entry('file')
        assert file.isfile()

        nonexistent = fs.Entry('nonexistent')
        assert not nonexistent.isfile()

    @unittest.skipUnless(sys.platform != 'win32' and hasattr(os, 'symlink'),
                         "symlink is not used on Windows")
    def test_islink(self):
        """Test the Base.islink() method"""
        test = self.test
        test.subdir('dir')
        test.write("file", "file\n")
        test.symlink("symlink", "symlink")
        fs = SCons.Node.FS.FS()

        dir = fs.Entry('dir')
        assert not dir.islink()

        file = fs.Entry('file')
        assert not file.islink()

        symlink = fs.Entry('symlink')
        assert symlink.islink()

        nonexistent = fs.Entry('nonexistent')
        assert not nonexistent.islink()


class DirNodeInfoTestCase(_tempdirTestCase):
    def test___init__(self):
        """Test DirNodeInfo initialization"""
        ddd = self.fs.Dir('ddd')
        ni = SCons.Node.FS.DirNodeInfo()


class DirBuildInfoTestCase(_tempdirTestCase):
    def test___init__(self):
        """Test DirBuildInfo initialization"""
        ddd = self.fs.Dir('ddd')
        bi = SCons.Node.FS.DirBuildInfo()


class FileNodeInfoTestCase(_tempdirTestCase):
    def test___init__(self):
        """Test FileNodeInfo initialization"""
        fff = self.fs.File('fff')
        ni = SCons.Node.FS.FileNodeInfo()
        assert isinstance(ni, SCons.Node.FS.FileNodeInfo)

    def test_update(self):
        """Test updating a File.NodeInfo with on-disk information"""
        test = self.test
        fff = self.fs.File('fff')

        ni = SCons.Node.FS.FileNodeInfo()

        test.write('fff', "fff\n")

        st = os.stat('fff')

        ni.update(fff)

        assert hasattr(ni, 'timestamp')
        assert hasattr(ni, 'size')

        ni.timestamp = 0
        ni.size = 0

        ni.update(fff)

        mtime = st[stat.ST_MTIME]
        assert ni.timestamp == mtime, (ni.timestamp, mtime)
        size = st.st_size
        assert ni.size == size, (ni.size, size)

        import time
        time.sleep(2)

        test.write('fff', "fff longer size, different time stamp\n")

        st = os.stat('fff')

        mtime = st[stat.ST_MTIME]
        assert ni.timestamp != mtime, (ni.timestamp, mtime)
        size = st.st_size
        assert ni.size != size, (ni.size, size)


class FileBuildInfoTestCase(_tempdirTestCase):
    def test___init__(self):
        """Test File.BuildInfo initialization"""
        fff = self.fs.File('fff')
        bi = SCons.Node.FS.FileBuildInfo()
        assert bi, bi

    def test_convert_to_sconsign(self):
        """Test converting to .sconsign file format"""
        fff = self.fs.File('fff')
        bi = SCons.Node.FS.FileBuildInfo()
        assert hasattr(bi, 'convert_to_sconsign')

    def test_convert_from_sconsign(self):
        """Test converting from .sconsign file format"""
        fff = self.fs.File('fff')
        bi = SCons.Node.FS.FileBuildInfo()
        assert hasattr(bi, 'convert_from_sconsign')

    def test_prepare_dependencies(self):
        """Test that we have a prepare_dependencies() method"""
        fff = self.fs.File('fff')
        bi = SCons.Node.FS.FileBuildInfo()
        bi.prepare_dependencies()

    def test_format(self):
        """Test the format() method"""
        f1 = self.fs.File('f1')
        bi1 = SCons.Node.FS.FileBuildInfo()

        self.fs.File('n1')
        self.fs.File('n2')
        self.fs.File('n3')

        s1sig = SCons.Node.FS.FileNodeInfo()
        s1sig.csig = 1
        d1sig = SCons.Node.FS.FileNodeInfo()
        d1sig.timestamp = 2
        i1sig = SCons.Node.FS.FileNodeInfo()
        i1sig.size = 3

        bi1.bsources = [self.fs.File('s1')]
        bi1.bdepends = [self.fs.File('d1')]
        bi1.bimplicit = [self.fs.File('i1')]
        bi1.bsourcesigs = [s1sig]
        bi1.bdependsigs = [d1sig]
        bi1.bimplicitsigs = [i1sig]
        bi1.bact = 'action'
        bi1.bactsig = 'actionsig'

        expect_lines = [
            's1: 1 None None',
            'd1: None 2 None',
            'i1: None None 3',
            'actionsig [action]',
        ]

        expect = '\n'.join(expect_lines)
        format = bi1.format()
        assert format == expect, (repr(expect), repr(format))


class FSTestCase(_tempdirTestCase):
    def test_needs_normpath(self):
        """Test the needs_normpath Regular expression

        This test case verifies that the regular expression used to
        determine whether a path needs normalization works as
        expected.
        """
        needs_normpath_match = SCons.Node.FS.needs_normpath_match

        do_not_need_normpath = [
            ".",
            "/",
            "/a",
            "/aa",
            "/a/",
            "/aa/",
            "/a/b",
            "/aa/bb",
            "/a/b/",
            "/aa/bb/",

            "",
            "a",
            "aa",
            "a/",
            "aa/",
            "a/b",
            "aa/bb",
            "a/b/",
            "aa/bb/",

            "a.",
            "a..",
            "/a.",
            "/a..",
            "a./",
            "a../",
            "/a./",
            "/a../",

            ".a",
            "..a",
            "/.a",
            "/..a",
            ".a/",
            "..a/",
            "/.a/",
            "/..a/",
        ]
        for p in do_not_need_normpath:
            assert needs_normpath_match(p) is None, p

        needs_normpath = [
            "//",
            "//a",
            "//aa",
            "//a/",
            "//a/",
            "/aa//",

            "//a/b",
            "//aa/bb",
            "//a/b/",
            "//aa/bb/",

            "/a//b",
            "/aa//bb",
            "/a/b//",
            "/aa/bb//",

            "/a/b//",
            "/aa/bb//",

            "a//",
            "aa//",
            "a//b",
            "aa//bb",
            "a//b/",
            "aa//bb/",
            "a/b//",
            "aa/bb//",

            "..",
            "/.",
            "/..",
            "./",
            "../",
            "/./",
            "/../",

            "a/.",
            "a/..",
            "./a",
            "../a",
            "a/./a",
            "a/../a",
        ]
        for p in needs_normpath:
            assert needs_normpath_match(p) is not None, p

    def test_runTest(self):
        """Test FS (file system) Node operations

        This test case handles all of the file system node
        tests in one environment, so we don't have to set up a
        complicated directory structure for each test individually.
        """
        test = self.test

        test.subdir('sub', ['sub', 'dir'])

        wp = test.workpath('')
        sub = test.workpath('sub', '')
        sub_dir = test.workpath('sub', 'dir', '')
        sub_dir_foo = test.workpath('sub', 'dir', 'foo', '')
        sub_dir_foo_bar = test.workpath('sub', 'dir', 'foo', 'bar', '')
        sub_foo = test.workpath('sub', 'foo', '')

        os.chdir(sub_dir)

        fs = SCons.Node.FS.FS()

        e1 = fs.Entry('e1')
        assert isinstance(e1, SCons.Node.FS.Entry)

        d1 = fs.Dir('d1')
        assert isinstance(d1, SCons.Node.FS.Dir)
        assert d1.cwd is d1, d1

        f1 = fs.File('f1', directory=d1)
        assert isinstance(f1, SCons.Node.FS.File)

        d1_f1 = os.path.join('d1', 'f1')
        assert f1.get_internal_path() == d1_f1, "f1.path %s != %s" % (f1.get_internal_path(), d1_f1)
        assert str(f1) == d1_f1, "str(f1) %s != %s" % (str(f1), d1_f1)

        x1 = d1.File('x1')
        assert isinstance(x1, SCons.Node.FS.File)
        assert str(x1) == os.path.join('d1', 'x1')

        x2 = d1.Dir('x2')
        assert isinstance(x2, SCons.Node.FS.Dir)
        assert str(x2) == os.path.join('d1', 'x2')

        x3 = d1.Entry('x3')
        assert isinstance(x3, SCons.Node.FS.Entry)
        assert str(x3) == os.path.join('d1', 'x3')

        assert d1.File(x1) == x1
        assert d1.Dir(x2) == x2
        assert d1.Entry(x3) == x3

        x1.cwd = d1

        x4 = x1.File('x4')
        assert str(x4) == os.path.join('d1', 'x4')

        x5 = x1.Dir('x5')
        assert str(x5) == os.path.join('d1', 'x5')

        x6 = x1.Entry('x6')
        assert str(x6) == os.path.join('d1', 'x6')
        x7 = x1.Entry('x7')
        assert str(x7) == os.path.join('d1', 'x7')

        assert x1.File(x4) == x4
        assert x1.Dir(x5) == x5
        assert x1.Entry(x6) == x6
        assert x1.Entry(x7) == x7

        assert x1.Entry(x5) == x5
        try:
            x1.File(x5)
        except TypeError:
            pass
        else:
            raise Exception("did not catch expected TypeError")

        assert x1.Entry(x4) == x4
        try:
            x1.Dir(x4)
        except TypeError:
            pass
        else:
            raise Exception("did not catch expected TypeError")

        x6 = x1.File(x6)
        assert isinstance(x6, SCons.Node.FS.File)

        x7 = x1.Dir(x7)
        assert isinstance(x7, SCons.Node.FS.Dir)

        seps = [os.sep]
        if os.sep != '/':
            seps = seps + ['/']

        drive, path = os.path.splitdrive(os.getcwd())

        def _do_Dir_test(lpath, path_, abspath_, up_path_, sep, fileSys=fs, drive=drive):
            dir = fileSys.Dir(lpath.replace('/', sep))

            if os.sep != '/':
                path_ = path_.replace('/', os.sep)
                abspath_ = abspath_.replace('/', os.sep)
                up_path_ = up_path_.replace('/', os.sep)

            def strip_slash(p, drive=drive):
                if p[-1] == os.sep and len(p) > 1:
                    p = p[:-1]
                if p[0] == os.sep:
                    p = drive + p
                return p

            path = strip_slash(path_)
            abspath = strip_slash(abspath_)
            up_path = strip_slash(up_path_)

            name = abspath.split(os.sep)[-1]

            if not name:
                if drive:
                    name = drive
                else:
                    name = os.sep

            if dir.up() is None:
                dir_up_path = dir.get_internal_path()
            else:
                dir_up_path = dir.up().get_internal_path()

            assert dir.name == name, \
                "dir.name %s != expected name %s" % \
                (dir.name, name)
            assert dir.get_internal_path() == path, \
                "dir.path %s != expected path %s" % \
                (dir.get_internal_path(), path)
            assert str(dir) == path, \
                "str(dir) %s != expected path %s" % \
                (str(dir), path)
            assert dir.get_abspath() == abspath, \
                "dir.abspath %s != expected absolute path %s" % \
                (dir.get_abspath(), abspath)
            assert dir_up_path == up_path, \
                "dir.up().path %s != expected parent path %s" % \
                (dir_up_path, up_path)

        for sep in seps:

            def Dir_test(lpath, path_, abspath_, up_path_, sep=sep, func=_do_Dir_test):
                return func(lpath, path_, abspath_, up_path_, sep)

            Dir_test('/', '/', '/', '/')
            Dir_test('', './', sub_dir, sub)
            Dir_test('foo', 'foo/', sub_dir_foo, './')
            Dir_test('foo/bar', 'foo/bar/', sub_dir_foo_bar, 'foo/')
            Dir_test('/foo', '/foo/', '/foo/', '/')
            Dir_test('/foo/bar', '/foo/bar/', '/foo/bar/', '/foo/')
            Dir_test('..', sub, sub, wp)
            Dir_test('foo/..', './', sub_dir, sub)
            Dir_test('../foo', sub_foo, sub_foo, sub)
            Dir_test('.', './', sub_dir, sub)
            Dir_test('./.', './', sub_dir, sub)
            Dir_test('foo/./bar', 'foo/bar/', sub_dir_foo_bar, 'foo/')
            Dir_test('#../foo', sub_foo, sub_foo, sub)
            Dir_test('#/../foo', sub_foo, sub_foo, sub)
            Dir_test('#foo/bar', 'foo/bar/', sub_dir_foo_bar, 'foo/')
            Dir_test('#/foo/bar', 'foo/bar/', sub_dir_foo_bar, 'foo/')
            Dir_test('#', './', sub_dir, sub)

            try:
                f2 = fs.File(sep.join(['f1', 'f2']), directory=d1)
            except TypeError as x:
                assert str(x) == ("Tried to lookup File '%s' as a Dir." %
                                  d1_f1), x
            except:
                raise

            try:
                dir = fs.Dir(sep.join(['d1', 'f1']))
            except TypeError as x:
                assert str(x) == ("Tried to lookup File '%s' as a Dir." %
                                  d1_f1), x
            except:
                raise

            try:
                f2 = fs.File('d1')
            except TypeError as x:
                assert str(x) == ("Tried to lookup Dir '%s' as a File." %
                                  'd1'), x
            except:
                raise

        # Test that just specifying the drive works to identify
        # its root directory.
        p = os.path.abspath(test.workpath('root_file'))
        drive, path = os.path.splitdrive(p)
        if drive:
            # The assert below probably isn't correct for the general
            # case, but it works for Windows, which covers a lot
            # of ground...
            dir = fs.Dir(drive)
            assert str(dir) == drive + os.sep, str(dir)

            # Make sure that lookups with and without the drive are
            # equivalent.
            p = os.path.abspath(test.workpath('some/file'))
            drive, path = os.path.splitdrive(p)

            e1 = fs.Entry(p)
            e2 = fs.Entry(path)
            assert e1 is e2, (e1, e2)
            a = str(e1)
            b = str(e2)
            assert a == b, ("Strings should match for same file/node\n%s\n%s" % (a, b))

        # Test for a bug in 0.04 that did not like looking up
        # dirs with a trailing slash on Windows.
        d = fs.Dir('./')
        assert d.get_internal_path() == '.', d.get_abspath()
        d = fs.Dir('foo/')
        assert d.get_internal_path() == 'foo', d.get_abspath()

        # Test for sub-classing of node building.
        global built_it

        built_it = None
        assert not built_it
        d1.add_source([SCons.Node.Node()])  # XXX FAKE SUBCLASS ATTRIBUTE
        d1.builder_set(Builder(fs.File))
        d1.reset_executor()
        d1.env_set(Environment())
        d1.build()
        assert built_it

        built_it = None
        assert not built_it
        f1.add_source([SCons.Node.Node()])  # XXX FAKE SUBCLASS ATTRIBUTE
        f1.builder_set(Builder(fs.File))
        f1.reset_executor()
        f1.env_set(Environment())
        f1.build()
        assert built_it

        def match(path, expect):
            expect = expect.replace('/', os.sep)
            assert path == expect, "path %s != expected %s" % (path, expect)

        e1 = fs.Entry("d1")
        assert e1.__class__.__name__ == 'Dir'
        match(e1.get_internal_path(), "d1")
        match(e1.dir.get_internal_path(), ".")

        e2 = fs.Entry("d1/f1")
        assert e2.__class__.__name__ == 'File'
        match(e2.get_internal_path(), "d1/f1")
        match(e2.dir.get_internal_path(), "d1")

        e3 = fs.Entry("e3")
        assert e3.__class__.__name__ == 'Entry'
        match(e3.get_internal_path(), "e3")
        match(e3.dir.get_internal_path(), ".")

        e4 = fs.Entry("d1/e4")
        assert e4.__class__.__name__ == 'Entry'
        match(e4.get_internal_path(), "d1/e4")
        match(e4.dir.get_internal_path(), "d1")

        e5 = fs.Entry("e3/e5")
        assert e3.__class__.__name__ == 'Dir'
        match(e3.get_internal_path(), "e3")
        match(e3.dir.get_internal_path(), ".")
        assert e5.__class__.__name__ == 'Entry'
        match(e5.get_internal_path(), "e3/e5")
        match(e5.dir.get_internal_path(), "e3")

        e6 = fs.Dir("d1/e4")
        assert e6 is e4
        assert e4.__class__.__name__ == 'Dir'
        match(e4.get_internal_path(), "d1/e4")
        match(e4.dir.get_internal_path(), "d1")

        e7 = fs.File("e3/e5")
        assert e7 is e5
        assert e5.__class__.__name__ == 'File'
        match(e5.get_internal_path(), "e3/e5")
        match(e5.dir.get_internal_path(), "e3")

        fs.chdir(fs.Dir('subdir'))
        f11 = fs.File("f11")
        match(f11.get_internal_path(), "subdir/f11")
        d12 = fs.Dir("d12")
        e13 = fs.Entry("subdir/e13")
        match(e13.get_internal_path(), "subdir/subdir/e13")
        fs.chdir(fs.Dir('..'))

        # Test scanning
        f1.builder_set(Builder(fs.File))
        f1.env_set(Environment())
        xyz = fs.File("xyz")
        f1.builder.target_scanner = Scanner(xyz)

        f1.scan()
        assert f1.implicit[0].get_internal_path() == "xyz"
        f1.implicit = []
        f1.scan()
        assert f1.implicit == []
        f1.implicit = None
        f1.scan()
        assert f1.implicit[0].get_internal_path() == "xyz"

        # Test underlying scanning functionality in get_found_includes()
        env = Environment()
        f12 = fs.File("f12")
        t1 = fs.File("t1")

        deps = f12.get_found_includes(env, None, t1)
        assert deps == [], deps

        class MyScanner(Scanner):
            call_count = 0

            def __call__(self, node, env, path):
                self.call_count = self.call_count + 1
                return Scanner.__call__(self, node, env, path)

        s = MyScanner(xyz)

        deps = f12.get_found_includes(env, s, t1)
        assert deps == [xyz], deps
        assert s.call_count == 1, s.call_count

        f12.built()

        deps = f12.get_found_includes(env, s, t1)
        assert deps == [xyz], deps
        assert s.call_count == 2, s.call_count

        env2 = Environment()

        deps = f12.get_found_includes(env2, s, t1)
        assert deps == [xyz], deps
        assert s.call_count == 3, s.call_count

        # Make sure we can scan this file even if the target isn't
        # a file that has a scanner (it might be an Alias, e.g.).
        class DummyNode:
            pass

        deps = f12.get_found_includes(env, s, DummyNode())
        assert deps == [xyz], deps

        # Test building a file whose directory is not there yet...
        f1 = fs.File(test.workpath("foo/bar/baz/ack"))
        assert not f1.dir.exists()
        f1.prepare()
        f1.build()
        assert f1.dir.exists()

        os.chdir('..')

        # Test getcwd()
        fs = SCons.Node.FS.FS()
        assert str(fs.getcwd()) == ".", str(fs.getcwd())
        fs.chdir(fs.Dir('subdir'))
        # The cwd's path is always "."
        assert str(fs.getcwd()) == ".", str(fs.getcwd())
        assert fs.getcwd().get_internal_path() == 'subdir', fs.getcwd().get_internal_path()
        fs.chdir(fs.Dir('../..'))
        assert fs.getcwd().get_internal_path() == test.workdir, fs.getcwd().get_internal_path()

        f1 = fs.File(test.workpath("do_i_exist"))
        assert not f1.exists()
        test.write("do_i_exist", "\n")
        assert not f1.exists(), "exists() call not cached"
        f1.built()
        assert f1.exists(), "exists() call caching not reset"
        test.unlink("do_i_exist")
        assert f1.exists()
        f1.built()
        assert not f1.exists()

        # For some reason, in Windows, the \x1a character terminates
        # the reading of files in text mode.  This tests that
        # get_contents() returns the binary contents.
        test.write("binary_file", "Foo\x1aBar")
        f1 = fs.File(test.workpath("binary_file"))
        assert f1.get_contents() == bytearray("Foo\x1aBar", 'utf-8'), \
            f1.get_contents()

        # This tests to make sure we can decode UTF-8 text files.
        test_string = "Foo\x1aBar"
        test.write("utf8_file", test_string.encode('utf-8'))
        f1 = fs.File(test.workpath("utf8_file"))
        assert f1.get_text_contents() == "Foo\x1aBar", \
            f1.get_text_contents()

        # Check for string which doesn't have BOM and isn't valid
        # ASCII
        test_string = b'Gan\xdfauge'
        test.write('latin1_file', test_string)
        f1 = fs.File(test.workpath("latin1_file"))
        assert f1.get_text_contents() == test_string.decode('latin-1'), \
            f1.get_text_contents()

        def nonexistent(method, s):
            try:
                x = method(s, create=0)
            except SCons.Errors.UserError:
                pass
            else:
                raise Exception("did not catch expected UserError")

        nonexistent(fs.Entry, 'nonexistent')
        nonexistent(fs.Entry, 'nonexistent/foo')

        nonexistent(fs.File, 'nonexistent')
        nonexistent(fs.File, 'nonexistent/foo')

        nonexistent(fs.Dir, 'nonexistent')
        nonexistent(fs.Dir, 'nonexistent/foo')

        test.write("preserve_me", "\n")
        assert os.path.exists(test.workpath("preserve_me"))
        f1 = fs.File(test.workpath("preserve_me"))
        f1.prepare()
        assert os.path.exists(test.workpath("preserve_me"))

        test.write("remove_me", "\n")
        assert os.path.exists(test.workpath("remove_me"))
        f1 = fs.File(test.workpath("remove_me"))
        f1.builder = Builder(fs.File)
        f1.env_set(Environment())
        f1.prepare()
        assert not os.path.exists(test.workpath("remove_me"))

        e = fs.Entry('e_local')
        assert not hasattr(e, '_local')
        e.set_local()
        assert e._local == 1
        f = fs.File('e_local')
        assert f._local == 1
        f = fs.File('f_local')
        assert f._local == 0

        # XXX test_is_up_to_date() for directories

        # XXX test_sconsign() for directories

        # XXX test_set_signature() for directories

        # XXX test_build() for directories

        # XXX test_root()

        # test Entry.get_contents()
        e = fs.Entry('does_not_exist')
        c = e.get_contents()
        assert c == "", c
        assert e.__class__ == SCons.Node.FS.Entry

        test.write("file", "file\n")
        try:
            e = fs.Entry('file')
            c = e.get_contents()
            assert c == bytearray("file\n", 'utf-8'), c
            assert e.__class__ == SCons.Node.FS.File
        finally:
            test.unlink("file")

        # test Entry.get_text_contents()
        e = fs.Entry('does_not_exist')
        c = e.get_text_contents()
        assert c == "", c
        assert e.__class__ == SCons.Node.FS.Entry

        test.write("file", "file\n")
        try:
            e = fs.Entry('file')
            c = e.get_text_contents()
            assert c == "file\n", c
            assert e.__class__ == SCons.Node.FS.File
        finally:
            test.unlink("file")

        test.subdir("dir")
        e = fs.Entry('dir')
        c = e.get_contents()
        assert c == "", c
        assert e.__class__ == SCons.Node.FS.Dir

        c = e.get_text_contents()
        try:
            eval('assert c == "", c')
        except SyntaxError:
            assert c == ""

        if sys.platform != 'win32' and hasattr(os, 'symlink'):
            os.symlink('nonexistent', test.workpath('dangling_symlink'))
            e = fs.Entry('dangling_symlink')
            c = e.get_contents()
            assert e.__class__ == SCons.Node.FS.Entry, e.__class__
            assert c == "", c
            c = e.get_text_contents()
            assert c == "", c

        test.write("tstamp", "tstamp\n")
        try:
            # Okay, *this* manipulation accomodates Windows FAT file systems
            # that only have two-second granularity on their timestamps.
            # We round down the current time to the nearest even integer
            # value, subtract two to make sure the timestamp is not "now,"
            # and then convert it back to a float.
            tstamp = float(int(time.time() // 2) * 2) - 2.0
            os.utime(test.workpath("tstamp"), (tstamp - 2.0, tstamp))
            f = fs.File("tstamp")
            t = f.get_timestamp()
            assert t == tstamp, "expected %f, got %f" % (tstamp, t)
        finally:
            test.unlink("tstamp")

        test.subdir('tdir1')
        d = fs.Dir('tdir1')
        t = d.get_timestamp()
        assert t == 0, "expected 0, got %s" % str(t)

        test.subdir('tdir2')
        f1 = test.workpath('tdir2', 'file1')
        f2 = test.workpath('tdir2', 'file2')
        test.write(f1, 'file1\n')
        test.write(f2, 'file2\n')
        current_time = float(int(time.time() // 2) * 2)
        t1 = current_time - 4.0
        t2 = current_time - 2.0
        os.utime(f1, (t1 - 2.0, t1))
        os.utime(f2, (t2 - 2.0, t2))
        d = fs.Dir('tdir2')
        fs.File(f1)
        fs.File(f2)
        t = d.get_timestamp()
        assert t == t2, "expected %f, got %f" % (t2, t)

        skey = fs.Entry('eee.x').scanner_key()
        assert skey == '.x', skey
        skey = fs.Entry('eee.xyz').scanner_key()
        assert skey == '.xyz', skey

        skey = fs.File('fff.x').scanner_key()
        assert skey == '.x', skey
        skey = fs.File('fff.xyz').scanner_key()
        assert skey == '.xyz', skey

        skey = fs.Dir('ddd.x').scanner_key()
        assert skey is None, skey

        test.write("i_am_not_a_directory", "\n")
        try:
            exc_caught = 0
            try:
                fs.Dir(test.workpath("i_am_not_a_directory"))
            except TypeError:
                exc_caught = 1
            assert exc_caught, "Should have caught a TypeError"
        finally:
            test.unlink("i_am_not_a_directory")

        exc_caught = 0
        try:
            fs.File(sub_dir)
        except TypeError:
            exc_caught = 1
        assert exc_caught, "Should have caught a TypeError"

        # XXX test_is_up_to_date()

        d = fs.Dir('dir')
        r = d.remove()
        assert r is None, r

        f = fs.File('does_not_exist')
        r = f.remove()
        assert r is None, r

        test.write('exists', "exists\n")
        f = fs.File('exists')
        r = f.remove()
        assert r, r
        assert not os.path.exists(test.workpath('exists')), "exists was not removed"

        if sys.platform != 'win32' and hasattr(os, 'symlink'):
            symlink = test.workpath('symlink')
            os.symlink(test.workpath('does_not_exist'), symlink)
            assert os.path.islink(symlink)
            f = fs.File('symlink')
            r = f.remove()
            assert r, r
            assert not os.path.islink(symlink), "symlink was not removed"

        test.write('can_not_remove', "can_not_remove\n")
        test.writable(test.workpath('.'), 0)
        fp = open(test.workpath('can_not_remove'))

        f = fs.File('can_not_remove')
        exc_caught = 0
        try:
            r = f.remove()
        except OSError:
            exc_caught = 1

        fp.close()

        assert exc_caught, "Should have caught an OSError, r = " + str(r)

        f = fs.Entry('foo/bar/baz')
        assert f.for_signature() == 'baz', f.for_signature()
        assert f.get_string(0) == os.path.normpath('foo/bar/baz'), \
            f.get_string(0)
        assert f.get_string(1) == 'baz', f.get_string(1)

    def test_drive_letters(self):
        """Test drive-letter look-ups"""

        test = self.test

        test.subdir('sub', ['sub', 'dir'])

        def drive_workpath(dirs, test=test):
            x = test.workpath(*dirs)
            drive, path = os.path.splitdrive(x)
            return 'X:' + path

        wp = drive_workpath([''])

        if wp[-1] in (os.sep, '/'):
            tmp = os.path.split(wp[:-1])[0]
        else:
            tmp = os.path.split(wp)[0]

        parent_tmp = os.path.split(tmp)[0]
        if parent_tmp == 'X:':
            parent_tmp = 'X:' + os.sep

        tmp_foo = os.path.join(tmp, 'foo')

        foo = drive_workpath(['foo'])
        foo_bar = drive_workpath(['foo', 'bar'])
        sub = drive_workpath(['sub', ''])
        sub_dir = drive_workpath(['sub', 'dir', ''])
        sub_dir_foo = drive_workpath(['sub', 'dir', 'foo', ''])
        sub_dir_foo_bar = drive_workpath(['sub', 'dir', 'foo', 'bar', ''])
        sub_foo = drive_workpath(['sub', 'foo', ''])

        fs = SCons.Node.FS.FS()

        seps = [os.sep]
        if os.sep != '/':
            seps = seps + ['/']

        def _do_Dir_test(lpath, path_, up_path_, sep, fileSys=fs):
            dir = fileSys.Dir(lpath.replace('/', sep))

            if os.sep != '/':
                path_ = path_.replace('/', os.sep)
                up_path_ = up_path_.replace('/', os.sep)

            def strip_slash(p):
                if p[-1] == os.sep and len(p) > 3:
                    p = p[:-1]
                return p

            path = strip_slash(path_)
            up_path = strip_slash(up_path_)
            name = path.split(os.sep)[-1]

            assert dir.name == name, \
                "dir.name %s != expected name %s" % \
                (dir.name, name)
            assert dir.get_internal_path() == path, \
                "dir.path %s != expected path %s" % \
                (dir.get_internal_path(), path)
            assert str(dir) == path, \
                "str(dir) %s != expected path %s" % \
                (str(dir), path)
            assert dir.up().get_internal_path() == up_path, \
                "dir.up().path %s != expected parent path %s" % \
                (dir.up().get_internal_path(), up_path)

        save_os_path = os.path
        save_os_sep = os.sep
        try:
            import ntpath
            os.path = ntpath
            os.sep = '\\'
            SCons.Node.FS.initialize_do_splitdrive()

            for sep in seps:
                def Dir_test(lpath, path_, up_path_, sep=sep, func=_do_Dir_test):
                    return func(lpath, path_, up_path_, sep)

                Dir_test('#X:', wp, tmp)
                Dir_test('X:foo', foo, wp)
                Dir_test('X:foo/bar', foo_bar, foo)
                Dir_test('X:/foo', 'X:/foo', 'X:/')
                Dir_test('X:/foo/bar', 'X:/foo/bar/', 'X:/foo/')
                Dir_test('X:..', tmp, parent_tmp)
                Dir_test('X:foo/..', wp, tmp)
                Dir_test('X:../foo', tmp_foo, tmp)
                Dir_test('X:.', wp, tmp)
                Dir_test('X:./.', wp, tmp)
                Dir_test('X:foo/./bar', foo_bar, foo)
                Dir_test('#X:../foo', tmp_foo, tmp)
                Dir_test('#X:/../foo', tmp_foo, tmp)
                Dir_test('#X:foo/bar', foo_bar, foo)
                Dir_test('#X:/foo/bar', foo_bar, foo)
                Dir_test('#X:/', wp, tmp)
        finally:
            os.path = save_os_path
            os.sep = save_os_sep
            SCons.Node.FS.initialize_do_splitdrive()

    def test_unc_path(self):
        """Test UNC path look-ups"""

        test = self.test

        test.subdir('sub', ['sub', 'dir'])

        def strip_slash(p):
            if p[-1] == os.sep and len(p) > 3:
                p = p[:-1]
            return p

        def unc_workpath(dirs, test=test):
            import ntpath
            x = test.workpath(*dirs)
            drive, path = ntpath.splitdrive(x)
            try:
                unc, path = ntpath.splitunc(path)
            except AttributeError:
                # could be python 3.7 or newer, make sure splitdrive can do UNC
                assert ntpath.splitdrive(r'\\split\drive\test')[0] == r'\\split\drive'
            path = strip_slash(path)
            return '//' + path[1:]

        wp = unc_workpath([''])

        if wp[-1] in (os.sep, '/'):
            tmp = os.path.split(wp[:-1])[0]
        else:
            tmp = os.path.split(wp)[0]

        parent_tmp = os.path.split(tmp)[0]

        tmp_foo = os.path.join(tmp, 'foo')

        foo = unc_workpath(['foo'])
        foo_bar = unc_workpath(['foo', 'bar'])
        sub = unc_workpath(['sub', ''])
        sub_dir = unc_workpath(['sub', 'dir', ''])
        sub_dir_foo = unc_workpath(['sub', 'dir', 'foo', ''])
        sub_dir_foo_bar = unc_workpath(['sub', 'dir', 'foo', 'bar', ''])
        sub_foo = unc_workpath(['sub', 'foo', ''])

        fs = SCons.Node.FS.FS()

        seps = [os.sep]
        if os.sep != '/':
            seps = seps + ['/']

        def _do_Dir_test(lpath, path, up_path, sep, fileSys=fs):
            dir = fileSys.Dir(lpath.replace('/', sep))

            if os.sep != '/':
                path = path.replace('/', os.sep)
                up_path = up_path.replace('/', os.sep)

            if path == os.sep + os.sep:
                name = os.sep + os.sep
            else:
                name = path.split(os.sep)[-1]

            if dir.up() is None:
                dir_up_path = dir.get_internal_path()
            else:
                dir_up_path = dir.up().get_internal_path()

            assert dir.name == name, \
                "dir.name %s != expected name %s" % \
                (dir.name, name)
            assert dir.get_internal_path() == path, \
                "dir.path %s != expected path %s" % \
                (dir.get_internal_path(), path)
            assert str(dir) == path, \
                "str(dir) %s != expected path %s" % \
                (str(dir), path)
            assert dir_up_path == up_path, \
                "dir.up().path %s != expected parent path %s" % \
                (dir.up().get_internal_path(), up_path)

        save_os_path = os.path
        save_os_sep = os.sep
        try:
            import ntpath
            os.path = ntpath
            os.sep = '\\'
            SCons.Node.FS.initialize_do_splitdrive()

            for sep in seps:
                def Dir_test(lpath, path_, up_path_, sep=sep, func=_do_Dir_test):
                    return func(lpath, path_, up_path_, sep)

                Dir_test('//foo', '//foo', '//')
                Dir_test('//foo/bar', '//foo/bar', '//foo')
                Dir_test('//', '//', '//')
                Dir_test('//..', '//', '//')
                Dir_test('//foo/..', '//', '//')
                Dir_test('//../foo', '//foo', '//')
                Dir_test('//.', '//', '//')
                Dir_test('//./.', '//', '//')
                Dir_test('//foo/./bar', '//foo/bar', '//foo')
                Dir_test('//foo/../bar', '//bar', '//')
                Dir_test('//foo/../../bar', '//bar', '//')
                Dir_test('//foo/bar/../..', '//', '//')
                Dir_test('#//', wp, tmp)
                Dir_test('#//../foo', tmp_foo, tmp)
                Dir_test('#//../foo', tmp_foo, tmp)
                Dir_test('#//foo/bar', foo_bar, foo)
                Dir_test('#//foo/bar', foo_bar, foo)
                Dir_test('#//', wp, tmp)
        finally:
            os.path = save_os_path
            os.sep = save_os_sep
            SCons.Node.FS.initialize_do_splitdrive()

    def test_target_from_source(self):
        """Test the method for generating target nodes from sources"""
        fs = self.fs

        x = fs.File('x.c')
        t = x.target_from_source('pre-', '-suf')
        assert str(t) == 'pre-x-suf', str(t)
        assert t.__class__ == SCons.Node.FS.Entry

        y = fs.File('dir/y')
        t = y.target_from_source('pre-', '-suf')
        assert str(t) == os.path.join('dir', 'pre-y-suf'), str(t)
        assert t.__class__ == SCons.Node.FS.Entry

        z = fs.File('zz')
        t = z.target_from_source('pre-', '-suf', lambda x: x[:-1])
        assert str(t) == 'pre-z-suf', str(t)
        assert t.__class__ == SCons.Node.FS.Entry

        d = fs.Dir('ddd')
        t = d.target_from_source('pre-', '-suf')
        assert str(t) == 'pre-ddd-suf', str(t)
        assert t.__class__ == SCons.Node.FS.Entry

        e = fs.Entry('eee')
        t = e.target_from_source('pre-', '-suf')
        assert str(t) == 'pre-eee-suf', str(t)
        assert t.__class__ == SCons.Node.FS.Entry

    def test_same_name(self):
        """Test that a local same-named file isn't found for a Dir lookup"""
        test = self.test
        fs = self.fs

        test.subdir('subdir')
        test.write(['subdir', 'build'], "subdir/build\n")

        subdir = fs.Dir('subdir')
        fs.chdir(subdir, change_os_dir=True)
        self.fs._lookup('#build/file', subdir, SCons.Node.FS.File)

    def test_above_root(self):
        """Testing looking up a path above the root directory"""
        test = self.test
        fs = self.fs

        d1 = fs.Dir('d1')
        d2 = d1.Dir('d2')
        dirs = os.path.normpath(d2.get_abspath()).split(os.sep)
        above_path = os.path.join(*['..'] * len(dirs) + ['above'])
        above = d2.Dir(above_path)

    def test_lookup_abs(self):
        """Exercise the _lookup_abs function"""
        test = self.test
        fs = self.fs

        root = fs.Dir('/')
        d = root._lookup_abs('/tmp/foo-nonexistent/nonexistent-dir', SCons.Node.FS.Dir)
        assert d.__class__ == SCons.Node.FS.Dir, str(d.__class__)

    @unittest.skipUnless(sys.platform == "win32", "requires Windows")
    def test_lookup_uncpath(self):
        """Testing looking up a UNC path on Windows"""
        test = self.test
        fs = self.fs
        path = '//servername/C$/foo'
        f = self.fs._lookup('//servername/C$/foo', fs.Dir('#'), SCons.Node.FS.File)
        # before the fix in this commit, this returned 'C:\servername\C$\foo'
        # Should be a normalized Windows UNC path as below.
        assert str(f) == r'\\servername\C$\foo', \
            'UNC path %s got looked up as %s' % (path, f)

    @unittest.skipUnless(sys.platform.startswith == "win32", "requires Windows")
    def test_unc_drive_letter(self):
        """Test drive-letter lookup for windows UNC-style directories"""
        share = self.fs.Dir(r'\\SERVER\SHARE\Directory')
        assert str(share) == r'\\SERVER\SHARE\Directory', str(share)

    @unittest.skipUnless(sys.platform == "win32", "requires Windows")
    def test_UNC_dirs_2689(self):
        """Test some UNC dirs that printed incorrectly and/or caused
        infinite recursion errors prior to r5180 (SCons 2.1)."""
        fs = self.fs
        p = fs.Dir(r"\\computername\sharename").get_abspath()
        assert p == r"\\computername\sharename", p
        p = fs.Dir(r"\\\computername\sharename").get_abspath()
        assert p == r"\\computername\sharename", p

    def test_rel_path(self):
        """Test the rel_path() method"""
        test = self.test
        fs = self.fs

        d1 = fs.Dir('d1')
        d1_f = d1.File('f')
        d1_d2 = d1.Dir('d2')
        d1_d2_f = d1_d2.File('f')

        d3 = fs.Dir('d3')
        d3_f = d3.File('f')
        d3_d4 = d3.Dir('d4')
        d3_d4_f = d3_d4.File('f')

        cases = [
            d1, d1, '.',
            d1, d1_f, 'f',
            d1, d1_d2, 'd2',
            d1, d1_d2_f, 'd2/f',
            d1, d3, '../d3',
            d1, d3_f, '../d3/f',
            d1, d3_d4, '../d3/d4',
            d1, d3_d4_f, '../d3/d4/f',

            d1_f, d1, '.',
            d1_f, d1_f, 'f',
            d1_f, d1_d2, 'd2',
            d1_f, d1_d2_f, 'd2/f',
            d1_f, d3, '../d3',
            d1_f, d3_f, '../d3/f',
            d1_f, d3_d4, '../d3/d4',
            d1_f, d3_d4_f, '../d3/d4/f',

            d1_d2, d1, '..',
            d1_d2, d1_f, '../f',
            d1_d2, d1_d2, '.',
            d1_d2, d1_d2_f, 'f',
            d1_d2, d3, '../../d3',
            d1_d2, d3_f, '../../d3/f',
            d1_d2, d3_d4, '../../d3/d4',
            d1_d2, d3_d4_f, '../../d3/d4/f',

            d1_d2_f, d1, '..',
            d1_d2_f, d1_f, '../f',
            d1_d2_f, d1_d2, '.',
            d1_d2_f, d1_d2_f, 'f',
            d1_d2_f, d3, '../../d3',
            d1_d2_f, d3_f, '../../d3/f',
            d1_d2_f, d3_d4, '../../d3/d4',
            d1_d2_f, d3_d4_f, '../../d3/d4/f',
        ]

        if sys.platform in ('win32',):
            x_d1 = fs.Dir(r'X:\d1')
            x_d1_d2 = x_d1.Dir('d2')
            y_d1 = fs.Dir(r'Y:\d1')
            y_d1_d2 = y_d1.Dir('d2')
            y_d2 = fs.Dir(r'Y:\d2')

            win32_cases = [
                x_d1, x_d1, '.',
                x_d1, x_d1_d2, 'd2',
                x_d1, y_d1, r'Y:\d1',
                x_d1, y_d1_d2, r'Y:\d1\d2',
                x_d1, y_d2, r'Y:\d2',
            ]

            cases.extend(win32_cases)

        failed = 0
        while cases:
            dir, other, expect = cases[:3]
            expect = os.path.normpath(expect)
            del cases[:3]
            result = dir.rel_path(other)
            if result != expect:
                if failed == 0: print()
                fmt = "    dir_path(%(dir)s, %(other)s) => '%(result)s' did not match '%(expect)s'"
                print(fmt % locals())
                failed = failed + 1
        assert failed == 0, "%d rel_path() cases failed" % failed

    def test_proxy(self):
        """Test a Node.FS object wrapped in a proxy instance"""
        f1 = self.fs.File('fff')

        class MyProxy(SCons.Util.Proxy):
            __str__ = SCons.Util.Delegate('__str__')

        p = MyProxy(f1)
        f2 = self.fs.Entry(p)
        assert f1 is f2, (f1, str(f1), f2, str(f2))


class DirTestCase(_tempdirTestCase):

    def test__morph(self):
        """Test handling of actions when morphing an Entry into a Dir"""
        test = self.test
        e = self.fs.Entry('eee')
        x = e.get_executor()
        x.add_pre_action('pre')
        x.add_post_action('post')
        e.must_be_same(SCons.Node.FS.Dir)
        a = x.get_action_list()
        assert 'pre' in a, a
        assert 'post' in a, a

    def test_subclass(self):
        """Test looking up subclass of Dir nodes"""

        class DirSubclass(SCons.Node.FS.Dir):
            pass

        sd = self.fs._lookup('special_dir', None, DirSubclass, create=1)
        sd.must_be_same(SCons.Node.FS.Dir)

    def test_get_env_scanner(self):
        """Test the Dir.get_env_scanner() method
        """
        import SCons.Defaults
        d = self.fs.Dir('ddd')
        s = d.get_env_scanner(Environment())
        assert s is SCons.Defaults.DirEntryScanner, s

    def test_get_target_scanner(self):
        """Test the Dir.get_target_scanner() method
        """
        import SCons.Defaults
        d = self.fs.Dir('ddd')
        s = d.get_target_scanner()
        assert s is SCons.Defaults.DirEntryScanner, s

    def test_scan(self):
        """Test scanning a directory for in-memory entries
        """
        fs = self.fs

        dir = fs.Dir('ddd')
        fs.File(os.path.join('ddd', 'f1'))
        fs.File(os.path.join('ddd', 'f2'))
        fs.File(os.path.join('ddd', 'f3'))
        fs.Dir(os.path.join('ddd', 'd1'))
        fs.Dir(os.path.join('ddd', 'd1', 'f4'))
        fs.Dir(os.path.join('ddd', 'd1', 'f5'))
        dir.scan()
        kids = sorted([x.get_internal_path() for x in dir.children(None)])
        assert kids == [os.path.join('ddd', 'd1'),
                        os.path.join('ddd', 'f1'),
                        os.path.join('ddd', 'f2'),
                        os.path.join('ddd', 'f3')], kids

    def test_get_contents(self):
        """Test getting the contents for a directory.
        """
        test = self.test

        test.subdir('d')
        test.write(['d', 'g'], "67890\n")
        test.write(['d', 'f'], "12345\n")
        test.subdir(['d', 'sub'])
        test.write(['d', 'sub', 'h'], "abcdef\n")
        test.subdir(['d', 'empty'])

        d = self.fs.Dir('d')
        g = self.fs.File(os.path.join('d', 'g'))
        f = self.fs.File(os.path.join('d', 'f'))
        h = self.fs.File(os.path.join('d', 'sub', 'h'))
        e = self.fs.Dir(os.path.join('d', 'empty'))
        s = self.fs.Dir(os.path.join('d', 'sub'))

        files = d.get_contents().split('\n')

        assert e.get_contents() == '', e.get_contents()
        assert e.get_text_contents() == '', e.get_text_contents()
        assert e.get_csig() + " empty" == files[0], files
        assert f.get_csig() + " f" == files[1], files
        assert g.get_csig() + " g" == files[2], files
        assert s.get_csig() + " sub" == files[3], files

    def test_hash_chunksize(self):
        """
        Test verifying that File.get_csig() correctly uses hash_chunksize. This
        variable is documented as the hash chunksize in kilobytes. This test
        verifies that if the file size is less than the hash chunksize,
        get_contents() is called; otherwise, it verifies that get_contents()
        is not called.
        """
        chunksize_bytes = SCons.Node.FS.File.hash_chunksize
        test = self.test

        test.subdir('chunksize_dir')
        test.write(['chunksize_dir', 'f1'], 'a' * (chunksize_bytes // 1024 - 1))
        test.write(['chunksize_dir', 'f2'], 'a' * (chunksize_bytes // 1024))
        test.write(['chunksize_dir', 'f3'], 'a' * (chunksize_bytes + 1))

        dir = self.fs.Dir('chunksize_dir')
        f1 = dir.File('f1')
        f2 = dir.File('f2')
        f3 = dir.File('f3')

        # Expect f1 and f2 to call get_contents(), while f3 will not because it
        # should do reads of chunksize kilobytes at a time.
        expected_get_contents_calls = {f1, f2}
        self.actual_get_contents_calls = 0

        def get_contents_override(file_object):
            self.actual_get_contents_calls += 1
            if file_object in expected_get_contents_calls:
                return file_object._old_get_contents()
            else:
                raise Exception('get_contents was unexpectedly called on node '
                                '%s' % file_object)

        SCons.Node.FS.File._old_get_contents = SCons.Node.FS.File.get_contents
        SCons.Node.FS.File.get_contents = get_contents_override.__get__(
            None, SCons.Node.FS)

        # Call get_csig() to test get_contents() usage. The actual results of
        # the calls to get_csig() are not relevant for this test. If an
        # exception is raised, we must first reset the get_contents function
        # before reraising it or other tests will fail too.
        exception = None
        try:
            f1.get_csig()
            f2.get_csig()
            f3.get_csig()
        except Exception as e:
            exception = e

        SCons.Node.FS.File.get_contents = SCons.Node.FS.File._old_get_contents
        delattr(SCons.Node.FS.File, '_old_get_contents')

        if exception:
            raise exception

        assert self.actual_get_contents_calls == len(expected_get_contents_calls), \
            self.actual_get_contents_calls

    def test_implicit_re_scans(self):
        """Test that adding entries causes a directory to be re-scanned
        """

        fs = self.fs

        dir = fs.Dir('ddd')

        fs.File(os.path.join('ddd', 'f1'))
        dir.scan()
        kids = sorted([x.get_internal_path() for x in dir.children()])
        assert kids == [os.path.join('ddd', 'f1')], kids

        fs.File(os.path.join('ddd', 'f2'))
        dir.scan()
        kids = sorted([x.get_internal_path() for x in dir.children()])
        assert kids == [os.path.join('ddd', 'f1'),
                        os.path.join('ddd', 'f2')], kids

    def test_entry_exists_on_disk(self):
        """Test the Dir.entry_exists_on_disk() method
        """
        test = self.test

        does_not_exist = self.fs.Dir('does_not_exist')
        assert not does_not_exist.entry_exists_on_disk('foo')

        test.subdir('d')
        test.write(['d', 'exists'], "d/exists\n")
        test.write(['d', 'Case-Insensitive'], "d/Case-Insensitive\n")

        d = self.fs.Dir('d')
        assert d.entry_exists_on_disk('exists')
        assert not d.entry_exists_on_disk('does_not_exist')

        if os.path.normcase("TeSt") != os.path.normpath("TeSt") or sys.platform == "cygwin":
            assert d.entry_exists_on_disk('case-insensitive')

    def test_rentry_exists_on_disk(self):
        """Test the Dir.rentry_exists_on_disk() method
        """
        test = self.test

        does_not_exist = self.fs.Dir('does_not_exist')
        assert not does_not_exist.rentry_exists_on_disk('foo')

        test.subdir('d')
        test.write(['d', 'exists'], "d/exists\n")
        test.write(['d', 'Case-Insensitive'], "d/Case-Insensitive\n")

        test.subdir('r')
        test.write(['r', 'rexists'], "r/rexists\n")

        d = self.fs.Dir('d')
        r = self.fs.Dir('r')
        d.addRepository(r)

        assert d.rentry_exists_on_disk('exists')
        assert d.rentry_exists_on_disk('rexists')
        assert not d.rentry_exists_on_disk('does_not_exist')

        if os.path.normcase("TeSt") != os.path.normpath("TeSt") or sys.platform == "cygwin":
            assert d.rentry_exists_on_disk('case-insensitive')

    def test_srcdir_list(self):
        """Test the Dir.srcdir_list() method
        """
        src = self.fs.Dir('src')
        bld = self.fs.Dir('bld')
        sub1 = bld.Dir('sub')
        sub2 = sub1.Dir('sub')
        sub3 = sub2.Dir('sub')
        self.fs.VariantDir(bld, src, duplicate=0)
        self.fs.VariantDir(sub2, src, duplicate=0)

        def check(result, expect):
            result = list(map(str, result))
            expect = list(map(os.path.normpath, expect))
            assert result == expect, result

        s = src.srcdir_list()
        check(s, [])

        s = bld.srcdir_list()
        check(s, ['src'])

        s = sub1.srcdir_list()
        check(s, ['src/sub'])

        s = sub2.srcdir_list()
        check(s, ['src', 'src/sub/sub'])

        s = sub3.srcdir_list()
        check(s, ['src/sub', 'src/sub/sub/sub'])

        self.fs.VariantDir('src/b1/b2', 'src')
        b1 = src.Dir('b1')
        b1_b2 = b1.Dir('b2')
        b1_b2_b1 = b1_b2.Dir('b1')
        b1_b2_b1_b2 = b1_b2_b1.Dir('b2')
        b1_b2_b1_b2_sub = b1_b2_b1_b2.Dir('sub')

        s = b1.srcdir_list()
        check(s, [])

        s = b1_b2.srcdir_list()
        check(s, ['src'])

        s = b1_b2_b1.srcdir_list()
        check(s, ['src/b1'])

        s = b1_b2_b1_b2.srcdir_list()
        check(s, ['src/b1/b2'])

        s = b1_b2_b1_b2_sub.srcdir_list()
        check(s, ['src/b1/b2/sub'])

    def test_srcdir_duplicate(self):
        """Test the Dir.srcdir_duplicate() method
        """
        test = self.test

        test.subdir('src0')
        test.write(['src0', 'exists'], "src0/exists\n")

        bld0 = self.fs.Dir('bld0')
        src0 = self.fs.Dir('src0')
        self.fs.VariantDir(bld0, src0, duplicate=0)

        n = bld0.srcdir_duplicate('does_not_exist')
        assert n is None, n
        assert not os.path.exists(test.workpath('bld0', 'does_not_exist'))

        n = bld0.srcdir_duplicate('exists')
        assert str(n) == os.path.normpath('src0/exists'), str(n)
        assert not os.path.exists(test.workpath('bld0', 'exists'))

        test.subdir('src1')
        test.write(['src1', 'exists'], "src0/exists\n")

        bld1 = self.fs.Dir('bld1')
        src1 = self.fs.Dir('src1')
        self.fs.VariantDir(bld1, src1, duplicate=1)

        n = bld1.srcdir_duplicate('does_not_exist')
        assert n is None, n
        assert not os.path.exists(test.workpath('bld1', 'does_not_exist'))

        n = bld1.srcdir_duplicate('exists')
        assert str(n) == os.path.normpath('bld1/exists'), str(n)
        assert os.path.exists(test.workpath('bld1', 'exists'))

    def test_srcdir_find_file(self):
        """Test the Dir.srcdir_find_file() method
        """
        test = self.test

        def return_true(node):
            return 1

        SCons.Node._is_derived_map[2] = return_true
        SCons.Node._exists_map[5] = return_true

        test.subdir('src0')
        test.write(['src0', 'on-disk-f1'], "src0/on-disk-f1\n")
        test.write(['src0', 'on-disk-f2'], "src0/on-disk-f2\n")
        test.write(['src0', 'on-disk-e1'], "src0/on-disk-e1\n")
        test.write(['src0', 'on-disk-e2'], "src0/on-disk-e2\n")

        bld0 = self.fs.Dir('bld0')
        src0 = self.fs.Dir('src0')
        self.fs.VariantDir(bld0, src0, duplicate=0)

        derived_f = src0.File('derived-f')
        derived_f._func_is_derived = 2
        exists_f = src0.File('exists-f')
        exists_f._func_exists = 5

        derived_e = src0.Entry('derived-e')
        derived_e._func_is_derived = 2
        exists_e = src0.Entry('exists-e')
        exists_e._func_exists = 5

        def check(result, expect):
            result = list(map(str, result))
            expect = list(map(os.path.normpath, expect))
            assert result == expect, result

        # First check from the source directory.
        n = src0.srcdir_find_file('does_not_exist')
        assert n == (None, None), n

        n = src0.srcdir_find_file('derived-f')
        check(n, ['src0/derived-f', 'src0'])
        n = src0.srcdir_find_file('exists-f')
        check(n, ['src0/exists-f', 'src0'])
        n = src0.srcdir_find_file('on-disk-f1')
        check(n, ['src0/on-disk-f1', 'src0'])

        n = src0.srcdir_find_file('derived-e')
        check(n, ['src0/derived-e', 'src0'])
        n = src0.srcdir_find_file('exists-e')
        check(n, ['src0/exists-e', 'src0'])
        n = src0.srcdir_find_file('on-disk-e1')
        check(n, ['src0/on-disk-e1', 'src0'])

        # Now check from the variant directory.
        n = bld0.srcdir_find_file('does_not_exist')
        assert n == (None, None), n

        n = bld0.srcdir_find_file('derived-f')
        check(n, ['src0/derived-f', 'bld0'])
        n = bld0.srcdir_find_file('exists-f')
        check(n, ['src0/exists-f', 'bld0'])
        n = bld0.srcdir_find_file('on-disk-f2')
        check(n, ['src0/on-disk-f2', 'bld0'])

        n = bld0.srcdir_find_file('derived-e')
        check(n, ['src0/derived-e', 'bld0'])
        n = bld0.srcdir_find_file('exists-e')
        check(n, ['src0/exists-e', 'bld0'])
        n = bld0.srcdir_find_file('on-disk-e2')
        check(n, ['src0/on-disk-e2', 'bld0'])

        test.subdir('src1')
        test.write(['src1', 'on-disk-f1'], "src1/on-disk-f1\n")
        test.write(['src1', 'on-disk-f2'], "src1/on-disk-f2\n")
        test.write(['src1', 'on-disk-e1'], "src1/on-disk-e1\n")
        test.write(['src1', 'on-disk-e2'], "src1/on-disk-e2\n")

        bld1 = self.fs.Dir('bld1')
        src1 = self.fs.Dir('src1')
        self.fs.VariantDir(bld1, src1, duplicate=1)

        derived_f = src1.File('derived-f')
        derived_f._func_is_derived = 2
        exists_f = src1.File('exists-f')
        exists_f._func_exists = 5

        derived_e = src1.Entry('derived-e')
        derived_e._func_is_derived = 2
        exists_e = src1.Entry('exists-e')
        exists_e._func_exists = 5

        # First check from the source directory.
        n = src1.srcdir_find_file('does_not_exist')
        assert n == (None, None), n

        n = src1.srcdir_find_file('derived-f')
        check(n, ['src1/derived-f', 'src1'])
        n = src1.srcdir_find_file('exists-f')
        check(n, ['src1/exists-f', 'src1'])
        n = src1.srcdir_find_file('on-disk-f1')
        check(n, ['src1/on-disk-f1', 'src1'])

        n = src1.srcdir_find_file('derived-e')
        check(n, ['src1/derived-e', 'src1'])
        n = src1.srcdir_find_file('exists-e')
        check(n, ['src1/exists-e', 'src1'])
        n = src1.srcdir_find_file('on-disk-e1')
        check(n, ['src1/on-disk-e1', 'src1'])

        # Now check from the variant directory.
        n = bld1.srcdir_find_file('does_not_exist')
        assert n == (None, None), n

        n = bld1.srcdir_find_file('derived-f')
        check(n, ['bld1/derived-f', 'src1'])
        n = bld1.srcdir_find_file('exists-f')
        check(n, ['bld1/exists-f', 'src1'])
        n = bld1.srcdir_find_file('on-disk-f2')
        check(n, ['bld1/on-disk-f2', 'bld1'])

        n = bld1.srcdir_find_file('derived-e')
        check(n, ['bld1/derived-e', 'src1'])
        n = bld1.srcdir_find_file('exists-e')
        check(n, ['bld1/exists-e', 'src1'])
        n = bld1.srcdir_find_file('on-disk-e2')
        check(n, ['bld1/on-disk-e2', 'bld1'])

    def test_dir_on_disk(self):
        """Test the Dir.dir_on_disk() method"""
        self.test.subdir('sub', ['sub', 'exists'])
        self.test.write(['sub', 'file'], "self/file\n")
        sub = self.fs.Dir('sub')

        r = sub.dir_on_disk('does_not_exist')
        assert not r, r

        r = sub.dir_on_disk('exists')
        assert r, r

        r = sub.dir_on_disk('file')
        assert not r, r

    def test_file_on_disk(self):
        """Test the Dir.file_on_disk() method"""
        self.test.subdir('sub', ['sub', 'dir'])
        self.test.write(['sub', 'exists'], "self/exists\n")
        sub = self.fs.Dir('sub')

        r = sub.file_on_disk('does_not_exist')
        assert not r, r

        r = sub.file_on_disk('exists')
        assert r, r

        r = sub.file_on_disk('dir')
        assert not r, r


class EntryTestCase(_tempdirTestCase):
    def test_runTest(self):
        """Test methods specific to the Entry sub-class.
        """
        test = TestCmd(workdir='')
        # FS doesn't like the cwd to be something other than its root.
        os.chdir(test.workpath(""))

        fs = SCons.Node.FS.FS()

        e1 = fs.Entry('e1')
        e1.rfile()
        assert e1.__class__ is SCons.Node.FS.File, e1.__class__

        test.subdir('e3d')
        test.write('e3f', "e3f\n")

        e3d = fs.Entry('e3d')
        e3d.get_contents()
        assert e3d.__class__ is SCons.Node.FS.Dir, e3d.__class__

        e3f = fs.Entry('e3f')
        e3f.get_contents()
        assert e3f.__class__ is SCons.Node.FS.File, e3f.__class__

        e3n = fs.Entry('e3n')
        e3n.get_contents()
        assert e3n.__class__ is SCons.Node.FS.Entry, e3n.__class__

        test.subdir('e4d')
        test.write('e4f', "e4f\n")

        e4d = fs.Entry('e4d')
        exists = e4d.exists()
        assert e4d.__class__ is SCons.Node.FS.Dir, e4d.__class__
        assert exists, "e4d does not exist?"

        e4f = fs.Entry('e4f')
        exists = e4f.exists()
        assert e4f.__class__ is SCons.Node.FS.File, e4f.__class__
        assert exists, "e4f does not exist?"

        e4n = fs.Entry('e4n')
        exists = e4n.exists()
        assert e4n.__class__ is SCons.Node.FS.File, e4n.__class__
        assert not exists, "e4n exists?"

        class MyCalc:
            def __init__(self, val):
                self.max_drift = 0

                class M:
                    def __init__(self, val):
                        self.val = val

                    def collect(self, args):
                        result = 0
                        for a in args:
                            result += a
                        return result

                    def signature(self, executor):
                        return self.val + 222

                self.module = M(val)

        test.subdir('e5d')
        test.write('e5f', "e5f\n")

    def test_Entry_Entry_lookup(self):
        """Test looking up an Entry within another Entry"""
        self.fs.Entry('#topdir')
        self.fs.Entry('#topdir/a/b/c')


class FileTestCase(_tempdirTestCase):

    def test_subclass(self):
        """Test looking up subclass of File nodes"""

        class FileSubclass(SCons.Node.FS.File):
            pass

        sd = self.fs._lookup('special_file', None, FileSubclass, create=1)
        sd.must_be_same(SCons.Node.FS.File)

    def test_Dirs(self):
        """Test the File.Dirs() method"""
        fff = self.fs.File('subdir/fff')
        # This simulates that the SConscript file that defined
        # fff is in subdir/.
        fff.cwd = self.fs.Dir('subdir')
        d1 = self.fs.Dir('subdir/d1')
        d2 = self.fs.Dir('subdir/d2')
        dirs = fff.Dirs(['d1', 'd2'])
        assert dirs == [d1, d2], list(map(str, dirs))

    def test_exists(self):
        """Test the File.exists() method"""
        fs = self.fs
        test = self.test

        src_f1 = fs.File('src/f1')
        assert not src_f1.exists(), "%s apparently exists?" % src_f1

        test.subdir('src')
        test.write(['src', 'f1'], "src/f1\n")

        assert not src_f1.exists(), "%s did not cache previous exists() value" % src_f1
        src_f1.clear()
        assert src_f1.exists(), "%s apparently does not exist?" % src_f1

        test.subdir('build')
        fs.VariantDir('build', 'src')
        build_f1 = fs.File('build/f1')

        assert build_f1.exists(), "%s did not realize that %s exists" % (build_f1, src_f1)
        assert os.path.exists(build_f1.get_abspath()), "%s did not get duplicated on disk" % build_f1.get_abspath()

        test.unlink(['src', 'f1'])
        src_f1.clear()  # so the next exists() call will look on disk again

        assert build_f1.exists(), "%s did not cache previous exists() value" % build_f1
        build_f1.clear()
        build_f1.linked = None
        assert not build_f1.exists(), "%s did not realize that %s disappeared" % (build_f1, src_f1)
        assert not os.path.exists(build_f1.get_abspath()), "%s did not get removed after %s was removed" % (
        build_f1, src_f1)

    def test_changed(self):
        """
        Verify that changes between BuildInfo's list of souces, depends, and implicit
        dependencies do not corrupt content signature values written to .SConsign
        when using CacheDir and Timestamp-MD5 decider.
        This is for issue #2980
        """

        # node should have
        # 1 source (for example main.cpp)
        # 0 depends
        # N implicits (for example ['alpha.h', 'beta.h', 'gamma.h', '/usr/bin/g++'])

        class ChangedNode(SCons.Node.FS.File):
            def __init__(self, name, directory=None, fs=None):
                super().__init__(name, directory, fs)
                self.name = name
                self.Tag('found_includes', [])
                self.stored_info = None
                self.build_env = None
                self.changed_since_last_build = 4
                self.timestamp = 1

            def get_stored_info(self):
                return self.stored_info

            def get_build_env(self):
                return self.build_env

            def get_timestamp(self):
                """ Fake timestamp so they always match"""
                return self.timestamp

            def get_contents(self):
                return self.name

            def get_ninfo(self):
                """ mocked to ensure csig will equal the filename"""
                if self.ninfo is not None:
                    return self.ninfo
                self.ninfo = FakeNodeInfo(self.name, self.timestamp)
                return self.ninfo

            def get_csig(self):
                ninfo = self.get_ninfo()
                try:
                    return ninfo.csig
                except AttributeError:
                    pass

                return "Should Never Happen"

        class ChangedEnvironment(SCons.Environment.Base):

            def __init__(self):
                super().__init__()
                self.decide_source = self._changed_timestamp_then_content

        class FakeNodeInfo:
            def __init__(self, csig, timestamp):
                self.csig = csig
                self.timestamp = timestamp

        # Create nodes
        fs = SCons.Node.FS.FS()
        d = self.fs.Dir('.')

        node = ChangedNode('main.o', d, fs)  # main.o
        s1 = ChangedNode('main.cpp', d, fs)  # main.cpp
        s1.timestamp = 2  # this changed
        i1 = ChangedNode('alpha.h', d, fs)  # alpha.h - The bug is caused because the second build adds this file
        i1.timestamp = 2  # This is the new file.
        i2 = ChangedNode('beta.h', d, fs)  # beta.h
        i3 = ChangedNode('gamma.h', d, fs)  # gamma.h - In the bug beta.h's csig from binfo overwrites this ones
        i4 = ChangedNode('/usr/bin/g++', d, fs)  # /usr/bin/g++

        node.add_source([s1])
        node.add_dependency([])
        node.implicit = [i1, i2, i3, i4]
        node.implicit_set = set()
        # node._add_child(node.implicit, node.implicit_set, [n7, n8, n9])
        # node._add_child(node.implicit, node.implicit_set, [n10, n11, n12])

        # Mock out node's scan method
        # node.scan = lambda *args: None

        # Mock out nodes' children() ?
        # Should return Node's.
        # All those nodes should have changed_since_last_build set to match Timestamp-MD5's
        # decider method...

        # Generate sconsign entry needed
        sconsign_entry = SCons.SConsign.SConsignEntry()
        sconsign_entry.binfo = node.new_binfo()
        sconsign_entry.ninfo = node.new_ninfo()

        # mock out loading info from sconsign
        # This will cause node.get_stored_info() to return our freshly created sconsign_entry
        node.stored_info = sconsign_entry

        # binfo = information from previous build (from sconsign)
        # We'll set the following attributes (which are lists): "bsources", "bsourcesigs",
        # "bdepends","bdependsigs", "bimplicit", "bimplicitsigs"
        bi = sconsign_entry.binfo
        bi.bsources = ['main.cpp']
        bi.bsourcesigs = [FakeNodeInfo('main.cpp', 1), ]

        bi.bdepends = []
        bi.bdependsigs = []

        bi.bimplicit = ['beta.h', 'gamma.h']
        bi.bimplicitsigs = [FakeNodeInfo('beta.h', 1), FakeNodeInfo('gamma.h', 1)]

        ni = sconsign_entry.ninfo
        # We'll set the following attributes (which are lists): sources, depends, implicit lists

        # Set timestamp-md5
        # Call changed
        # Check results
        node.build_env = ChangedEnvironment()

        changed = node.changed()

        # change to true to debug
        if False:
            print("Changed:%s" % changed)
            print("%15s -> csig:%s" % (s1.name, s1.ninfo.csig))
            print("%15s -> csig:%s" % (i1.name, i1.ninfo.csig))
            print("%15s -> csig:%s" % (i2.name, i2.ninfo.csig))
            print("%15s -> csig:%s" % (i3.name, i3.ninfo.csig))
            print("%15s -> csig:%s" % (i4.name, i4.ninfo.csig))

        self.assertEqual(
            i2.name,
            i2.ninfo.csig,
            "gamma.h's fake csig should equal gamma.h but equals:%s" % i2.ninfo.csig,
        )


class GlobTestCase(_tempdirTestCase):
    def setUp(self):
        _tempdirTestCase.setUp(self)

        fs = SCons.Node.FS.FS()
        self.fs = fs

        # Make entries on disk that will not have Nodes, so we can verify
        # the behavior of looking for things on disk.
        self.test.write('disk-bbb', "disk-bbb\n")
        self.test.write('disk-aaa', "disk-aaa\n")
        self.test.write('disk-ccc', "disk-ccc\n")
        self.test.write('#disk-hash', "#disk-hash\n")
        self.test.subdir('disk-sub')
        self.test.write(['disk-sub', 'disk-ddd'], "disk-sub/disk-ddd\n")
        self.test.write(['disk-sub', 'disk-eee'], "disk-sub/disk-eee\n")
        self.test.write(['disk-sub', 'disk-fff'], "disk-sub/disk-fff\n")

        # Make some entries that have both Nodes and on-disk entries,
        # so we can verify what we do with
        self.test.write('both-aaa', "both-aaa\n")
        self.test.write('both-bbb', "both-bbb\n")
        self.test.write('both-ccc', "both-ccc\n")
        self.test.write('#both-hash', "#both-hash\n")
        self.test.subdir('both-sub1')
        self.test.write(['both-sub1', 'both-ddd'], "both-sub1/both-ddd\n")
        self.test.write(['both-sub1', 'both-eee'], "both-sub1/both-eee\n")
        self.test.write(['both-sub1', 'both-fff'], "both-sub1/both-fff\n")
        self.test.subdir('both-sub2')
        self.test.write(['both-sub2', 'both-ddd'], "both-sub2/both-ddd\n")
        self.test.write(['both-sub2', 'both-eee'], "both-sub2/both-eee\n")
        self.test.write(['both-sub2', 'both-fff'], "both-sub2/both-fff\n")

        self.both_aaa = fs.File('both-aaa')
        self.both_bbb = fs.File('both-bbb')
        self.both_ccc = fs.File('both-ccc')
        self._both_hash = fs.File('./#both-hash')
        self.both_sub1 = fs.Dir('both-sub1')
        self.both_sub1_both_ddd = self.both_sub1.File('both-ddd')
        self.both_sub1_both_eee = self.both_sub1.File('both-eee')
        self.both_sub1_both_fff = self.both_sub1.File('both-fff')
        self.both_sub2 = fs.Dir('both-sub2')
        self.both_sub2_both_ddd = self.both_sub2.File('both-ddd')
        self.both_sub2_both_eee = self.both_sub2.File('both-eee')
        self.both_sub2_both_fff = self.both_sub2.File('both-fff')

        # Make various Nodes (that don't have on-disk entries) so we
        # can verify how we match them.
        self.ggg = fs.File('ggg')
        self.hhh = fs.File('hhh')
        self.iii = fs.File('iii')
        self._hash = fs.File('./#hash')
        self.subdir1 = fs.Dir('subdir1')
        self.subdir1_lll = self.subdir1.File('lll')
        self.subdir1_jjj = self.subdir1.File('jjj')
        self.subdir1_kkk = self.subdir1.File('kkk')
        self.subdir2 = fs.Dir('subdir2')
        self.subdir2_lll = self.subdir2.File('lll')
        self.subdir2_kkk = self.subdir2.File('kkk')
        self.subdir2_jjj = self.subdir2.File('jjj')
        self.sub = fs.Dir('sub')
        self.sub_dir3 = self.sub.Dir('dir3')
        self.sub_dir3_kkk = self.sub_dir3.File('kkk')
        self.sub_dir3_jjj = self.sub_dir3.File('jjj')
        self.sub_dir3_lll = self.sub_dir3.File('lll')

    def do_cases(self, cases, **kwargs):

        # First, execute all of the cases with string=True and verify
        # that we get the expected strings returned.  We do this first
        # so the Glob() calls don't add Nodes to the self.fs file system
        # hierarchy.

        import copy
        strings_kwargs = copy.copy(kwargs)
        strings_kwargs['strings'] = True
        for input, string_expect, node_expect in cases:
            r = sorted(self.fs.Glob(input, **strings_kwargs))
            assert r == string_expect, "Glob(%s, strings=True) expected %s, got %s" % (input, string_expect, r)

        # Now execute all of the cases without string=True and look for
        # the expected Nodes to be returned.  If we don't have a list of
        # actual expected Nodes, that means we're expecting a search for
        # on-disk-only files to have returned some newly-created nodes.
        # Verify those by running the list through str() before comparing
        # them with the expected list of strings.
        for input, string_expect, node_expect in cases:
            r = self.fs.Glob(input, **kwargs)
            if node_expect:
                r = sorted(r, key=lambda a: a.get_internal_path())
                result = []
                for n in node_expect:
                    if isinstance(n, str):
                        n = self.fs.Entry(n)
                    result.append(n)
                fmt = lambda n: "%s %s" % (repr(n), repr(str(n)))
            else:
                r = sorted(map(str, r))
                result = string_expect
                fmt = lambda n: n
            if r != result:
                import pprint
                print("Glob(%s) expected:" % repr(input))
                pprint.pprint(list(map(fmt, result)))
                print("Glob(%s) got:" % repr(input))
                pprint.pprint(list(map(fmt, r)))
                self.fail()

    def test_exact_match(self):
        """Test globbing for exact Node matches"""
        join = os.path.join

        cases = (
            ('ggg', ['ggg'], [self.ggg]),

            ('subdir1', ['subdir1'], [self.subdir1]),

            ('subdir1/jjj', [join('subdir1', 'jjj')], [self.subdir1_jjj]),

            ('disk-aaa', ['disk-aaa'], None),

            ('disk-sub', ['disk-sub'], None),

            ('both-aaa', ['both-aaa'], []),
        )

        self.do_cases(cases)

    def test_subdir_matches(self):
        """Test globbing for exact Node matches in subdirectories"""
        join = os.path.join

        cases = (
            ('*/jjj',
             [join('subdir1', 'jjj'), join('subdir2', 'jjj')],
             [self.subdir1_jjj, self.subdir2_jjj]),

            ('*/disk-ddd',
             [join('disk-sub', 'disk-ddd')],
             None),
        )

        self.do_cases(cases)

    def test_asterisk1(self):
        """Test globbing for simple asterisk Node matches (1)"""
        cases = (
            ('h*',
             ['hhh'],
             [self.hhh]),

            ('*',
             ['#both-hash', '#hash',
              'both-aaa', 'both-bbb', 'both-ccc',
              'both-sub1', 'both-sub2',
              'ggg', 'hhh', 'iii',
              'sub', 'subdir1', 'subdir2'],
             [self._both_hash, self._hash,
              self.both_aaa, self.both_bbb, self.both_ccc, 'both-hash',
              self.both_sub1, self.both_sub2,
              self.ggg, 'hash', self.hhh, self.iii,
              self.sub, self.subdir1, self.subdir2]),
        )

        self.do_cases(cases, ondisk=False)

    def test_asterisk2(self):
        """Test globbing for simple asterisk Node matches (2)"""
        cases = (
            ('disk-b*',
             ['disk-bbb'],
             None),

            ('*',
             ['#both-hash', '#disk-hash', '#hash',
              'both-aaa', 'both-bbb', 'both-ccc',
              'both-sub1', 'both-sub2',
              'disk-aaa', 'disk-bbb', 'disk-ccc', 'disk-sub',
              'ggg', 'hhh', 'iii',
              'sub', 'subdir1', 'subdir2'],
             ['./#both-hash', './#disk-hash', './#hash',
              'both-aaa', 'both-bbb', 'both-ccc', 'both-hash',
              'both-sub1', 'both-sub2',
              'disk-aaa', 'disk-bbb', 'disk-ccc', 'disk-sub',
              'ggg', 'hash', 'hhh', 'iii',
              'sub', 'subdir1', 'subdir2']),
        )

        self.do_cases(cases)

    def test_question_mark(self):
        """Test globbing for simple question-mark Node matches"""
        join = os.path.join

        cases = (
            ('ii?',
             ['iii'],
             [self.iii]),

            ('both-sub?/both-eee',
             [join('both-sub1', 'both-eee'), join('both-sub2', 'both-eee')],
             [self.both_sub1_both_eee, self.both_sub2_both_eee]),

            ('subdir?/jjj',
             [join('subdir1', 'jjj'), join('subdir2', 'jjj')],
             [self.subdir1_jjj, self.subdir2_jjj]),

            ('disk-cc?',
             ['disk-ccc'],
             None),
        )

        self.do_cases(cases)

    def test_does_not_exist(self):
        """Test globbing for things that don't exist"""

        cases = (
            ('does_not_exist', [], []),
            ('no_subdir/*', [], []),
            ('subdir?/no_file', [], []),
        )

        self.do_cases(cases)

    def test_subdir_asterisk(self):
        """Test globbing for asterisk Node matches in subdirectories"""
        join = os.path.join

        cases = (
            ('*/k*',
             [join('subdir1', 'kkk'), join('subdir2', 'kkk')],
             [self.subdir1_kkk, self.subdir2_kkk]),

            ('both-sub?/*',
             [join('both-sub1', 'both-ddd'),
              join('both-sub1', 'both-eee'),
              join('both-sub1', 'both-fff'),
              join('both-sub2', 'both-ddd'),
              join('both-sub2', 'both-eee'),
              join('both-sub2', 'both-fff')],
             [self.both_sub1_both_ddd,
              self.both_sub1_both_eee,
              self.both_sub1_both_fff,
              self.both_sub2_both_ddd,
              self.both_sub2_both_eee,
              self.both_sub2_both_fff],
             ),

            ('subdir?/*',
             [join('subdir1', 'jjj'),
              join('subdir1', 'kkk'),
              join('subdir1', 'lll'),
              join('subdir2', 'jjj'),
              join('subdir2', 'kkk'),
              join('subdir2', 'lll')],
             [self.subdir1_jjj, self.subdir1_kkk, self.subdir1_lll,
              self.subdir2_jjj, self.subdir2_kkk, self.subdir2_lll]),

            ('sub/*/*',
             [join('sub', 'dir3', 'jjj'),
              join('sub', 'dir3', 'kkk'),
              join('sub', 'dir3', 'lll')],
             [self.sub_dir3_jjj, self.sub_dir3_kkk, self.sub_dir3_lll]),

            ('*/k*',
             [join('subdir1', 'kkk'), join('subdir2', 'kkk')],
             None),

            ('subdir?/*',
             [join('subdir1', 'jjj'),
              join('subdir1', 'kkk'),
              join('subdir1', 'lll'),
              join('subdir2', 'jjj'),
              join('subdir2', 'kkk'),
              join('subdir2', 'lll')],
             None),

            ('sub/*/*',
             [join('sub', 'dir3', 'jjj'),
              join('sub', 'dir3', 'kkk'),
              join('sub', 'dir3', 'lll')],
             None),
        )

        self.do_cases(cases)

    def test_subdir_question(self):
        """Test globbing for question-mark Node matches in subdirectories"""
        join = os.path.join

        cases = (
            ('*/?kk',
             [join('subdir1', 'kkk'), join('subdir2', 'kkk')],
             [self.subdir1_kkk, self.subdir2_kkk]),

            ('subdir?/l?l',
             [join('subdir1', 'lll'), join('subdir2', 'lll')],
             [self.subdir1_lll, self.subdir2_lll]),

            ('*/disk-?ff',
             [join('disk-sub', 'disk-fff')],
             None),

            ('subdir?/l?l',
             [join('subdir1', 'lll'), join('subdir2', 'lll')],
             None),
        )

        self.do_cases(cases)

    def test_sort(self):
        """Test whether globbing sorts"""
        join = os.path.join
        # At least sometimes this should return out-of-order items
        # if Glob doesn't sort.
        # It's not a very good test though since it depends on the
        # order returned by glob, which might already be sorted.
        g = self.fs.Glob('disk-sub/*', strings=True)
        expect = [
            os.path.join('disk-sub', 'disk-ddd'),
            os.path.join('disk-sub', 'disk-eee'),
            os.path.join('disk-sub', 'disk-fff'),
        ]
        assert g == expect, str(g) + " is not sorted, but should be!"

        g = self.fs.Glob('disk-*', strings=True)
        expect = ['disk-aaa', 'disk-bbb', 'disk-ccc', 'disk-sub']
        assert g == expect, str(g) + " is not sorted, but should be!"


class RepositoryTestCase(_tempdirTestCase):

    def setUp(self):
        _tempdirTestCase.setUp(self)

        self.test.subdir('rep1', 'rep2', 'rep3', 'work')

        self.rep1 = self.test.workpath('rep1')
        self.rep2 = self.test.workpath('rep2')
        self.rep3 = self.test.workpath('rep3')

        os.chdir(self.test.workpath('work'))

        self.fs = SCons.Node.FS.FS()
        self.fs.Repository(self.rep1, self.rep2, self.rep3)

    def test_getRepositories(self):
        """Test the Dir.getRepositories() method"""
        self.fs.Repository('foo')
        self.fs.Repository(os.path.join('foo', 'bar'))
        self.fs.Repository('bar/foo')
        self.fs.Repository('bar')

        expect = [
            self.rep1,
            self.rep2,
            self.rep3,
            'foo',
            os.path.join('foo', 'bar'),
            os.path.join('bar', 'foo'),
            'bar'
        ]

        rep = self.fs.Dir('#').getRepositories()
        r = [os.path.normpath(str(x)) for x in rep]
        assert r == expect, r

    def test_get_all_rdirs(self):
        """Test the Dir.get_all_rdirs() method"""
        self.fs.Repository('foo')
        self.fs.Repository(os.path.join('foo', 'bar'))
        self.fs.Repository('bar/foo')
        self.fs.Repository('bar')

        expect = [
            '.',
            self.rep1,
            self.rep2,
            self.rep3,
            'foo',
            os.path.join('foo', 'bar'),
            os.path.join('bar', 'foo'),
            'bar'
        ]

        rep = self.fs.Dir('#').get_all_rdirs()
        r = [os.path.normpath(str(x)) for x in rep]
        assert r == expect, r

    def test_rentry(self):
        """Test the Base.entry() method"""
        return_true = lambda: 1
        return_false = lambda: 0

        d1 = self.fs.Dir('d1')
        d2 = self.fs.Dir('d2')
        d3 = self.fs.Dir('d3')

        e1 = self.fs.Entry('e1')
        e2 = self.fs.Entry('e2')
        e3 = self.fs.Entry('e3')

        f1 = self.fs.File('f1')
        f2 = self.fs.File('f2')
        f3 = self.fs.File('f3')

        self.test.write([self.rep1, 'd2'], "")
        self.test.subdir([self.rep2, 'd3'])
        self.test.write([self.rep3, 'd3'], "")

        self.test.write([self.rep1, 'e2'], "")
        self.test.subdir([self.rep2, 'e3'])
        self.test.write([self.rep3, 'e3'], "")

        self.test.write([self.rep1, 'f2'], "")
        self.test.subdir([self.rep2, 'f3'])
        self.test.write([self.rep3, 'f3'], "")

        r = d1.rentry()
        assert r is d1, r

        r = d2.rentry()
        assert r is not d2, r
        r = str(r)
        assert r == os.path.join(self.rep1, 'd2'), r

        r = d3.rentry()
        assert r is not d3, r
        r = str(r)
        assert r == os.path.join(self.rep2, 'd3'), r

        r = e1.rentry()
        assert r is e1, r

        r = e2.rentry()
        assert r is not e2, r
        r = str(r)
        assert r == os.path.join(self.rep1, 'e2'), r

        r = e3.rentry()
        assert r is not e3, r
        r = str(r)
        assert r == os.path.join(self.rep2, 'e3'), r

        r = f1.rentry()
        assert r is f1, r

        r = f2.rentry()
        assert r is not f2, r
        r = str(r)
        assert r == os.path.join(self.rep1, 'f2'), r

        r = f3.rentry()
        assert r is not f3, r
        r = str(r)
        assert r == os.path.join(self.rep2, 'f3'), r

    def test_rdir(self):
        """Test the Dir.rdir() method"""

        def return_true(obj):
            return 1

        def return_false(obj):
            return 0

        SCons.Node._exists_map[5] = return_true
        SCons.Node._exists_map[6] = return_false
        SCons.Node._is_derived_map[2] = return_true
        SCons.Node._is_derived_map[3] = return_false

        d1 = self.fs.Dir('d1')
        d2 = self.fs.Dir('d2')
        d3 = self.fs.Dir('d3')

        self.test.subdir([self.rep1, 'd2'])
        self.test.write([self.rep2, 'd3'], "")
        self.test.subdir([self.rep3, 'd3'])

        r = d1.rdir()
        assert r is d1, r

        r = d2.rdir()
        assert r is not d2, r
        r = str(r)
        assert r == os.path.join(self.rep1, 'd2'), r

        r = d3.rdir()
        assert r is not d3, r
        r = str(r)
        assert r == os.path.join(self.rep3, 'd3'), r

        e1 = self.fs.Dir('e1')
        e1._func_exists = 6
        e2 = self.fs.Dir('e2')
        e2._func_exists = 6

        # Make sure we match entries in repositories,
        # regardless of whether they're derived or not.

        re1 = self.fs.Entry(os.path.join(self.rep1, 'e1'))
        re1._func_exists = 5
        re1._func_is_derived = 2
        re2 = self.fs.Entry(os.path.join(self.rep2, 'e2'))
        re2._func_exists = 5
        re2._func_is_derived = 3

        r = e1.rdir()
        assert r is re1, r

        r = e2.rdir()
        assert r is re2, r

    def test_rfile(self):
        """Test the File.rfile() method"""

        def return_true(obj):
            return 1

        def return_false(obj):
            return 0

        SCons.Node._exists_map[5] = return_true
        SCons.Node._exists_map[6] = return_false
        SCons.Node._is_derived_map[2] = return_true
        SCons.Node._is_derived_map[3] = return_false

        f1 = self.fs.File('f1')
        f2 = self.fs.File('f2')
        f3 = self.fs.File('f3')

        self.test.write([self.rep1, 'f2'], "")
        self.test.subdir([self.rep2, 'f3'])
        self.test.write([self.rep3, 'f3'], "")

        r = f1.rfile()
        assert r is f1, r

        r = f2.rfile()
        assert r is not f2, r
        r = str(r)
        assert r == os.path.join(self.rep1, 'f2'), r

        r = f3.rfile()
        assert r is not f3, r
        r = f3.rstr()
        assert r == os.path.join(self.rep3, 'f3'), r

        e1 = self.fs.File('e1')
        e1._func_exists = 6
        e2 = self.fs.File('e2')
        e2._func_exists = 6

        # Make sure we match entries in repositories,
        # regardless of whether they're derived or not.

        re1 = self.fs.Entry(os.path.join(self.rep1, 'e1'))
        re1._func_exists = 5
        re1._func_is_derived = 2
        re2 = self.fs.Entry(os.path.join(self.rep2, 'e2'))
        re2._func_exists = 5
        re2._func_is_derived = 3

        r = e1.rfile()
        assert r is re1, r

        r = e2.rfile()
        assert r is re2, r

    def test_Rfindalldirs(self):
        """Test the Rfindalldirs() methods"""
        fs = self.fs
        test = self.test

        d1 = fs.Dir('d1')
        d2 = fs.Dir('d2')
        rep1_d1 = fs.Dir(test.workpath('rep1', 'd1'))
        rep2_d1 = fs.Dir(test.workpath('rep2', 'd1'))
        rep3_d1 = fs.Dir(test.workpath('rep3', 'd1'))
        sub = fs.Dir('sub')
        sub_d1 = sub.Dir('d1')
        rep1_sub_d1 = fs.Dir(test.workpath('rep1', 'sub', 'd1'))
        rep2_sub_d1 = fs.Dir(test.workpath('rep2', 'sub', 'd1'))
        rep3_sub_d1 = fs.Dir(test.workpath('rep3', 'sub', 'd1'))

        r = fs.Top.Rfindalldirs((d1,))
        assert r == [d1], list(map(str, r))

        r = fs.Top.Rfindalldirs((d1, d2))
        assert r == [d1, d2], list(map(str, r))

        r = fs.Top.Rfindalldirs(('d1',))
        assert r == [d1, rep1_d1, rep2_d1, rep3_d1], list(map(str, r))

        r = fs.Top.Rfindalldirs(('#d1',))
        assert r == [d1, rep1_d1, rep2_d1, rep3_d1], list(map(str, r))

        r = sub.Rfindalldirs(('d1',))
        assert r == [sub_d1, rep1_sub_d1, rep2_sub_d1, rep3_sub_d1], list(map(str, r))

        r = sub.Rfindalldirs(('#d1',))
        assert r == [d1, rep1_d1, rep2_d1, rep3_d1], list(map(str, r))

        r = fs.Top.Rfindalldirs(('d1', d2))
        assert r == [d1, rep1_d1, rep2_d1, rep3_d1, d2], list(map(str, r))

    def test_rexists(self):
        """Test the Entry.rexists() method"""
        fs = self.fs
        test = self.test

        test.write([self.rep1, 'f2'], "")
        test.write([self.rep2, "i_exist"], "\n")
        test.write(["work", "i_exist_too"], "\n")

        fs.VariantDir('build', '.')

        f = fs.File(test.workpath("work", "i_do_not_exist"))
        assert not f.rexists()

        f = fs.File(test.workpath("work", "i_exist"))
        assert f.rexists()

        f = fs.File(test.workpath("work", "i_exist_too"))
        assert f.rexists()

        f1 = fs.File(os.path.join('build', 'f1'))
        assert not f1.rexists()

        f2 = fs.File(os.path.join('build', 'f2'))
        assert f2.rexists()

    def test_FAT_timestamps(self):
        """Test repository timestamps on FAT file systems"""
        fs = self.fs
        test = self.test

        test.write(["rep2", "tstamp"], "tstamp\n")
        try:
            # Okay, *this* manipulation accomodates Windows FAT file systems
            # that only have two-second granularity on their timestamps.
            # We round down the current time to the nearest even integer
            # value, subtract two to make sure the timestamp is not "now,"
            # and then convert it back to a float.
            tstamp = float(int(time.time() // 2) * 2) - 2.0
            os.utime(test.workpath("rep2", "tstamp"), (tstamp - 2.0, tstamp))
            f = fs.File("tstamp")
            t = f.get_timestamp()
            assert t == tstamp, "expected %f, got %f" % (tstamp, t)
        finally:
            test.unlink(["rep2", "tstamp"])

    def test_get_contents(self):
        """Ensure get_contents() returns binary contents from Repositories"""
        fs = self.fs
        test = self.test

        test.write(["rep3", "contents"], "Con\x1aTents\n")
        try:
            c = fs.File("contents").get_contents()
            assert c == bytearray("Con\x1aTents\n", 'utf-8'), "got '%s'" % c
        finally:
            test.unlink(["rep3", "contents"])

    def test_get_text_contents(self):
        """Ensure get_text_contents() returns text contents from
        Repositories"""
        fs = self.fs
        test = self.test

        # Use a test string that has a file terminator in it to make
        # sure we read the entire file, regardless of its contents.
        test_string = "Con\x1aTents\n"

        # Test with ASCII.
        test.write(["rep3", "contents"], test_string.encode('ascii'))
        try:
            c = fs.File("contents").get_text_contents()
            assert test_string == c, "got %s" % repr(c)
        finally:
            test.unlink(["rep3", "contents"])

        # Test with utf-8
        test.write(["rep3", "contents"], test_string.encode('utf-8'))
        try:
            c = fs.File("contents").get_text_contents()
            assert test_string == c, "got %s" % repr(c)
        finally:
            test.unlink(["rep3", "contents"])

        # Test with utf-16
        test.write(["rep3", "contents"], test_string.encode('utf-16'))
        try:
            c = fs.File("contents").get_text_contents()
            assert test_string == c, "got %s" % repr(c)
        finally:
            test.unlink(["rep3", "contents"])

    # def test_is_up_to_date(self):


class find_fileTestCase(unittest.TestCase):
    def runTest(self):
        """Testing find_file function"""
        test = TestCmd(workdir='')
        test.write('./foo', 'Some file\n')
        test.write('./foo2', 'Another file\n')
        test.subdir('same')
        test.subdir('bar')
        test.write(['bar', 'on_disk'], 'Another file\n')
        test.write(['bar', 'same'], 'bar/same\n')

        fs = SCons.Node.FS.FS(test.workpath(""))
        # FS doesn't like the cwd to be something other than its root.
        os.chdir(test.workpath(""))

        node_derived = fs.File(test.workpath('bar/baz'))
        node_derived.builder_set(1)  # Any non-zero value.
        node_pseudo = fs.File(test.workpath('pseudo'))
        node_pseudo.set_src_builder(1)  # Any non-zero value.

        paths = tuple(map(fs.Dir, ['.', 'same', './bar']))
        nodes = [SCons.Node.FS.find_file('foo', paths),
                 SCons.Node.FS.find_file('baz', paths),
                 SCons.Node.FS.find_file('pseudo', paths),
                 SCons.Node.FS.find_file('same', paths)
        ]

        file_names = list(map(str, nodes))
        file_names = list(map(os.path.normpath, file_names))
        expect = ['./foo', './bar/baz', './pseudo', './bar/same']
        expect = list(map(os.path.normpath, expect))
        assert file_names == expect, file_names

        # Make sure we don't blow up if there's already a File in place
        # of a directory that we'd otherwise try to search.  If this
        # is broken, we'll see an exception like "Tried to lookup File
        # 'bar/baz' as a Dir.
        SCons.Node.FS.find_file('baz/no_file_here', paths)

        import io
        save_sys_stdout = sys.stdout

        try:
            sio = io.StringIO()
            sys.stdout = sio
            SCons.Node.FS.find_file('foo2', paths, verbose="xyz")
            expect = "  xyz: looking for 'foo2' in '.' ...\n" + \
                     "  xyz: ... FOUND 'foo2' in '.'\n"
            c = sio.getvalue()
            assert c == expect, c

            sio = io.StringIO()
            sys.stdout = sio
            SCons.Node.FS.find_file('baz2', paths, verbose=1)
            expect = "  find_file: looking for 'baz2' in '.' ...\n" + \
                     "  find_file: looking for 'baz2' in 'same' ...\n" + \
                     "  find_file: looking for 'baz2' in 'bar' ...\n"
            c = sio.getvalue()
            assert c == expect, c

            sio = io.StringIO()
            sys.stdout = sio
            SCons.Node.FS.find_file('on_disk', paths, verbose=1)
            expect = "  find_file: looking for 'on_disk' in '.' ...\n" + \
                     "  find_file: looking for 'on_disk' in 'same' ...\n" + \
                     "  find_file: looking for 'on_disk' in 'bar' ...\n" + \
                     "  find_file: ... FOUND 'on_disk' in 'bar'\n"
            c = sio.getvalue()
            assert c == expect, c
        finally:
            sys.stdout = save_sys_stdout


class StringDirTestCase(unittest.TestCase):
    def runTest(self):
        """Test using a string as the second argument of
        File() and Dir()"""

        test = TestCmd(workdir='')
        test.subdir('sub')
        fs = SCons.Node.FS.FS(test.workpath(''))

        d = fs.Dir('sub', '.')
        assert str(d) == 'sub', str(d)
        assert d.exists()
        f = fs.File('file', 'sub')
        assert str(f) == os.path.join('sub', 'file')
        assert not f.exists()


class stored_infoTestCase(unittest.TestCase):
    def runTest(self):
        """Test how we store build information"""
        test = TestCmd(workdir='')
        test.subdir('sub')
        fs = SCons.Node.FS.FS(test.workpath(''))

        d = fs.Dir('sub')
        f = fs.File('file1', d)
        bi = f.get_stored_info()
        assert hasattr(bi, 'ninfo')

        class MySConsign:
            class Null:
                def __init__(self):
                    self.xyzzy = 7

            def get_entry(self, name):
                return self.Null()

        def test_sconsign(node):
            return MySConsign()

        f = fs.File('file2', d)
        SCons.Node.FS._sconsign_map[2] = test_sconsign
        f.dir._func_sconsign = 2
        bi = f.get_stored_info()
        assert bi.xyzzy == 7, bi


class has_src_builderTestCase(unittest.TestCase):
    def runTest(self):
        """Test the has_src_builder() method"""
        test = TestCmd(workdir='')
        fs = SCons.Node.FS.FS(test.workpath(''))
        os.chdir(test.workpath(''))
        test.subdir('sub1')

        sub1 = fs.Dir('sub1', '.')
        f1 = fs.File('f1', sub1)
        f2 = fs.File('f2', sub1)
        f3 = fs.File('f3', sub1)

        h = f1.has_src_builder()
        assert not h, h
        h = f1.has_builder()
        assert not h, h

        b1 = Builder(fs.File)
        sub1.set_src_builder(b1)

        test.write(['sub1', 'f2'], "sub1/f2\n")
        h = f1.has_src_builder()  # cached from previous call
        assert not h, h
        h = f1.has_builder()  # cached from previous call
        assert not h, h
        h = f2.has_src_builder()
        assert not h, h
        h = f2.has_builder()
        assert not h, h
        h = f3.has_src_builder()
        assert h, h
        h = f3.has_builder()
        assert h, h
        assert f3.builder is b1, f3.builder


class prepareTestCase(unittest.TestCase):
    def runTest(self):
        """Test the prepare() method"""

        class MyFile(SCons.Node.FS.File):
            def _createDir(self, update=None):
                raise SCons.Errors.StopError

            def exists(self):
                return None

        fs = SCons.Node.FS.FS()
        file = MyFile('foo', fs.Dir('.'), fs)

        exc_caught = 0
        try:
            file.prepare()
        except SCons.Errors.StopError:
            exc_caught = 1
        assert exc_caught, "Should have caught a StopError."

        class MkdirAction(Action):
            def __init__(self, dir_made):
                self.dir_made = dir_made

            def __call__(self, target, source, env, executor=None):
                if executor:
                    target = executor.get_all_targets()
                    source = executor.get_all_sources()
                self.dir_made.extend(target)

        dir_made = []
        new_dir = fs.Dir("new_dir")
        new_dir.builder = Builder(fs.Dir, action=MkdirAction(dir_made))
        new_dir.reset_executor()
        xyz = fs.File(os.path.join("new_dir", "xyz"))

        xyz.set_state(SCons.Node.up_to_date)
        xyz.prepare()
        assert dir_made == [], dir_made

        xyz.set_state(0)
        xyz.prepare()
        assert dir_made[0].get_internal_path() == "new_dir", dir_made[0]

        dir = fs.Dir("dir")
        dir.prepare()

@unittest.skipIf(IS_WINDOWS, "No symlinks on windows")
@unittest.skipUnless(hasattr(os, 'symlink'), "Platform doesn't support symlink")
class CleanSymlinksTestCase(_tempdirTestCase):

    def test_cleans_symlinks(self):
        """Test the prepare() method will cleanup symlinks."""

        test = self.test

        with open(test.workpath("foo"), "w") as foo:
            foo.write("baz")

        os.symlink(test.workpath("foo"), test.workpath("bar"))
        bar = self.fs.File(test.workpath("bar"))
        bar.side_effect = True
        bar.set_state(0)
        assert bar.exists(), "Symlink %s should not exist after prepare"%str(bar)

        bar.prepare()
        try:
            os.lstat(test.workpath("bar"))
            assert False, "bar should not exist"
        except FileNotFoundError:
            pass

        try:
            os.stat(test.workpath("foo"))
        except FileNotFoundError:
            test.fail('Real file %s should not be removed'%test.workpath('foo'))

    def test_cleans_dangling_symlinks(self):
        """Test the prepare() method will cleanup dangling symlinks."""
        test = self.test

        with open(test.workpath("foo"), "w") as foo:
            foo.write("baz")

        os.symlink(test.workpath("foo"), test.workpath("bar"))
        os.remove(test.workpath("foo"))
        try:
            os.stat(test.workpath("foo"))
            assert False, "foo should not exist"
        except FileNotFoundError:
            pass

        bar = self.fs.File(test.workpath("bar"))
        bar.side_effect = True
        bar.set_state(0)

        # Dangling links should report not exists
        assert not bar.exists()

        bar.prepare()
        try:
            os.lstat(test.workpath("bar"))
            assert False, "bar [%s] should not exist"%test.workpath("bar")
        except FileNotFoundError:
            pass


class SConstruct_dirTestCase(unittest.TestCase):
    def runTest(self):
        """Test setting the SConstruct directory"""

        fs = SCons.Node.FS.FS()
        fs.set_SConstruct_dir(fs.Dir('xxx'))
        assert fs.SConstruct_dir.get_internal_path() == 'xxx'


class CacheDirTestCase(unittest.TestCase):

    def test_get_cachedir_csig(self):
        fs = SCons.Node.FS.FS()

        f9 = fs.File('f9')
        r = f9.get_cachedir_csig()
        exsig = SCons.Util.MD5signature(SCons.Util.NOFILE)
        assert r == exsig, r


class clearTestCase(unittest.TestCase):
    def runTest(self):
        """Test clearing FS nodes of cached data."""
        fs = SCons.Node.FS.FS()
        test = TestCmd(workdir='')

        e = fs.Entry('e')
        assert not e.exists()
        assert not e.rexists()
        assert str(e) == 'e', str(e)
        e.clear()
        assert not e.exists()
        assert not e.rexists()
        assert str(e) == 'e', str(e)

        d = fs.Dir(test.workpath('d'))
        test.subdir('d')
        assert d.exists()
        assert d.rexists()
        assert str(d) == test.workpath('d'), str(d)
        fs.rename(test.workpath('d'), test.workpath('gone'))
        # Verify caching is active
        assert d.exists(), 'caching not active'
        assert d.rexists()
        assert str(d) == test.workpath('d'), str(d)
        # Now verify clear() resets the cache
        d.clear()
        assert not d.exists()
        assert not d.rexists()
        assert str(d) == test.workpath('d'), str(d)

        f = fs.File(test.workpath('f'))
        test.write(test.workpath('f'), 'file f')
        assert f.exists()
        assert f.rexists()
        assert str(f) == test.workpath('f'), str(f)
        # Verify caching is active
        test.unlink(test.workpath('f'))
        assert f.exists()
        assert f.rexists()
        assert str(f) == test.workpath('f'), str(f)
        # Now verify clear() resets the cache
        f.clear()
        assert not f.exists()
        assert not f.rexists()
        assert str(f) == test.workpath('f'), str(f)
        # Now verify clear() resets optional File-specific attributes
        optional_attrs = ['cachedir_csig', 'cachesig', 'contentsig']
        for attr in optional_attrs:
            setattr(f, attr, 'xyz')
        f.clear()
        for attr in optional_attrs:
            assert not hasattr(f, attr), attr


class disambiguateTestCase(unittest.TestCase):
    def runTest(self):
        """Test calling the disambiguate() method."""
        test = TestCmd(workdir='')

        fs = SCons.Node.FS.FS()

        ddd = fs.Dir('ddd')
        d = ddd.disambiguate()
        assert d is ddd, d

        fff = fs.File('fff')
        f = fff.disambiguate()
        assert f is fff, f

        test.subdir('edir')
        test.write('efile', "efile\n")

        edir = fs.Entry(test.workpath('edir'))
        d = edir.disambiguate()
        assert d.__class__ is ddd.__class__, d.__class__

        efile = fs.Entry(test.workpath('efile'))
        f = efile.disambiguate()
        assert f.__class__ is fff.__class__, f.__class__

        test.subdir('build')
        test.subdir(['build', 'bdir'])
        test.write(['build', 'bfile'], "build/bfile\n")

        test.subdir('src')
        test.write(['src', 'bdir'], "src/bdir\n")
        test.subdir(['src', 'bfile'])

        test.subdir(['src', 'edir'])
        test.write(['src', 'efile'], "src/efile\n")

        fs.VariantDir(test.workpath('build'), test.workpath('src'))

        build_bdir = fs.Entry(test.workpath('build/bdir'))
        d = build_bdir.disambiguate()
        assert d is build_bdir, d
        assert d.__class__ is ddd.__class__, d.__class__

        build_bfile = fs.Entry(test.workpath('build/bfile'))
        f = build_bfile.disambiguate()
        assert f is build_bfile, f
        assert f.__class__ is fff.__class__, f.__class__

        build_edir = fs.Entry(test.workpath('build/edir'))
        d = build_edir.disambiguate()
        assert d.__class__ is ddd.__class__, d.__class__

        build_efile = fs.Entry(test.workpath('build/efile'))
        f = build_efile.disambiguate()
        assert f.__class__ is fff.__class__, f.__class__

        build_nonexistant = fs.Entry(test.workpath('build/nonexistant'))
        f = build_nonexistant.disambiguate()
        assert f.__class__ is fff.__class__, f.__class__


class postprocessTestCase(unittest.TestCase):
    def runTest(self):
        """Test calling the postprocess() method."""
        fs = SCons.Node.FS.FS()

        e = fs.Entry('e')
        e.postprocess()

        d = fs.Dir('d')
        d.postprocess()

        f = fs.File('f')
        f.postprocess()


class SpecialAttrTestCase(unittest.TestCase):
    def runTest(self):
        """Test special attributes of file nodes."""
        test = TestCmd(workdir='')
        fs = SCons.Node.FS.FS(test.workpath('work'))

        f = fs.Entry('foo/bar/baz.blat').get_subst_proxy()

        s = str(f.dir)
        assert s == os.path.normpath('foo/bar'), s
        assert f.dir.is_literal(), f.dir
        for_sig = f.dir.for_signature()
        assert for_sig == 'bar', for_sig

        s = str(f.file)
        assert s == 'baz.blat', s
        assert f.file.is_literal(), f.file
        for_sig = f.file.for_signature()
        assert for_sig == 'baz.blat_file', for_sig

        s = str(f.base)
        assert s == os.path.normpath('foo/bar/baz'), s
        assert f.base.is_literal(), f.base
        for_sig = f.base.for_signature()
        assert for_sig == 'baz.blat_base', for_sig

        s = str(f.filebase)
        assert s == 'baz', s
        assert f.filebase.is_literal(), f.filebase
        for_sig = f.filebase.for_signature()
        assert for_sig == 'baz.blat_filebase', for_sig

        s = str(f.suffix)
        assert s == '.blat', s
        assert f.suffix.is_literal(), f.suffix
        for_sig = f.suffix.for_signature()
        assert for_sig == 'baz.blat_suffix', for_sig

        s = str(f.get_abspath())
        assert s == test.workpath('work', 'foo', 'bar', 'baz.blat'), s
        assert f.abspath.is_literal(), f.abspath
        for_sig = f.abspath.for_signature()
        assert for_sig == 'baz.blat_abspath', for_sig

        s = str(f.posix)
        assert s == 'foo/bar/baz.blat', s
        assert f.posix.is_literal(), f.posix
        if f.posix != f:
            for_sig = f.posix.for_signature()
            assert for_sig == 'baz.blat_posix', for_sig

        s = str(f.windows)
        assert s == 'foo\\bar\\baz.blat', repr(s)
        assert f.windows.is_literal(), f.windows
        if f.windows != f:
            for_sig = f.windows.for_signature()
            assert for_sig == 'baz.blat_windows', for_sig

        # Deprecated synonym for the .windows suffix.
        s = str(f.win32)
        assert s == 'foo\\bar\\baz.blat', repr(s)
        assert f.win32.is_literal(), f.win32
        if f.win32 != f:
            for_sig = f.win32.for_signature()
            assert for_sig == 'baz.blat_windows', for_sig

        # And now, combinations!!!
        s = str(f.srcpath.base)
        assert s == os.path.normpath('foo/bar/baz'), s
        s = str(f.srcpath.dir)
        assert s == str(f.srcdir), s
        s = str(f.srcpath.posix)
        assert s == 'foo/bar/baz.blat', s
        s = str(f.srcpath.windows)
        assert s == 'foo\\bar\\baz.blat', s
        s = str(f.srcpath.win32)
        assert s == 'foo\\bar\\baz.blat', s

        # Test what happens with VariantDir()
        fs.VariantDir('foo', 'baz')

        s = str(f.srcpath)
        assert s == os.path.normpath('baz/bar/baz.blat'), s
        assert f.srcpath.is_literal(), f.srcpath
        g = f.srcpath.get()
        assert isinstance(g, SCons.Node.FS.File), g.__class__

        s = str(f.srcdir)
        assert s == os.path.normpath('baz/bar'), s
        assert f.srcdir.is_literal(), f.srcdir
        g = f.srcdir.get()
        assert isinstance(g, SCons.Node.FS.Dir), g.__class__

        # And now what happens with VariantDir() + Repository()
        fs.Repository(test.workpath('repository'))

        f = fs.Entry('foo/sub/file.suffix').get_subst_proxy()
        test.subdir('repository',
                    ['repository', 'baz'],
                    ['repository', 'baz', 'sub'])

        rd = test.workpath('repository', 'baz', 'sub')
        rf = test.workpath('repository', 'baz', 'sub', 'file.suffix')
        test.write(rf, "\n")

        s = str(f.srcpath)
        assert s == os.path.normpath('baz/sub/file.suffix'), s
        assert f.srcpath.is_literal(), f.srcpath
        g = f.srcpath.get()
        # Gets disambiguated to SCons.Node.FS.File by get_subst_proxy().
        assert isinstance(g, SCons.Node.FS.File), g.__class__

        s = str(f.srcdir)
        assert s == os.path.normpath('baz/sub'), s
        assert f.srcdir.is_literal(), f.srcdir
        g = f.srcdir.get()
        assert isinstance(g, SCons.Node.FS.Dir), g.__class__

        s = str(f.rsrcpath)
        assert s == rf, s
        assert f.rsrcpath.is_literal(), f.rsrcpath
        g = f.rsrcpath.get()
        assert isinstance(g, SCons.Node.FS.File), g.__class__

        s = str(f.rsrcdir)
        assert s == rd, s
        assert f.rsrcdir.is_literal(), f.rsrcdir
        g = f.rsrcdir.get()
        assert isinstance(g, SCons.Node.FS.Dir), g.__class__

        # Check that attempts to access non-existent attributes of the
        # subst proxy generate the right exceptions and messages.
        caught = None
        try:
            fs.Dir('ddd').get_subst_proxy().no_such_attr
        except AttributeError as e:
            assert str(e) == "Dir instance 'ddd' has no attribute 'no_such_attr'", e
            caught = 1
        assert caught, "did not catch expected AttributeError"

        caught = None
        try:
            fs.Entry('eee').get_subst_proxy().no_such_attr
        except AttributeError as e:
            # Gets disambiguated to File instance by get_subst_proxy().
            assert str(e) == "File instance 'eee' has no attribute 'no_such_attr'", e
            caught = 1
        assert caught, "did not catch expected AttributeError"

        caught = None
        try:
            fs.File('fff').get_subst_proxy().no_such_attr
        except AttributeError as e:
            assert str(e) == "File instance 'fff' has no attribute 'no_such_attr'", e
            caught = 1
        assert caught, "did not catch expected AttributeError"


class SaveStringsTestCase(unittest.TestCase):
    def runTest(self):
        """Test caching string values of nodes."""
        test = TestCmd(workdir='')

        def setup(fs):
            fs.Dir('src')
            fs.Dir('d0')
            fs.Dir('d1')

            d0_f = fs.File('d0/f')
            d1_f = fs.File('d1/f')
            d0_b = fs.File('d0/b')
            d1_b = fs.File('d1/b')
            d1_f.duplicate = 1
            d1_b.duplicate = 1
            d0_b.builder = 1
            d1_b.builder = 1

            return [d0_f, d1_f, d0_b, d1_b]

        def modify(nodes):
            d0_f, d1_f, d0_b, d1_b = nodes
            d1_f.duplicate = 0
            d1_b.duplicate = 0
            d0_b.builder = 0
            d1_b.builder = 0

        fs1 = SCons.Node.FS.FS(test.workpath('fs1'))
        nodes = setup(fs1)
        fs1.VariantDir('d0', 'src', duplicate=0)
        fs1.VariantDir('d1', 'src', duplicate=1)

        s = list(map(str, nodes))
        expect = list(map(os.path.normpath, ['src/f', 'd1/f', 'd0/b', 'd1/b']))
        assert s == expect, s

        modify(nodes)

        s = list(map(str, nodes))
        expect = list(map(os.path.normpath, ['src/f', 'src/f', 'd0/b', 'd1/b']))
        assert s == expect, s

        SCons.Node.FS.save_strings(1)
        fs2 = SCons.Node.FS.FS(test.workpath('fs2'))
        nodes = setup(fs2)
        fs2.VariantDir('d0', 'src', duplicate=0)
        fs2.VariantDir('d1', 'src', duplicate=1)

        s = list(map(str, nodes))
        expect = list(map(os.path.normpath, ['src/f', 'd1/f', 'd0/b', 'd1/b']))
        assert s == expect, s

        modify(nodes)

        s = list(map(str, nodes))
        expect = list(map(os.path.normpath, ['src/f', 'd1/f', 'd0/b', 'd1/b']))
        assert s == expect, 'node str() not cached: %s' % s


class AbsolutePathTestCase(unittest.TestCase):
    def test_root_lookup_equivalence(self):
        """Test looking up /fff vs. fff in the / directory"""
        test = TestCmd(workdir='')

        fs = SCons.Node.FS.FS('/')

        save_cwd = os.getcwd()
        try:
            os.chdir('/')
            fff1 = fs.File('fff')
            fff2 = fs.File('/fff')
            assert fff1 is fff2, "fff and /fff returned different Nodes!"
        finally:
            os.chdir(save_cwd)


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

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