File: ProjectConstruction\TestProject.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.TestFramework\Microsoft.NET.TestFramework.csproj (Microsoft.NET.TestFramework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Runtime.CompilerServices;
using NuGet.Frameworks;
 
namespace Microsoft.NET.TestFramework.ProjectConstruction
{
    public class TestProject
    {
        public TestProject([CallerMemberName] string? name = null)
        {
            if (name != null)
            {
                Name = name;
            }
        }
 
        /// <summary>
        /// A name for the test project that's used to isolate it from a test's root folder by appending it to the root test path.
        /// By default, it is the unhashed name of the function that instantiated the TestProject object.
        /// </summary>
        public string? Name { get; set; }
 
        public string TargetExtension { get; set; } = ".csproj";
 
        public bool IsSdkProject { get; set; } = true;
 
        public bool IsExe { get; set; }
 
        /// <summary>
        /// This value merely sets the OutputType and is not automatically tied here to whether the project is a WPF or Windows Form App Executable.
        /// </summary>
        public bool IsWinExe { get; set; }
 
        public string? ProjectSdk { get; set; }
 
        /// <summary>
        /// Applies to SDK-style projects. If the value has only one target framework (ie no semicolons), the value will be used
        /// for the MSBuild TargetFramework (singular) property.  Otherwise, the value will be used for the TargetFrameworks property.
        /// </summary>
        public string TargetFrameworks { get; set; } = ToolsetInfo.CurrentTargetFramework;
 
        public string? RuntimeFrameworkVersion { get; set; }
 
        public string? RuntimeIdentifier { get; set; }
 
        // Set to either true, false, or empty string "". The empty string does not undefine SelfContained, it just doesn't specify it.
        public string SelfContained { get; set; } = "";
 
        //  TargetFrameworkVersion applies to non-SDK projects
        public string? TargetFrameworkVersion { get; set; }
 
        public string? TargetFrameworkProfile { get; set; }
 
        public bool UseArtifactsOutput { get; set; }
 
        public List<TestProject> ReferencedProjects { get; } = [];
 
        public List<string> References { get; } = new List<string>();
 
        public List<string> FrameworkReferences { get; } = new List<string>();
 
        public List<TestPackageReference> PackageReferences { get; } = new List<TestPackageReference>();
 
        public List<TestPackageReference> DotNetCliToolReferences { get; } = new List<TestPackageReference>();
 
        public List<CopyFilesTarget> CopyFilesTargets { get; } = new List<CopyFilesTarget>();
 
        public Dictionary<string, string> SourceFiles { get; } = new Dictionary<string, string>();
 
        public Dictionary<string, string> EmbeddedResources { get; } = new Dictionary<string, string>();
 
        /// <summary>
        /// Use this dictionary to set a property (the key) to a value for the created project.
        /// </summary>
        public Dictionary<string, string> AdditionalProperties { get; } = new Dictionary<string, string>();
 
        public List<KeyValuePair<string, Dictionary<string, string>>> AdditionalItems { get; } = new();
 
        public List<Action<XDocument>> ProjectChanges { get; } = new List<Action<XDocument>>();
 
        /// <summary>
        /// A list of properties to record the values for when the project is built.
        /// Values can be retrieved with <see cref="GetPropertyValues"/>
        /// </summary>
        public List<string> PropertiesToRecord { get; } = new List<string>();
 
        public IEnumerable<string> TargetFrameworkIdentifiers
        {
            get
            {
                if (!IsSdkProject)
                {
                    //  Assume .NET Framework
                    yield return ".NETFramework";
                    yield break;
                }
 
                foreach (var target in TargetFrameworks.Split(';'))
                {
                    yield return NuGetFramework.Parse(target).Framework;
                }
            }
        }
 
        public bool BuildsOnNonWindows
        {
            get
            {
                if (!IsSdkProject)
                {
                    return false;
                }
 
                //  Currently can't build projects targeting .NET Framework on non-Windows: https://github.com/dotnet/sdk/issues/335
                foreach (var identifier in TargetFrameworkIdentifiers)
                {
                    if (identifier.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase))
                    {
                        return false;
                    }
                }
                return true;
            }
        }
 
        internal void Create(TestAsset targetTestAsset, string testProjectsSourceFolder)
        {
            string targetFolder = Path.Combine(targetTestAsset.Path, Name ?? string.Empty);
            Directory.CreateDirectory(targetFolder);
 
            string targetProjectPath = Path.Combine(targetFolder, Name + TargetExtension);
 
            string sourceProject;
            string sourceProjectBase = Path.Combine(testProjectsSourceFolder, "ProjectConstruction");
            if (IsSdkProject)
            {
                sourceProject = Path.Combine(sourceProjectBase, "SdkProject", "SdkProject.csproj");
            }
            else if (TargetExtension == ".vbproj")
            {
                sourceProject = Path.Combine(sourceProjectBase, "NetFrameworkProjectVB", "NetFrameworkProject.vbproj");
            }
            else
            {
                sourceProject = Path.Combine(sourceProjectBase, "NetFrameworkProject", "NetFrameworkProject.csproj");
            }
 
            //  Copy any additional files from template
            foreach (var file in Directory.GetFiles(Path.GetDirectoryName(sourceProject) ?? string.Empty))
            {
                if (file != sourceProject)
                {
                    File.Copy(file, Path.Combine(targetFolder, Path.GetFileName(file)));
                }
            }
 
            var projectXml = XDocument.Load(sourceProject);
 
            if (projectXml.Root is null)
            {
                throw new InvalidOperationException($"The project file '{sourceProject}' does not have a root element.");
            }
            var ns = projectXml.Root.Name.Namespace;
 
            var sdkAttribute = projectXml.Root.Attribute("Sdk");
            if (sdkAttribute is not null && ProjectSdk != null)
            {
                sdkAttribute.Value = ProjectSdk;
            }
 
            var propertyGroup = projectXml.Root?.Elements(ns + "PropertyGroup").First();
 
            var packageReferenceItemGroup = projectXml.Root?.Elements(ns + "ItemGroup")
                .FirstOrDefault(itemGroup => itemGroup.Elements(ns + "PackageReference").Count() > 0);
            if (packageReferenceItemGroup == null)
            {
                packageReferenceItemGroup = projectXml.Root?.Elements(ns + "ItemGroup")
                    .FirstOrDefault();
            }
            if (packageReferenceItemGroup == null)
            {
                packageReferenceItemGroup = new XElement(ns + "ItemGroup");
                projectXml.Root?.Add(packageReferenceItemGroup);
            }
 
            foreach (TestPackageReference packageReference in PackageReferences)
            {
                var includeOrUpdate = packageReference.UpdatePackageReference ? "Update" : "Include";
                var packageReferenceElement = new XElement(ns + "PackageReference",
                    new XAttribute(includeOrUpdate, packageReference.ID));
                if (packageReference.Version != null)
                {
                    packageReferenceElement.Add(new XAttribute("Version", packageReference.Version));
                }
                if (packageReference.PrivateAssets != null)
                {
                    packageReferenceElement.Add(new XAttribute("PrivateAssets", packageReference.PrivateAssets));
                }
                if (packageReference.Aliases != null)
                {
                    packageReferenceElement.Add(new XAttribute("Aliases", packageReference.Aliases));
                }
                if (packageReference.Publish != null)
                {
                    packageReferenceElement.Add(new XAttribute("Publish", packageReference.Publish));
                }
                packageReferenceItemGroup.Add(packageReferenceElement);
            }
 
            foreach (TestPackageReference dotnetCliToolReference in DotNetCliToolReferences)
            {
                packageReferenceItemGroup.Add(new XElement(ns + "DotNetCliToolReference",
                    new XAttribute("Include", $"{dotnetCliToolReference.ID}"),
                    new XAttribute("Version", $"{dotnetCliToolReference.Version}")));
            }
 
            var targetFrameworks = IsSdkProject ? TargetFrameworks.Split(';') : new[] { "net" };
 
            if (IsSdkProject)
            {
                if (TargetFrameworks.Contains(";"))
                {
                    propertyGroup?.Add(new XElement(ns + "TargetFrameworks", TargetFrameworks));
                }
                else
                {
                    propertyGroup?.Add(new XElement(ns + "TargetFramework", TargetFrameworks));
                }
 
                if (!string.IsNullOrEmpty(RuntimeFrameworkVersion))
                {
                    propertyGroup?.Add(new XElement(ns + "RuntimeFrameworkVersion", RuntimeFrameworkVersion));
                }
 
                if (!string.IsNullOrEmpty(RuntimeIdentifier))
                {
                    propertyGroup?.Add(new XElement(ns + "RuntimeIdentifier", RuntimeIdentifier));
                }
            }
            else
            {
                if (!string.IsNullOrEmpty(TargetFrameworkProfile))
                {
                    propertyGroup?.Add(new XElement(ns + "TargetFrameworkProfile", TargetFrameworkProfile));
 
                    //  To construct an accurate PCL project file, we must modify the import of the CSharp targets;
                    //    building/testing the SDK requires a VSDev command prompt which sets 'VSINSTALLDIR'
                    var importGroup = projectXml.Root?.Elements(ns + "Import").Last();
                    if(importGroup?.Attribute("Project") is not null)
                    {
                        importGroup.Attribute("Project")!.Value = "$(VSINSTALLDIR)\\MSBuild\\Microsoft\\Portable\\$(TargetFrameworkVersion)\\Microsoft.Portable.CSharp.targets";
                    }  
                }
 
                if(TargetFrameworkVersion is not null)
                {
                    propertyGroup?.Element(ns + "TargetFrameworkVersion")?.SetValue(TargetFrameworkVersion);
                }
            }
 
            foreach (var additionalProperty in AdditionalProperties)
            {
                propertyGroup?.Add(new XElement(ns + additionalProperty.Key, additionalProperty.Value));
            }
 
            if (AdditionalItems.Any())
            {
                foreach (var additionalItem in AdditionalItems)
                {
                    var additionalItemGroup = projectXml.Root?.Elements(ns + "ItemGroup").FirstOrDefault();
                    if (additionalItemGroup == null)
                    {
                        additionalItemGroup = new XElement(ns + "ItemGroup");
                        projectXml.Root?.Add(packageReferenceItemGroup);
                    }
                    var item = new XElement(ns + additionalItem.Key);
                    foreach (var attribute in additionalItem.Value)
                        item.Add(new XAttribute(attribute.Key, attribute.Value));
                    additionalItemGroup.Add(item);
                }
            }
 
            if (IsExe && !IsWinExe)
            {
                propertyGroup?.Element(ns + "OutputType")?.SetValue("Exe");
            }
            else if (IsWinExe)
            {
                propertyGroup?.Element(ns + "OutputType")?.SetValue("WinExe");
            }
 
            if (SelfContained != "")
            {
                propertyGroup?.Add(new XElement(ns + "SelfContained", string.Equals(SelfContained, "true", StringComparison.OrdinalIgnoreCase) ? "true" : "false"));
            }
 
            if (ReferencedProjects.Any())
            {
                var projectReferenceItemGroup = projectXml.Root?.Elements(ns + "ItemGroup")
                    .FirstOrDefault(itemGroup => itemGroup.Elements(ns + "ProjectReference").Count() > 0);
                if (projectReferenceItemGroup == null)
                {
                    projectReferenceItemGroup = new XElement(ns + "ItemGroup");
                    packageReferenceItemGroup.AddBeforeSelf(projectReferenceItemGroup);
                }
                foreach (var referencedProject in ReferencedProjects)
                {
                    projectReferenceItemGroup.Add(new XElement(ns + "ProjectReference",
                    new XAttribute("Include", $"../{referencedProject.Name}/{referencedProject.Name}{referencedProject.TargetExtension}")));
                }
            }
 
            if (References.Any())
            {
                var referenceItemGroup = projectXml.Root?.Elements(ns + "ItemGroup")
                    .FirstOrDefault(itemGroup => itemGroup.Elements(ns + "Reference").Count() > 0);
                if (referenceItemGroup == null)
                {
                    referenceItemGroup = new XElement(ns + "ItemGroup");
                    packageReferenceItemGroup.AddBeforeSelf(referenceItemGroup);
                }
 
                foreach (var reference in References)
                {
                    referenceItemGroup.Add(new XElement(ns + "Reference",
                        new XAttribute("Include", reference)));
                }
            }
 
            if (FrameworkReferences.Any())
            {
                var frameworkReferenceItemGroup = new XElement(ns + "ItemGroup");
                projectXml.Root?.Add(frameworkReferenceItemGroup);
                foreach (var frameworkReference in FrameworkReferences)
                {
                    frameworkReferenceItemGroup.Add(new XElement(ns + "FrameworkReference",
                        new XAttribute("Include", frameworkReference)));
                }
            }
 
            if (CopyFilesTargets.Any())
            {
                foreach (var copyFilesTarget in CopyFilesTargets)
                {
                    var target = new XElement(ns + "Target",
                        new XAttribute("Name", copyFilesTarget.TargetName),
                        new XAttribute("AfterTargets", copyFilesTarget.TargetToRunAfter));
 
                    var copyElement = new XElement(ns + "Copy",
                        new XAttribute("SourceFiles", copyFilesTarget.SourceFiles),
                        new XAttribute("DestinationFolder", copyFilesTarget.Destination));
 
                    if (!string.IsNullOrEmpty(copyFilesTarget.Condition))
                    {
                        copyElement.SetAttributeValue("Condition", copyFilesTarget.Condition);
                    }
 
                    target.Add(copyElement);
                    projectXml.Root?.Add(target);
                }
            }
 
            foreach (var projectChange in ProjectChanges)
            {
                projectChange(projectXml);
            }
 
            if (PropertiesToRecord.Any())
            {
                var customAfterDirectoryBuildTargetsPath = new FileInfo(Path.Combine(targetFolder, "obj", "Custom.After.Directory.Build.targets"));
                customAfterDirectoryBuildTargetsPath.Directory?.Create();
 
                propertyGroup?.Add(new XElement(ns + "CustomAfterDirectoryBuildTargets", $"$(CustomAfterDirectoryBuildTargets);{customAfterDirectoryBuildTargetsPath.FullName}"));
                propertyGroup?.Add(new XElement(ns + "CustomAfterMicrosoftCommonCrossTargetingTargets", $"$(CustomAfterMicrosoftCommonCrossTargetingTargets);{customAfterDirectoryBuildTargetsPath.FullName}"));
                
                var customAfterDirectoryBuildTargets = new XDocument(new XElement(ns + "Project"));
 
                var target = new XElement(ns + "Target",
                    new XAttribute("Name", "WritePropertyValues"),
                    new XAttribute("BeforeTargets", "AfterBuild"));
 
                customAfterDirectoryBuildTargets.Root?.Add(target);
 
                var itemGroup = new XElement(ns + "ItemGroup");
                target.Add(itemGroup);
 
                foreach (var propertyName in PropertiesToRecord)
                {
                    itemGroup.Add(
                        new XElement(ns + "LinesToWrite",
                            new XAttribute("Include", $"{propertyName}: $({propertyName})")));
                }
 
                target.Add(
                    new XElement(ns + "WriteLinesToFile",
                        new XAttribute("File", $@"$(BaseIntermediateOutputPath)\$(Configuration)\$(TargetFramework)\PropertyValues.txt"),
                        new XAttribute("Lines", "@(LinesToWrite)"),
                        new XAttribute("Overwrite", bool.TrueString),
                        new XAttribute("Encoding", "Unicode")));
 
                customAfterDirectoryBuildTargets.Save(customAfterDirectoryBuildTargetsPath.FullName);
            }
 
            using (var file = File.CreateText(targetProjectPath))
            {
                projectXml.Save(file);
            }
 
            if (SourceFiles.Count == 0)
            {
                if (IsExe || IsWinExe)
                {
                    string source =
    @"using System;
 
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(""Hello World!"");
";
 
                    foreach (var dependency in ReferencedProjects)
                    {
                        string? safeDependencyName = dependency.Name?.Replace('.', '_');
 
                        source += $"        Console.WriteLine({safeDependencyName}.{safeDependencyName}Class.Name);" + Environment.NewLine;
                        source += $"        Console.WriteLine({safeDependencyName}.{safeDependencyName}Class.List);" + Environment.NewLine;
                    }
 
                    source +=
    @"    }
}";
                    string sourcePath = Path.Combine(targetFolder, Name + "Program.cs");
 
                    File.WriteAllText(sourcePath, source);
                }
 
                {
                    string? safeThisName = Name?.Replace('.', '_');
                    string source =
    $@"using System;
using System.Collections.Generic;
 
namespace {safeThisName}
{{
    public class {safeThisName}Class
    {{
        public static string Name {{ get {{ return ""{Name}""; }} }}
        public static List<string> List {{ get {{ return null; }} }}
";
                    foreach (var dependency in ReferencedProjects)
                    {
                        string? safeDependencyName = dependency.Name?.Replace('.', '_');
 
                        source += $"        public string {safeDependencyName}Name {{ get {{ return {safeDependencyName}.{safeDependencyName}Class.Name; }} }}" + Environment.NewLine;
                        source += $"        public List<string> {safeDependencyName}List {{ get {{ return {safeDependencyName}.{safeDependencyName}Class.List; }} }}" + Environment.NewLine;
                    }
 
                    source +=
    @"    }
}";
                    string sourcePath = Path.Combine(targetFolder, Name + ".cs");
                    File.WriteAllText(sourcePath, source);
                }
            }
 
            foreach (var kvp in SourceFiles.Concat(EmbeddedResources))
            {
                var targetPath = Path.Combine(targetFolder, kvp.Key);
                Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
                File.WriteAllText(targetPath, kvp.Value);
            }
        }
 
        public void AddItem(string itemName, string attributeName, string attributeValue)
        {
            AddItem(itemName, new Dictionary<string, string>() { { attributeName, attributeValue } });
        }
 
        public void AddItem(string itemName, Dictionary<string, string> attributes)
        {
            AdditionalItems.Add(new(itemName, attributes));
        }
 
        public void RecordProperties(params string[] propertyNames)
        {
            PropertiesToRecord.AddRange(propertyNames);
        }
 
        /// <returns>
        /// A dictionary of property keys to property value strings, case sensitive.
        /// Only properties added to the <see cref="PropertiesToRecord"/> member will be observed.
        /// </returns>
        public Dictionary<string, string> GetPropertyValues(string testRoot, string? targetFramework = null, string configuration = "Debug")
        {
            var propertyValues = new Dictionary<string, string>();
 
            string intermediateOutputPath = Path.Combine(testRoot, Name ?? string.Empty, "obj", configuration, targetFramework ?? TargetFrameworks);
 
            foreach (var line in File.ReadAllLines(Path.Combine(intermediateOutputPath, "PropertyValues.txt")))
            {
                int colonIndex = line.IndexOf(':');
                if (colonIndex > 0)
                {
                    string propertyName = line.Substring(0, colonIndex);
                    string propertyValue = line.Length == colonIndex + 1 ? string.Empty : line.Substring(colonIndex + 2);
                    propertyValues[propertyName] = propertyValue;
                }
            }
 
            return propertyValues;
        }
 
        private OutputPathCalculator GetOutputPathCalculator(string testRoot)
        {
            return OutputPathCalculator.FromProject(Path.Combine(testRoot, Name ?? string.Empty, Name + ".csproj"), this);
        }
 
        public string GetOutputDirectory(string testRoot, string? targetFramework = null, string configuration = "Debug", string runtimeIdentifier = "")
        {
            return GetOutputPathCalculator(testRoot)
                .GetOutputDirectory(targetFramework, configuration, runtimeIdentifier);
        }
 
        public string GetOutputFileName()
        {
            Debug.Assert(Name != null);
            var extension = IsExe
                ? (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty)
                : ".dll";
            return Name + extension;
        }
    }
}