3
\$\begingroup\$

Currently, I'm working on a legacy application written in .NET that does not contain any unit tests. This application, when executed produces a very large directory tree with roughly 20.000 files in total in different folders.

The application is currently being used heavily in production thus any mistakes during the refactoring cannot be afforded.

Therefore, I've decided to write a Directory Comparer tool which can compare 2 directories for equality. This can then be used in an integration test to see if the generated output after the refactoring matches a predefined snapshot.

Below is the code for it. Any feedback is highly appreciated.

The interfaces

public interface IDirectoryValidator
{
    bool Exists(string path);

    IEnumerable<string> GetSubDirectories(string path);
}

public interface IFileValidator
{
    IEnumerable<string> GetFiles(string path);

    IEnumerable<byte> Read(string path);
}

Interface Implementations

public sealed class DirectoryValidator : IDirectoryValidator
{
    public bool Exists(string path)
    {
        return Directory.Exists(path);
    }

    public IEnumerable<string> GetSubDirectories(string path)
    {
        return Directory.GetDirectories(path);
    }
}

public sealed class FileValidator : IFileValidator
{
    public bool Exists(string path)
    {
        return File.Exists(path);
    }

    public IEnumerable<string> GetFiles(string path)
    {
        return Directory.GetFiles(path);
    }

    public IEnumerable<byte> Read(string path)
    {
        return File.ReadAllBytes(path);
    }
}

Validator Entry Point

public sealed class FileSystemEqualityVerifier
{
    private readonly FileSystemValidator fileSystemValidator;

    public FileSystemEqualityVerifier(FileSystemValidator fileSystemValidator)
    {
        this.fileSystemValidator = fileSystemValidator;
    }

    public bool AreEqual(string referencePath, string actualPath)
    {
        return this.DirectoriesExists(referencePath, actualPath) &&
               this.DirectoryContentsEqual(referencePath, actualPath);
    }

    private static bool LastPathPartsEqual(string referencePath, string actualPath)
    {
        return Path.GetFileName(referencePath) == Path.GetFileName(actualPath);
    }

    private bool DirectoriesExists(params string[] paths)
    {
        return paths.All(path => this.fileSystemValidator.DirectoryExists(path));
    }

    private bool DirectoryContentsEqual(string referencePath, string actualPath)
    {
        return this.AmountOfEntriesInDirectoriesEqual(referencePath, actualPath) &&
               this.FilesInDirectoriesEqual(referencePath, actualPath) &&
               this.SubDirectoriesEqual(referencePath, actualPath);
    }

    private bool AmountOfEntriesInDirectoriesEqual(string referenceDirectory, string actualDirectory)
    {
        return this.DirectoriesContainsSameAmountOfDirectories(referenceDirectory, actualDirectory) &&
               this.DirectoriesContainsSameAmountOfFiles(referenceDirectory, actualDirectory);
    }

    private bool DirectoriesContainsSameAmountOfDirectories(string referenceDirectory, string actualDirectory)
    {
        var referenceSubDirectoriesCount = this.fileSystemValidator.GetDirectories(referenceDirectory).Count();
        var actualSubDirectoriesCount = this.fileSystemValidator.GetDirectories(actualDirectory).Count();

        return referenceSubDirectoriesCount.Equals(actualSubDirectoriesCount);
    }

    private bool DirectoriesContainsSameAmountOfFiles(string referenceDirectory, string actualDirectory)
    {
        var referenceFilesCount = this.fileSystemValidator.GetFiles(referenceDirectory).Count();
        var actualFilesCount = this.fileSystemValidator.GetFiles(actualDirectory).Count();

        return referenceFilesCount.Equals(actualFilesCount);
    }

    private bool FilesInDirectoriesEqual(string referencePath, string actualPath)
    {
        var referenceFilesAsArray = this.fileSystemValidator.GetFiles(referencePath).ToArray();
        var actualFilesAsArray = this.fileSystemValidator.GetFiles(actualPath).ToArray();

        return referenceFilesAsArray
            .Select((value, index) => new { Index = index, Value = value })
            .All(element => this.FileEquals(element.Value, actualFilesAsArray[element.Index]));
    }

    private bool FileEquals(string referencePath, string actualPath)
    {
        return LastPathPartsEqual(referencePath, actualPath) &&
               this.FileContentEquals(referencePath, actualPath);
    }

