Skip to content

Instantly share code, notes, and snippets.

@AndyButland
Created February 27, 2018 21:11
Show Gist options
  • Save AndyButland/7c213cc0bd31116ffdb18d12566b9361 to your computer and use it in GitHub Desktop.
Save AndyButland/7c213cc0bd31116ffdb18d12566b9361 to your computer and use it in GitHub Desktop.
MSTest based Fitness Function for Verification of Sitecore Helix Architecture
namespace MySolution.FitnessFunctions
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
/// <summary>
/// Fitness functions for software architecture are defined as a way of confirming that a particular
/// characteristic of a solution's architecture that is considered important to maintain over time,
/// is in fact still the case.
/// Some of these can be confirmed via automated tests.
/// This class defines tests that ensure the project reference structure as defined by Sitecore
/// Helix principles are being maintained.
/// </summary>
/// <remarks>
/// References:
/// https://www.thoughtworks.com/insights/blog/microservices-evolutionary-architecture
/// http://shop.oreilly.com/product/0636920080237.do
/// http://helix.sitecore.net/introduction/index.html
/// </remarks>
[TestClass]
public class HelixFitnessFunctionTests
{
[TestClass]
public class TheSolutionShould : HelixFitnessFunctionTests
{
protected static readonly string SolutionName = "MySolution.sln";
protected static readonly string Foundation = "Foundation";
protected static readonly string Feature = "Feature";
protected static readonly string Project = "Project";
protected static readonly string TestProjectSuffix = ".Tests";
/// <summary>
/// Ensures that only appropriate inter-project references are in place within the solution.
/// Specifically:
/// - Foundation projects should only reference other Foundation projects
/// - Feature projects should only reference Foundation projects
/// - Project projects should only reference Foundation or Feature projects
/// </summary>
/// <remarks>
/// Hat-tip for locating projects and references in solution: https://stackoverflow.com/a/17571223/489433
/// </remarks>
[TestMethod]
public void ContainNoHelixInvalidProjectReferences()
{
// Arrange - get list of projects in the solution, along with a list of references for each project
Dictionary<string, IList<string>> projects = GetProjectsWithReferences();
// Act - review project list for invalid references
var invalidFoundationReferences = new List<string>();
var invalidFeatureReferences = new List<string>();
var invalidProjectReferences = new List<string>();
foreach (var project in projects)
{
if (project.Key.Contains(Foundation))
{
GetInvalidReferences(project, new[] { Feature, Project }, invalidFoundationReferences);
}
else if (project.Key.Contains(Feature))
{
GetInvalidReferences(project, new[] { Feature, Project }, invalidFeatureReferences);
}
else if (project.Key.Contains(Project))
{
GetInvalidReferences(project, new[] { Project }, invalidProjectReferences);
}
}
// Assert - check if we have any invalid refererences
AssertLayerReferences(invalidFoundationReferences, Foundation);
AssertLayerReferences(invalidFeatureReferences, Feature);
AssertLayerReferences(invalidProjectReferences, Project);
}
private static Dictionary<string, IList<string>> GetProjectsWithReferences()
{
var solutionFilePath = GetSolutionFilePath();
var solutionFileContents = File.ReadAllText(solutionFilePath);
var matches = GetProjectsFromSolutionFileContents(solutionFileContents);
return matches
.Select(x => x.Groups[2].Value)
.ToDictionary(GetProjectFileName, p => GetReferencedProjects(solutionFilePath, p));
}
private static string GetSolutionFilePath()
{
var pathParts = GetPathParts(Directory.GetCurrentDirectory());
var rootIndex = pathParts.IndexOf("src");
return Path.Combine(string.Join("\\", pathParts.Take(rootIndex)), SolutionName);
}
private static IList<string> GetPathParts(string directory)
{
return directory
.Split(new[] { "\\" }, StringSplitOptions.None)
.ToList();
}
private static IEnumerable<Match> GetProjectsFromSolutionFileContents(string solutionFileContents)
{
var regex = new Regex(
"Project\\(\"\\{[\\w-]*\\}\"\\) = \"([\\w _]*.*)\", \"(.*\\.(cs|vcx|vb)proj)\"",
RegexOptions.Compiled);
return regex.Matches(solutionFileContents).Cast<Match>();
}
private static string GetProjectFileName(string value)
{
return GetPathParts(value).Last().Replace(".csproj", string.Empty);
}
private static IList<string> GetReferencedProjects(string solutionFilePath, string projectFilePath)
{
var rootedProjectFilePath = Path.Combine(solutionFilePath.Replace(SolutionName, string.Empty), projectFilePath);
return GetReferencedProjects(rootedProjectFilePath);
}
private static List<string> GetReferencedProjects(string rootedProjectFilePath)
{
var result = new List<string>();
var xmlDoc = new XmlDocument();
xmlDoc.Load(rootedProjectFilePath);
var ns = new XmlNamespaceManager(xmlDoc.NameTable);
ns.AddNamespace("tu", "http://schemas.microsoft.com/developer/msbuild/2003");
var rootNode = xmlDoc.DocumentElement;
var referenceNodes = rootNode?.SelectNodes("//tu:ProjectReference", ns);
if (referenceNodes == null)
{
return result;
}
foreach (XmlNode node in referenceNodes)
{
result.Add(node.ChildNodes[1].InnerText);
}
return result;
}
private static void GetInvalidReferences(KeyValuePair<string, IList<string>> project, string[] invalidLayers, List<string> invalidReferences)
{
invalidReferences.AddRange(project.Value
.Where(x => ContainsInvalidReference(project.Key, x, invalidLayers))
.Select(x => $"{project.Key} references {x}"));
}
private static bool ContainsInvalidReference(string projectName, string referencedProjectName, IEnumerable<string> invalidLayers)
{
return invalidLayers
.Any(x => ProjectReferencesInvalidLayer(referencedProjectName, x) &&
!ReferencedProjectIsFromDedicatedTestProject(projectName, referencedProjectName) &&
!ReferencedProjectIsPartOfSubFeature(projectName, referencedProjectName));
}
private static bool ProjectReferencesInvalidLayer(string referencedProject, string invalidLayer)
{
return referencedProject.Contains(invalidLayer);
}
private static bool ReferencedProjectIsFromDedicatedTestProject(string projectName, string referencedProject)
{
return projectName == referencedProject + TestProjectSuffix;
}
private static bool ReferencedProjectIsPartOfSubFeature(string projectName, string referencedProject)
{
// We have some features that are broken into more than one project. Via naming convention
// we should allow these to be valid.
// E.g. MySolution.Feature.FeatureName.Api references MySolution.Feature.FeatureName.Model
if (projectName.Split('.').Length <= 3 || referencedProject.Split('.').Length <= 3)
{
// Not a sub-feature
return false;
}
return projectName.Split('.')[2] == referencedProject.Split('.')[2];
}
private static void AssertLayerReferences(IReadOnlyCollection<string> invalidReferences, string layer)
{
invalidReferences.Count.Should().Be(0, "there should be no Helix incompatible references in the {1} layer. {0} invalid reference{2} found: {3}. Expected 0",
invalidReferences.Count,
layer,
invalidReferences.Count == 1 ? " was" : "s were",
string.Join(", ", invalidReferences));
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment