# 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.

from __future__ import annotations

import SCons.compat
import os
import os.path
import sys
import time
import unittest
import shutil
import stat
from typing import TYPE_CHECKING

from TestCmd import TestCmd, IS_WINDOWS, IS_ROOT

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

if TYPE_CHECKING:
    from SCons.Executor import Executor

built_it = None

scanner_count = 0

class Scanner:
    def __init__(self, node=None) -> 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) -> None:
        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) -> None:
        pass


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

    def show(self, string) -> None:
        pass

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

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

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

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


class Builder:
    def __init__(self, factory, action=Action()) -> None:
        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) -> None:
        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) -> None:
        os.chdir(self.save_cwd)


class VariantDirTestCase(unittest.TestCase):
    def runTest(self) -> None:
        """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) -> None:
                self.dir_made = dir_made

            def __call__(self, target, source, env, executor: Executor | None = None) -> 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) -> None:
            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) -> None:
                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) -> None:
                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 IS_WINDOWS:
                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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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.skipIf(IS_WINDOWS or not hasattr(os, 'symlink'),
                         "symlink is not used on Windows")
    def test_islink(self) -> None:
        """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) -> None:
        """Test DirNodeInfo initialization"""
        ddd = self.fs.Dir('ddd')
        ni = SCons.Node.FS.DirNodeInfo()


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


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

    def test_update(self) -> None:
        """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)

        # TODO: flip this to st.st_mtime when Node/FS.py does
        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')

        # TODO: flip this to st.st_mtime when Node/FS.py does
        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) -> None:
        """Test File.BuildInfo initialization"""
        fff = self.fs.File('fff')
        bi = SCons.Node.FS.FileBuildInfo()
        assert bi, bi

    def test_convert_to_sconsign(self) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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.
        This isn't ideal: normally you want to separate tests a bit
        more to make it easier to debug and not fail too fast.
        """
        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) -> None:
            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) -> None:
            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 not IS_WINDOWS 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 not IS_WINDOWS 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"

        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)


    @unittest.skipIf(IS_ROOT, "Skip file removal in protected dir if running as root.")
    def test_remove_fail(self) -> None:
        """Test failure when removing a file where permissions don't allow.

        Split from :math:`test_runTest` to be able to skip on root.
        We want to be able to skip only this one testcase and run the rest.
        """
        test = self.test
        fs = SCons.Node.FS.FS()
        test.write('can_not_remove', "can_not_remove\n")
        test.writable(test.workpath('.'), False)
        with open(test.workpath('can_not_remove')):
            f = fs.File('can_not_remove')
            with self.assertRaises(OSError, msg="Should have caught an OSError"):
                r = f.remove()


    def test_drive_letters(self) -> None:
        """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) -> None:
            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) -> None:
        """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) -> None:
            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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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(IS_WINDOWS, "requires Windows")
    def test_lookup_uncpath(self) -> None:
        """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(IS_WINDOWS, "requires Windows")
    def test_unc_drive_letter(self) -> None:
        """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(IS_WINDOWS, "requires Windows")
    def test_UNC_dirs_2689(self) -> None:
        """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) -> None:
        """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 IS_WINDOWS:
            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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
            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) -> None:
        """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) -> None:
        """Test the Dir.srcdir_find_file() method
        """
        test = self.test

        def return_true(node) -> int:
            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) -> None:
            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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
                self.max_drift = 0

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

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

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

                self.module = M(val)

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

    def test_Entry_Entry_lookup(self) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """
        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) -> 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) -> None:
                super().__init__()
                self.decide_source = self._changed_timestamp_then_content

        class FakeNodeInfo:
            def __init__(self, csig, timestamp) -> None:
                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) -> None:
        _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) -> None:

        # 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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        _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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """Test the Dir.rdir() method"""

        def return_true(obj) -> int:
            return 1

        def return_false(obj) -> int:
            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) -> None:
        """Test the File.rfile() method"""

        def return_true(obj) -> int:
            return 1

        def return_false(obj) -> int:
            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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
                    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) -> None:
        """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) -> None:
        """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) -> None:
                self.dir_made = dir_made

            def __call__(self, target, source, env, executor: Executor | None = None) -> 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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
        """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) -> None:
            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) -> None:
        """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)


class PyPackageDir(unittest.TestCase):
    def runTest(self) -> None:
        """Test calling the PyPackageDir() method.

        We don't want to mock the positive case here - there's
        testing for that in E2E test test/Dir/PyPackageDir.
        We're only making sure we don't die in the negative case
        (module not found) and instead return None.
        """
        fs = SCons.Node.FS.FS('/')
        try:
            pkdir = fs.PyPackageDir("garglemod")
        except AttributeError:
            self.fail("non-existent module raised AttributeError")
        self.assertIsNone(pkdir)


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

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