    private bool FileContentEquals(string referencePath, string actualPath)
    {
        var referenceFileContent = this.fileSystemValidator.ReadFile(referencePath);
        var actualFileContent = this.fileSystemValidator.ReadFile(actualPath);

        return referenceFileContent.SequenceEqual(actualFileContent);
    }

    private bool SubDirectoriesEqual(string referencePath, string actualPath)
    {
        var referenceDirectoriesAsArray = this.fileSystemValidator.GetDirectories(referencePath).ToArray();
        var actualDirectoriesAsArray = this.fileSystemValidator.GetDirectories(actualPath).ToArray();

        return referenceDirectoriesAsArray
            .Select((value, index) => new { Index = index, Value = value })
            .All(element => this.SubDirectoryContentsEqual(element.Value, actualDirectoriesAsArray[element.Index]));
    }

    private bool SubDirectoryContentsEqual(string referencePath, string actualPath)
    {
        var result = this.DirectoryContentsEqual(referencePath, actualPath) &&
               LastPathPartsEqual(referencePath, actualPath);

        return result;
    }
}

It's covered by unit tests with the code below.

The Models

internal enum EntryType
{
    Dir = 1,
    File = 2,
}

internal sealed class Entry
{
    private readonly string name;
    private readonly string? content;

    private Entry(string parentPath, string name)
    {
        this.ParentPath = parentPath;
        this.name = name;
        this.EntryType = EntryType.Dir;
    }

    private Entry(string parentPath, string name, string content)
    {
        this.ParentPath = parentPath;
        this.name = name;
        this.content = content;
        this.EntryType = EntryType.File;
    }

    internal string ParentPath { get; }

    internal string FullPath => this.BuildFullPath();

    internal IEnumerable<byte> ContentBytes => Encoding.ASCII.GetBytes(this.content ?? string.Empty);

    internal EntryType EntryType { get; }

    public static Entry Directory(string parentPath, string name)
    {
        return new Entry(parentPath, name);
    }

    public static Entry File(string parentPath, string name, string content)
    {
        return new Entry(parentPath, name, content);
    }

    public bool IsOfTypeWithFullPath(string directoryPath, EntryType entryType)
    {
        return (this.FullPath == directoryPath) && (this.EntryType == entryType);
    }

    private string BuildFullPath()
    {
        return string.IsNullOrEmpty(this.ParentPath) ? this.name : this.ParentPath + "/" + this.name;
    }
}

The FileSystem STUB

public sealed class FileSystemStub
{
    private readonly Mock<IDirectoryValidator> directoryValidatorMock;
    private readonly Mock<IFileValidator> fileValidatorMock;

    private readonly IList<Entry> entries;

    internal FileSystemStub()
    {
        this.directoryValidatorMock = new Mock<IDirectoryValidator>();
        this.fileValidatorMock = new Mock<IFileValidator>();

        this.entries = new List<Entry>();
    }

    internal IDirectoryValidator DirectoryValidator => this.directoryValidatorMock.Object;

    internal IFileValidator FileValidator => this.fileValidatorMock.Object;

    internal void AddDirectory(string name)
    {
        this.AddDirectory(string.Empty, name);
    }

    internal void AddDirectory(string parentPath, string name)
    {
        this.entries.Add(Entry.Directory(parentPath, name));

        this.ConfigureDirectoryValidatorMock();
    }

    internal void AddFile(string parentPath, string name, string contentHash)
    {
        this.entries.Add(Entry.File(parentPath, name, contentHash));

        this.ConfigureFileValidatorMock();
    }

    private void ConfigureDirectoryValidatorMock()
    {
        this.directoryValidatorMock
            .Setup(validator => validator.Exists(It.IsAny<string>()))
            .Returns(this.BuildExistsPredicate(EntryType.Dir));

        this.directoryValidatorMock
            .Setup(validator => validator.GetSubDirectories(It.IsAny<string>()))
            .Returns(this.BuildListDirectoryContentsPredicate(EntryType.Dir));
    }

    private void ConfigureFileValidatorMock()
    {
        this.fileValidatorMock
            .Setup(validator => validator.GetFiles(It.IsAny<string>()))
            .Returns(this.BuildListDirectoryContentsPredicate(EntryType.File));

        this.fileValidatorMock
            .Setup(validator => validator.Read(It.IsAny<string>()))
            .Returns(this.GetFileContentsPredicate());
    }

