Testing Your Visual Studio Solutions for Files That Are Missing or Not Included

When several people collaborate on a project, it's very likely that they use different tools to complete the job. Backend developers, most likely, use Visual Studio and Resharper, while front-end developers use a combination of Visual Studio and their favorite code editor.

When it comes to choosing a Git client, there are many options to choose from, including the console/Git bash. Git clients will keep track of all file changes. In addition, it will notify you if there are any untracked files when you try to sync remote and local repositories. However, things don't always go according to a plan, and some files remain uncommitted (especially images, css and javascript files).

The same goes for the files inside Visual Studio. Files might exist in the Git repository, but they are not included in the project. Or, they simply don't have the correct build action - if they were generated by a 3rd party tool/add-on.

Therefore, when one of those two things happen, the build server may not be able to compile the project or transfer all the files to the test/stage/production environment.

One way to detect this, and stop bad deploys, is to set up unit tests. I'm sure there are other ways to solve this, but we always run all the unit tests locally before we perform "Git push", right? :)

Here's the code:

[TestFixture]
public class SolutionTests
{
    private readonly List<string> _ignoredFolders = new List<string>
    {
        @"App_Data\",
        @"bin\",
        @"obj\",
        @"Resources\node_modules\",
        @"Properties\PublishProfiles\"
        // other solution-specific files
    };

    private readonly List<string> _ignoredExtensions = new List<string>
    {
        ".user",
        ".csproj",
        "app.config",
        "packages.config",
        "web.debug.config",
        "license.config"
        // other solution-specific files
    };

    private readonly List<string> _ignoredProjects = new List<string>
    {
        // MyProject.csproj
    };

    [Test]
    public void Solution_Contains_No_Files_That_Are_Not_Included()
    {
        // find the full path of .sln file
        var solutionFileName = GetSolutionFileName();
        // find the full path of all .csproj files
        var projects = GetAllProjects(solutionFileName)
            .Where(x => !_ignoredProjects.Any(y => x.EndsWith(y, StringComparison.InvariantCultureIgnoreCase)))
            .ToList();

        var notIncludedFiles = new List<string>();
        foreach (var project in projects)
        {
            var fi = new FileInfo(project);
            var xdoc = XDocument.Load(project);

            var referencedFiles = xdoc.Descendants(_csprojNamespace + "ItemGroup")
                .Descendants()
                .Where(x => (x.Name == _csprojNamespace + "Content") | (x.Name == _csprojNamespace + "Compile"))
                .Select(element => element.Attribute("Include").Value);

            var di = new DirectoryInfo(fi.DirectoryName);
            var directoryPath = fi.DirectoryName + @"\";

            notIncludedFiles.AddRange(di.GetFiles("*.*", SearchOption.AllDirectories)
                .Select(filename => filename.FullName.Replace(directoryPath, ""))
                .Where(filename => !ShouldIgnoreFile(filename) &&
                                    !referencedFiles.Contains(filename))
                .Select(filename => $"Project: {fi.Name}, Filename: {filename}"));
        }

        if (notIncludedFiles.Count > 0)
        {
            var output = $"The following are not included:{Environment.NewLine}";
            output = notIncludedFiles.Aggregate(output, (current, file) => current + file + Environment.NewLine);

            Assert.Fail(output);
        }
    }

    [Test]
    public void Solution_Contains_No_Missing_Files()
    {
        // find the full path of .sln file
        var solutionFile = GetSolutionFileName();
        // find the full path of all .csproj files
        var projects = GetAllProjects(solutionFile);

        var missingFiles = new List<string>();
        foreach (var project in projects)
        {
            var fi = new FileInfo(project);
            var xdoc = XDocument.Load(project);

            missingFiles.AddRange(xdoc.Descendants(_csprojNamespace + "ItemGroup")
                .Descendants()
                .Where(x => (x.Name == _csprojNamespace + "Content") | (x.Name == _csprojNamespace + "Compile"))
                .Select(element => element.Attribute("Include").Value)
                .Select(filename => filename.Replace("%27", "'"))
                .Distinct()
                .Where(filename => !File.Exists(Path.Combine(fi.DirectoryName, filename)))
                .Select(filename => $"Project: {fi.Name}, Filename: {filename}"));
        }

        if (missingFiles.Count > 0)
        {
            var output = $"The following files are missing:{Environment.NewLine}";
            output = missingFiles.Aggregate(output, (current, file) => current + file + Environment.NewLine);

            Assert.Fail(output);
        }
    }

    #region Helpers

    private readonly XNamespace _csprojNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003");

    private string GetSolutionFileName()
    {
        var currentDirectory = new DirectoryInfo(Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath));

        while (true)
        {
            var solutionFile = currentDirectory.GetFiles("*.sln", SearchOption.TopDirectoryOnly).FirstOrDefault();
            if (solutionFile == null)
            {
                currentDirectory = currentDirectory.Parent;
            }
            else
            {
                return solutionFile.FullName;
            }
        }
    }

    private List<string> GetAllProjects(string solutionFile)
    {
        var slnContent = File.ReadAllText(solutionFile);
        var regex = new Regex("Project\\(\"\\{[\\w-]*\\}\"\\) = \"([\\w _]*.*)\", \"(.*\\.csproj)\"", RegexOptions.Compiled);
        var matches = regex.Matches(slnContent).Cast<Match>();
        var projects = matches.Select(x => x.Groups[2].Value).ToList();
        for (var i = 0; i < projects.Count; ++i)
        {
            if (!Path.IsPathRooted(projects[i]))
            {
                projects[i] = Path.Combine(Path.GetDirectoryName(solutionFile), projects[i]);
            }
            projects[i] = Path.GetFullPath(projects[i]);
        }
        return projects;
    }

    private bool ShouldIgnoreFile(string filename)
    {
        return _ignoredFolders.Any(item => filename.StartsWith(item, StringComparison.InvariantCultureIgnoreCase)) ||
                _ignoredExtensions.Any(item => filename.EndsWith(item, StringComparison.InvariantCultureIgnoreCase));
    }

    #endregion
}

Missing files

Not included files

comments powered by Disqus