Skip to content

Commit

Permalink
Convert MSTest projects
Browse files Browse the repository at this point in the history
  • Loading branch information
cartermp committed Oct 4, 2019
1 parent 01ba6df commit 067f31e
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 43 deletions.
36 changes: 36 additions & 0 deletions src/Facts/MSTestFacts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text;

namespace Facts
{
public static class MSTestFacts
{
public static ImmutableArray<string> MSTestProps => ImmutableArray.Create(
"MSTest.TestAdapter.props"
);

public static ImmutableArray<string> MSTestTargets => ImmutableArray.Create(
"Microsoft.TestTools.targets",
"MSTest.TestAdapter.targets"
);

public static ImmutableArray<Guid> KnownOldMSTestProjectTypeGuids => ImmutableArray.Create(
Guid.Parse("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"), // C#
Guid.Parse("{3AC096D0-A1C2-E12C-1390-A8335801FDAB}") // Test
);

public static ImmutableArray<string> MSTestReferences = ImmutableArray.Create(
"Microsoft.VisualStudio.TestPlatform.TestFramework",
"Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions"
);

public const string IsCodedUITestNodeName = "IsCodedUITest";
public const string TestProjectTypeNodeName = "TestProjectType";
public const string UnitTestTestProjectType = "UnitTest";
public const string UITestExtensionPackagesReferencePathFileName = "UITestExtensionPackages";
public const string MSTestSDKPackageName = "Microsoft.NET.Test.Sdk";
public const string MSTestSDKDev16FloatingVersion = "16.*"; // This is probably fine
}
}
17 changes: 14 additions & 3 deletions src/MSBuild.Abstractions/MSBuildHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public static IEnumerable<string> GetAllProjectTypeGuids(IProjectRootElement roo
/// </summary>
public static bool IsWPF(IProjectRootElement projectRoot)
{
var references = projectRoot.ItemGroups.SelectMany(GetReferences)?.Select(elem => elem.Include);
var references = projectRoot.ItemGroups.SelectMany(GetReferences)?.Select(elem => elem.Include.Split(',').First());
return DesktopFacts.KnownWPFReferences.Any(reference => references.Contains(reference, StringComparer.OrdinalIgnoreCase));
}

Expand All @@ -194,7 +194,7 @@ public static bool IsWPF(IProjectRootElement projectRoot)
/// </summary>
public static bool IsWinForms(IProjectRootElement projectRoot)
{
var references = projectRoot.ItemGroups.SelectMany(GetReferences)?.Select(elem => elem.Include);
var references = projectRoot.ItemGroups.SelectMany(GetReferences)?.Select(elem => elem.Include.Split(',').First());
return DesktopFacts.KnownWinFormsReferences.Any(reference => references.Contains(reference, StringComparer.OrdinalIgnoreCase));
}

Expand All @@ -203,10 +203,21 @@ public static bool IsWinForms(IProjectRootElement projectRoot)
/// </summary>
public static bool IsDesktop(IProjectRootElement projectRoot)
{
var references = projectRoot.ItemGroups.SelectMany(GetReferences)?.Select(elem => elem.Include);
var references = projectRoot.ItemGroups.SelectMany(GetReferences)?.Select(elem => elem.Include.Split(',').First());
return DesktopFacts.KnownDesktopReferences.Any(reference => references.Contains(reference, StringComparer.OrdinalIgnoreCase));
}

/// <summary>
/// Determines if a project is a .NET Framework MSTest project by looking at its references.
/// </summary>
/// <param name="root"></param>
/// <returns></returns>
public static bool IsNETFrameworkMSTestProject(IProjectRootElement projectRoot)
{
var references = projectRoot.ItemGroups.SelectMany(GetReferences)?.Select(elem => elem.Include.Split(',').First());
return MSTestFacts.MSTestReferences.All(reference => references.Contains(reference, StringComparer.OrdinalIgnoreCase));
}

public static bool HasWPFOrWinForms(ProjectPropertyGroupElement propGroup)
{
return (propGroup.Properties.Any(p => StringComparer.OrdinalIgnoreCase.Compare(p.Name, DesktopFacts.UseWPFPropertyName) == 0) ||
Expand Down
2 changes: 2 additions & 0 deletions src/MSBuild.Abstractions/MSBuildProjectRootElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public interface IProjectRootElement
ICollection<ProjectImportGroupElement> ImportGroups { get; }
ICollection<ProjectPropertyGroupElement> PropertyGroups { get; }
ICollection<ProjectItemGroupElement> ItemGroups { get; }
ICollection<ProjectTargetElement> Targets { get; }

ProjectPropertyElement CreatePropertyElement(string propertyName);
ProjectPropertyGroupElement AddPropertyGroup();
Expand Down Expand Up @@ -47,6 +48,7 @@ public MSBuildProjectRootElement(ProjectRootElement rootElement)
public ICollection<ProjectImportGroupElement> ImportGroups => _rootElement.ImportGroups;
public ICollection<ProjectPropertyGroupElement> PropertyGroups => _rootElement.PropertyGroups;
public ICollection<ProjectItemGroupElement> ItemGroups => _rootElement.ItemGroups;
public ICollection<ProjectTargetElement> Targets => _rootElement.Targets;

public ProjectItemGroupElement AddItemGroup() => _rootElement.AddItemGroup();

Expand Down
87 changes: 60 additions & 27 deletions src/MSBuild.Abstractions/MSBuildWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ public MSBuildWorkspace(ImmutableArray<string> paths, bool noBackup)
File.Copy(path, path + ".old");
}

// Let them know about System.Web
if (MSBuildHelpers.IsProjectReferencingSystemWeb(root))
{
Console.WriteLine($"'{root.FullPath}' references System.Web, which is unsupported on .NET Core. You may have significant work remaining after conversion.");
}

var configurations = DetermineConfigurations(root);

var unconfiguredProject = new UnconfiguredProject(configurations);
Expand Down Expand Up @@ -91,6 +85,7 @@ private BaselineProject CreateSdkBaselineProject(string projectFilePath, IProjec
{
case ProjectStyle.Default:
case ProjectStyle.DefaultSubset:
case ProjectStyle.MSTest:
rootElement.Sdk = MSBuildFacts.DefaultSDKAttribute;
break;
case ProjectStyle.WindowsDesktop:
Expand Down Expand Up @@ -126,7 +121,7 @@ void CopyImport(ProjectImportElement import)
{
MSBuildHelpers.AddUseWPF(propGroup);
}

// User is referencing WindowsBase only
if (MSBuildHelpers.IsDesktop(root) && !MSBuildHelpers.HasWPFOrWinForms(propGroup))
{
Expand Down Expand Up @@ -182,7 +177,6 @@ private ProjectStyle GetProjectStyle(IProjectRootElement project)
}
else
{

var lastImport = project.Imports.Last();
var lastImportFileName = Path.GetFileName(lastImport.Project);

Expand All @@ -203,7 +197,15 @@ private ProjectStyle GetProjectStyle(IProjectRootElement project)
{
return ProjectStyle.WindowsDesktop;
}
return ProjectStyle.Default;
else if (MSTestFacts.MSTestProps.Contains(firstImportFileName, StringComparer.OrdinalIgnoreCase) &&
MSTestFacts.MSTestTargets.Contains(lastImportFileName, StringComparer.OrdinalIgnoreCase))
{
return ProjectStyle.MSTest;
}
else
{
return ProjectStyle.Default;
}
}
}
}
Expand All @@ -229,32 +231,63 @@ private bool IsSupportedProjectType(MSBuildProjectRootElement root)
return false;
}

if (MSBuildHelpers.IsDesktop(root)
&& MSBuildHelpers.HasProjectTypeGuidsNode(root)
&& (!root.PropertyGroups.Any(pg => pg.Properties.Any(ProjectPropertyHelpers.AllProjectTypeGuidsAreDesktopProjectTypeGuids))))
// Lots of wild old project types have project type guids that the old project system uses to light things up!
if (MSBuildHelpers.HasProjectTypeGuidsNode(root))
{
var allSupportedProjectTypeGuids = DesktopFacts.KnownSupportedDesktopProjectTypeGuids.Select(ptg => ptg.ToString());
var allReadProjectTypeGuids = MSBuildHelpers.GetAllProjectTypeGuids(root);
// Check if it's an acceptable desktop type
if (MSBuildHelpers.IsDesktop(root))
{
if (!root.PropertyGroups.Any(pg => pg.Properties.Any(ProjectPropertyHelpers.AllProjectTypeGuidsAreDesktopProjectTypeGuids)))
{
var allSupportedProjectTypeGuids = DesktopFacts.KnownSupportedDesktopProjectTypeGuids.Select(ptg => ptg.ToString());
var allReadProjectTypeGuids = MSBuildHelpers.GetAllProjectTypeGuids(root);

Console.WriteLine($"{root.FullPath} is an unsupported project type. Not all project type guids are supported.");
Console.WriteLine($"{root.FullPath} is an unsupported project type. Not all project type guids are supported.");

PrintGuidMessage(allSupportedProjectTypeGuids, allReadProjectTypeGuids);
PrintGuidMessage(allSupportedProjectTypeGuids, allReadProjectTypeGuids);

return false;
}
return false;
}
}
// Reject if it's a web project
else if (root.PropertyGroups.Any(pg => pg.Properties.Any(ProjectPropertyHelpers.IsLegacyWebProjectTypeGuidsProperty)))
{
Console.WriteLine($"'{root.FullPath}' is a legacy web project, which is unsupported by this tool.");
return false;
}
// Check if it's a valid MSTest project type.
else if (MSBuildHelpers.IsNETFrameworkMSTestProject(root))
{
if (!root.PropertyGroups.Any(pg => pg.Properties.Any(ProjectPropertyHelpers.AllProjectTypeGuidsAreLegacyTestProjectTypeGuids)))
{
Console.WriteLine($"'{root.FullPath}' has invalid Project Type Guids for test projects and is not supported.");
return false;
}

if (root.PropertyGroups.Any(pg => pg.Properties.Any(ProjectPropertyHelpers.IsLegacyWebProjectTypeGuidsProperty)))
{
Console.WriteLine($"'{root.FullPath}' is a legacy web project, which is unsupported by this tool.");
return false;
if (root.PropertyGroups.Any(pg => pg.Properties.Any(ProjectPropertyHelpers.IsCodedUITest)))
{
Console.WriteLine($"'{root.FullPath}' is a coded UI test. Coded UI tests are deprecated and not convertable to .NET Core.");
return false;
}

if (!root.PropertyGroups.Any(pg => pg.Properties.Any(ProjectPropertyHelpers.IsUnitTestType)))
{
Console.WriteLine($"'{root.FullPath}' is an unsupported MSTest test type. Only Unit Tests are supported.");
return false;
}
}
// If it still has project type guids, bail if it's anything other than a language GUID because it probably wouldn't work on .NET Core
else if (!root.PropertyGroups.Any(pg => pg.Properties.Any(ProjectPropertyHelpers.AllProjectTypeGuidsAreLanguageProjectTypeGuids)))
{
Console.WriteLine($"{root.FullPath} is an unsupported project type.");
return false;
}
}

if (MSBuildHelpers.HasProjectTypeGuidsNode(root)
&& !MSBuildHelpers.IsDesktop(root)
&& (!root.PropertyGroups.Any(pg => pg.Properties.Any(ProjectPropertyHelpers.AllProjectTypeGuidsAreLanguageProjectTypeGuids))))
// Let them know about System.Web while we're here
if (MSBuildHelpers.IsProjectReferencingSystemWeb(root))
{
Console.WriteLine($"{root.FullPath} is an unsupported project type.");
return false;
Console.WriteLine($"'{root.FullPath}' references System.Web, which is unsupported on .NET Core. You may have significant work remaining after conversion.");
}

// It's supported
Expand Down
49 changes: 49 additions & 0 deletions src/MSBuild.Abstractions/ProjectPropertyHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,30 @@ public static bool IsDocumentationFileDefault(ProjectPropertyElement prop) =>
prop.ElementName.Equals(MSBuildFacts.DocumentationFileNodeName, StringComparison.OrdinalIgnoreCase)
&& prop.Value.Equals(MSBuildFacts.DefaultDocumentationFileLocation, StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Checks if a property is any of the unencessary test properties. This is only good to do <em>after</em> the loading phase where we discard inapplicable test projects.
/// </summary>
public static bool IsUnnecessaryTestProperty(ProjectPropertyElement prop) =>
IsUITestExtensionsPackagesReferencePath(prop) ||
prop.ElementName.Equals(MSTestFacts.IsCodedUITestNodeName, StringComparison.OrdinalIgnoreCase) ||
prop.ElementName.Equals(MSTestFacts.TestProjectTypeNodeName, StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Checks if a property is the old NuGetPackageImportStamp property that used to be required in VS 2013, is no longer required, but is still<em>stamped</em>(har har har...) into test project files (lol).
/// </summary>
public static bool IsEmptyNuGetPackageImportStamp(ProjectPropertyElement prop) =>
prop.ElementName.Equals(MSBuildFacts.NuGetPackageImportStampNodeName, StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Checks if a property is a default ReferencePath property that MSTest stamps into the project file.
/// </summary>
public static bool IsUITestExtensionsPackagesReferencePath(ProjectPropertyElement prop) =>
prop.ElementName.Equals(MSBuildFacts.ReferencePathNodeName, StringComparison.OrdinalIgnoreCase)
&& prop.Value.ContainsIgnoreCase(MSTestFacts.UITestExtensionPackagesReferencePathFileName);

public static bool IsOutputTypeNode(ProjectPropertyElement prop) =>
prop.ElementName.Equals(MSBuildFacts.OutputTypeNodeName, StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Determines if the name and value of two properties are identical.
/// </summary>
Expand Down Expand Up @@ -90,7 +114,32 @@ public static bool IsSupportedOutputType(ProjectPropertyElement prop) =>
public static bool AllProjectTypeGuidsAreLanguageProjectTypeGuids(ProjectPropertyElement prop) =>
IsProjectTypeGuidsNode(prop) && prop.Value.Split(';').All(guidString => MSBuildFacts.LanguageProjectTypeGuids.Contains(Guid.Parse(guidString)));

/// <summary>
/// Checks if all projecttypeguids specified are known desktop project type guids.
/// </summary>
/// <param name="prop"></param>
/// <returns></returns>
public static bool AllProjectTypeGuidsAreDesktopProjectTypeGuids(ProjectPropertyElement prop) =>
IsProjectTypeGuidsNode(prop) && prop.Value.Split(';').All(guidString => DesktopFacts.KnownSupportedDesktopProjectTypeGuids.Contains(Guid.Parse(guidString)));

/// <summary>
/// Checks if all projecttypeguids specified are known desktop project type guids.
/// </summary>
/// <param name="prop"></param>
/// <returns></returns>
public static bool AllProjectTypeGuidsAreLegacyTestProjectTypeGuids(ProjectPropertyElement prop) =>
IsProjectTypeGuidsNode(prop) && prop.Value.Split(';').All(guidString => MSTestFacts.KnownOldMSTestProjectTypeGuids.Contains(Guid.Parse(guidString)));

/// <summary>
/// Checks if a given property specifies IsCodedUITest=True, which is not only unsupported for .NET Core but is also deprecated after VS 2019.
/// </summary>
public static bool IsCodedUITest(ProjectPropertyElement prop) =>
prop.ElementName.Equals(MSTestFacts.IsCodedUITestNodeName, StringComparison.OrdinalIgnoreCase) && prop.Value.Equals("True", StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Checks if a test property is TestProjectType=UnitTest
/// </summary>
public static bool IsUnitTestType(ProjectPropertyElement prop) =>
prop.ElementName.Equals(MSTestFacts.TestProjectTypeNodeName, StringComparison.OrdinalIgnoreCase) && prop.Value.Equals(MSTestFacts.UnitTestTestProjectType, StringComparison.OrdinalIgnoreCase);
}
}
7 changes: 6 additions & 1 deletion src/MSBuild.Abstractions/ProjectStyle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public enum ProjectStyle
/// <summary>
/// The project is WPF or WinForms, and will use the WinDesktop framework reference
/// </summary>
WindowsDesktop
WindowsDesktop,

/// <summary>
/// The project is an MSTest project that pulls in a lot of unnecessary imports.
/// </summary>
MSTest
}
}
12 changes: 9 additions & 3 deletions src/MSBuild.Conversion.Facts/MSBuildFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ public static class MSBuildFacts
/// <summary>
/// Props files which are known to be imported in standard projects created from templates that can be converted to use the SDK
/// </summary>
public static ImmutableArray<string> PropsConvertibleToSDK => ImmutableArray.Create("Microsoft.Common.props");
public static ImmutableArray<string> PropsConvertibleToSDK => ImmutableArray.Create(
"Microsoft.Common.props",
"MSTest.TestAdapter.props"
);

/// <summary>
/// Targets files which are known to be imported in standard projects created from templates that can be converted to use the SDK.
Expand All @@ -22,7 +25,8 @@ public static class MSBuildFacts
"Microsoft.VisualBasic.targets",
"Microsoft.Portable.CSharp.targets",
"Microsoft.Portable.VisualBasic.targets",
"Microsoft.FSharp.Targets"
"Microsoft.FSharp.Targets",
"MSTest.TestAdapter.targets"
);

/// <summary>
Expand Down Expand Up @@ -187,7 +191,7 @@ public static class MSBuildFacts
public const string DefaultSDKAttribute = "Microsoft.NET.Sdk";
public const string LowestFrameworkVersionWithSystemValueTuple = "net47";
public const string SharedProjectsImportLabel = "Shared";
public const string NETCoreDesktopTFM = "netcoreapp3.0";
public const string NetCoreAppTFM = "netcoreapp3.0";
public const string SystemValueTupleName = "System.ValueTuple";
public const string DefineConstantsName = "DefineConstants";
public const string OutputPathName = "OutputPath";
Expand All @@ -214,5 +218,7 @@ public static class MSBuildFacts
public const string LibraryOutputType = "Library";
public const string ExeOutputType = "Exe";
public const string WinExeOutputType = "WinExe";
public const string NuGetPackageImportStampNodeName = "NuGetPackageImportStamp";
public const string ReferencePathNodeName = "ReferencePath";
}
}
1 change: 1 addition & 0 deletions src/MSBuild.Conversion.Facts/PackageFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public static class PackageFacts
public const string PackageReferenceIDName = "id";
public const string PackageReferenceVersionName = "version";
public const string PackageReferencePackagesNodeName = "packages";
public const string EnsureNuGetPackageBuildImportsName = "EnsureNuGetPackageBuildImports";
}
}
Loading

0 comments on commit 067f31e

Please sign in to comment.