    private Func<string, bool> BuildExistsPredicate(EntryType entryType)
    {
        return element => this.entries.Any(entry => entry.IsOfTypeWithFullPath(element, entryType));
    }

    private Func<string, IEnumerable<string>> BuildListDirectoryContentsPredicate(EntryType entryType)
    {
        return element => this.entries
            .Where(entry => (entry.ParentPath == element) && (entry.EntryType == entryType))
            .Select(entry => entry.FullPath);
    }

    private Func<string, byte[]> GetFileContentsPredicate()
    {
        return element => this.entries
            .Where(entry => (entry.FullPath == element) && (entry.EntryType == EntryType.File))
            .SelectMany(entry => entry.ContentBytes)
            .ToArray();
    }
}

The actual UT's

public sealed class FileSystemEqualityVerifierUT
{
    private readonly FileSystemStub fileSystem;
    private readonly FileSystemEqualityVerifier fileSystemEqualityVerifier;

    public FileSystemEqualityVerifierUT()
    {
        this.fileSystem = new FileSystemStub();

        this.fileSystemEqualityVerifier = this.CreateFileSystemEqualityVerifier();
    }

    public static IEnumerable<object[]> UnEqualFileSystems =>
        new List<object[]>
        {
            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem => { }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem => { fileSystem.AddDirectory("/REF"); }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem => { fileSystem.AddDirectory("/ACTUAL"); }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddDirectory("/REF", "DIR 1");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddDirectory("/ACTUAL", "DIR 1");
                        fileSystem.AddDirectory("/ACTUAL", "DIR 2");
                    }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddFile("/REF", "FILE 1", "FILE 1 DATA");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddFile("/ACTUAL", "FILE 1", "FILE 1 DATA");
                        fileSystem.AddFile("/ACTUAL", "FILE 2", "FILE 2 DATA");
                    }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddFile("/REF", "FILE 1", "FILE 1 DATA");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddFile("/ACTUAL", "FILE 1 ALT.", "FILE 1 DATA");
                    }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddFile("/REF", "FILE 1", "FILE 1 DATA");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddFile("/ACTUAL", "FILE 1", "FILE 1 DATA ALT.");
                    }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddDirectory("/REF", "DIR 1");
                        fileSystem.AddDirectory("/REF/DIR 1", "DIR 1.1");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddDirectory("/ACTUAL", "DIR 1");
                        fileSystem.AddDirectory("/ACTUAL/DIR 1", "DIR 1.1");
                        fileSystem.AddDirectory("/ACTUAL/DIR 1", "DIR 1.2");
                    }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddDirectory("/REF", "DIR 1");
                        fileSystem.AddFile("/REF/DIR 1", "FILE 1.1", "FILE 1.1 DATA");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddDirectory("/ACTUAL", "DIR 1");
                        fileSystem.AddFile("/ACTUAL/DIR 1", "FILE 1.1", "FILE 1.1 DATA");
                        fileSystem.AddFile("/ACTUAL/DIR 1", "FILE 1.2", "FILE 1.2 DATA");
                    }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddDirectory("/REF", "DIR 1");
                        fileSystem.AddFile("/REF/DIR 1", "FILE 1.1", "FILE 1.1 DATA");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddDirectory("/ACTUAL", "DIR 1");
                        fileSystem.AddFile("/ACTUAL/DIR 1", "FILE 1.1 ALT.", "FILE 1.1 DATA");
                    }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddDirectory("/REF", "DIR 1");
                        fileSystem.AddFile("/REF/DIR 1", "FILE 1.1", "FILE 1.1 DATA");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddDirectory("/ACTUAL", "DIR 1");
                        fileSystem.AddFile("/ACTUAL/DIR 1", "FILE 1.1", "FILE 1.1 DATA ALT.");
                    }),
            },
        };

    public static IEnumerable<object[]> EqualFileSystems =>
        new List<object[]>
        {
            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddDirectory("/REF", "DIR 1");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddDirectory("/ACTUAL", "DIR 1");
                    }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddFile("/REF", "FILE 1", "FILE 1 DATA");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddFile("/ACTUAL", "FILE 1", "FILE 1 DATA");
                    }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddDirectory("/REF", "DIR 1");
                        fileSystem.AddDirectory("/REF/DIR 1", "DIR 1.1");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddDirectory("/ACTUAL", "DIR 1");
                        fileSystem.AddDirectory("/ACTUAL/DIR 1", "DIR 1.1");
                    }),
            },

            new object[]
            {
                new Action<FileSystemStub>(
                    fileSystem =>
                    {
                        fileSystem.AddDirectory("/REF");
                        fileSystem.AddDirectory("/REF", "DIR 1");
                        fileSystem.AddFile("/REF/DIR 1", "FILE 1.1", "FILE 1.1 DATA");

                        fileSystem.AddDirectory("/ACTUAL");
                        fileSystem.AddDirectory("/ACTUAL", "DIR 1");
                        fileSystem.AddFile("/ACTUAL/DIR 1", "FILE 1.1", "FILE 1.1 DATA");
                    }),
            },
        };

    private IDirectoryValidator DirectoryValidator => this.fileSystem.DirectoryValidator;

    private IFileValidator FileValidator => this.fileSystem.FileValidator;

    [Theory(DisplayName = "Returns 'FALSE' when the 'Reference' directory is NOT equal to the 'Actual' directory.")]
    [MemberData(nameof(UnEqualFileSystems))]
    public void GivenUnEqualFileSystemsReturnFalse(Action<FileSystemStub> configureFileSystem)
    {
        // ARRANGE.
        configureFileSystem(this.fileSystem);

        // ACT.
        var areFileSystemsEqual = this.fileSystemEqualityVerifier.AreEqual("/REF", "/ACTUAL");

        // ASSERT.
        Assert.False(areFileSystemsEqual);
    }

    [Theory(DisplayName = "Returns 'TRUE' when the 'Reference' directory is equal to the 'Actual' directory.")]
    [MemberData(nameof(EqualFileSystems))]
    public void GivenEqualFileSystemsReturnTrue(Action<FileSystemStub> configureFileSystem)
    {
        // ARRANGE.
        configureFileSystem(this.fileSystem);

        // ACT.
        var areFileSystemsEqual = this.fileSystemEqualityVerifier.AreEqual("/REF", "/ACTUAL");

        // ASSERT.
        Assert.True(areFileSystemsEqual);
    }

    private FileSystemEqualityVerifier CreateFileSystemEqualityVerifier()
    {
        var fileSystemValidator = new FileSystemValidator(this.DirectoryValidator, this.FileValidator);

        return new FileSystemEqualityVerifier(fileSystemValidator);
    }
}

