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.