Thanks for the feedback in advance.

\$\endgroup\$
2
\$\begingroup\$

My first feedback about IFileValidator interface would be to have an Exists() method, just like how you're having one in IDirectoryValidator interface. Plus the Validator interfaces are doing much more than validation I feel. For example, it is also trying to get a list of subdirectories and files. It is good if Validators only handle the job of validations and let the classes who implement them handle the other jobs. In fact, you can have one more interface called IValidator like

interface IValidator
{
    bool Validate();
}

interface IDirectoryValidator: IValidator
{

}

interface IFileValidator : IValidator
{

}

class DirectoryValidator : IDirectoryValidator
{
    private string mPath;

    public DirectoryValidator(string path)
    {
        mPath = path;
    }

    public bool Validate()
    {
        // You can have following validations implemented in this class
        // Check if the path is a valid string, if so
        // Check if path exists, if so 
        // Check if the directory is accessible and so on...

        return false;
    }
}

class FileValidator : IFileValidator 
{
    private string mPath;

    public FileValidator(string path)
    {
        mPath = path;
    }

    public bool Validate()
    {
        // You can have following validations implemented in this class
        // Check if the file path is valid string, if so
        // Check if path exists, if so 
        // Check if the file is accessible and so on...

        return false;
    }
}

Of course, this is just an idea, on how Validators can just have the validation logic.

Also, I see GetFiles() is being called multiple times like in FilesInDirectoriesEqual() and DirectoriesContainsSameAmountOfFiles() for the same path. It means you're trying to access your drives again and again. If your drives are huge then it might take time to access every folder and file. Hence, to improve it based on your needs you can make it a one-time operation.

Looking at the code I sense you're using just two paths to compare at a time. If it so, you can pass them inside the constructor of the class and run GetFiles() or GetDirectories() and store it in a property. It can significantly improve performance.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.