Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -226,20 +226,21 @@ public void EndBatchBuild()
{
Debug.Assert(BatchBuildStarted);

var targets = new[] { TargetNames.Compile, TargetNames.CoreCompile };
var requiredTargets = new[] { TargetNames.Compile, TargetNames.CoreCompile };
var optionalTargets = new[] { TargetNames.DesignTimeMarkupCompilation };

return BuildProjectAsync(project, targets, log, cancellationToken);
return BuildProjectAsync(project, requiredTargets, optionalTargets, log, cancellationToken);
}

private async Task<MSB.Execution.ProjectInstance> BuildProjectAsync(
MSB.Evaluation.Project project, string[] targets, DiagnosticLog log, CancellationToken cancellationToken)
MSB.Evaluation.Project project, string[] requiredTargets, string[] optionalTargets, DiagnosticLog log, CancellationToken cancellationToken)
{
// create a project instance to be executed by build engine.
// The executed project will hold the final model of the project after execution via msbuild.
var projectInstance = project.CreateProjectInstance();

// Verify targets
foreach (var target in targets)
foreach (var target in requiredTargets)
{
if (!projectInstance.Targets.ContainsKey(target))
{
Expand All @@ -248,9 +249,18 @@ public void EndBatchBuild()
}
}

var targets = new List<string>(requiredTargets);
foreach (var target in optionalTargets)
{
if (projectInstance.Targets.ContainsKey(target))
{
targets.Add(target);
}
}

_batchBuildLogger?.SetProjectAndLog(projectInstance.FullPath, log);

var buildRequestData = new MSB.Execution.BuildRequestData(projectInstance, targets);
var buildRequestData = new MSB.Execution.BuildRequestData(projectInstance, targets.ToArray());

var result = await BuildAsync(buildRequestData, cancellationToken).ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ internal static class TargetNames
{
public const string Compile = nameof(Compile);
public const string CoreCompile = nameof(CoreCompile);
public const string DesignTimeMarkupCompilation = nameof(DesignTimeMarkupCompilation);
}
}
264 changes: 264 additions & 0 deletions src/Workspaces/MSBuildTest/NewlyCreatedProjectsFromDotNetNew.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.CodeAnalysis.MSBuild.UnitTests
{
public class NewlyCreatedProjectsFromDotNetNew : MSBuildWorkspaceTestBase
{
private static readonly string s_globalJsonPath;

// The Maui templates require additional dotnet workloads to be installed.
// Running `dotnet workload restore` will install workloads but may require
// admin permissions. In addition a restart may be required after workload
// installation.
private const bool ExcludeMauiTemplates = true;

protected ITestOutputHelper TestOutputHelper { get; set; }

static NewlyCreatedProjectsFromDotNetNew()
{
// We'll use the same global.json as we use for our own build.
s_globalJsonPath = Path.Combine(GetSolutionFolder(), "global.json");

static string GetSolutionFolder()
{
// Expected assembly path:
// <solutionFolder>\artifacts\bin\Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests\<Configuration>\<TFM>\Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests.dll
var assemblyLocation = typeof(DotNetSdkMSBuildInstalled).Assembly.Location;
var solutionFolder = Directory.GetParent(assemblyLocation)
?.Parent?.Parent?.Parent?.Parent?.Parent?.FullName;
Assumes.NotNull(solutionFolder);
return solutionFolder;
}
}

public NewlyCreatedProjectsFromDotNetNew(ITestOutputHelper output)
{
TestOutputHelper = output;
}

[ConditionalTheory(typeof(DotNetSdkMSBuildInstalled))]
[MemberData(nameof(GetCSharpProjectTemplateNames), DisableDiscoveryEnumeration = false)]
public async Task ValidateCSharpTemplateProjects(string templateName)
{
var ignoredDiagnostics = templateName switch
{
"blazor" or "blazorwasm" or "blazorwasm-empty" =>
[
// The type or namespace name {'csharp_blazor_project'|'App'} could not be found
// (are you missing a using directive or an assembly reference?)
// Bug: https://github.com/dotnet/roslyn/issues/72015
"CS0246",
],
_ => Array.Empty<string>(),
};

await AssertTemplateProjectLoadsCleanlyAsync(templateName, LanguageNames.CSharp, ignoredDiagnostics);
}

[ConditionalTheory(typeof(DotNetSdkMSBuildInstalled))]
[MemberData(nameof(GetVisualBasicProjectTemplateNames), DisableDiscoveryEnumeration = false)]
public async Task ValidateVisualBasicTemplateProjects(string templateName)
{
var ignoredDiagnostics = !ExecutionConditionUtil.IsWindows
? [
// Type 'Global.Microsoft.VisualBasic.ApplicationServices.ApplicationBase' is not defined.
// Bug: https://github.com/dotnet/roslyn/issues/72014
"BC30002",
]
: Array.Empty<string>();

await AssertTemplateProjectLoadsCleanlyAsync(templateName, LanguageNames.VisualBasic, ignoredDiagnostics);
}

public static TheoryData<string> GetCSharpProjectTemplateNames()
=> GetProjectTemplateNames("c#");

public static TheoryData<string> GetVisualBasicProjectTemplateNames()
=> GetProjectTemplateNames("vb");

public static TheoryData<string> GetProjectTemplateNames(string language)
{
// The expected output from the list command is as follows.

// These templates matched your input: --language='vb', --type='project'
//
// Template Name Short Name Language Tags
// ----------------------------- ------------------- -------- ---------------
// Class Library classlib C#,F#,VB Common/Library
// Console App console C#,F#,VB Common/Console
// ...

var result = RunDotNet($"new list --type project --language {language}", output: null);

var lines = result.Output.Split(new[] { "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);

TheoryData<string> templateNames = [];
var foundDivider = false;

foreach (var line in lines)
{
if (!foundDivider)
{
if (line.StartsWith("----"))
{
foundDivider = true;
}
continue;
}

var columns = line.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries)
.Select(c => c.Trim())
.ToArray();
var templateShortName = columns[1].Split(',').First();

if (ExcludeMauiTemplates && templateShortName.StartsWith("maui"))
continue;

templateNames.Add(templateShortName);
}

Assert.True(foundDivider);

return templateNames;
}

private async Task AssertTemplateProjectLoadsCleanlyAsync(string templateName, string languageName, string[]? ignoredDiagnostics = null)
{
if (ignoredDiagnostics?.Length > 0)
{
TestOutputHelper.WriteLine($"Ignoring compiler diagnostics: \"{string.Join("\", \"", ignoredDiagnostics)}\"");
}

var projectDirectory = SolutionDirectory.Path;
var projectFilePath = GetProjectFilePath(projectDirectory, languageName);

CreateNewProject(templateName, projectDirectory, languageName, TestOutputHelper);

await AssertProjectLoadsCleanlyAsync(projectFilePath, ignoredDiagnostics ?? []);

return;

static string GetProjectFilePath(string projectDirectory, string languageName)
{
var projectName = new DirectoryInfo(projectDirectory).Name;
var projectExtension = languageName switch
{
LanguageNames.CSharp => "csproj",
LanguageNames.VisualBasic => "vbproj",
_ => throw new ArgumentOutOfRangeException(nameof(languageName), actualValue: languageName, message: "Only C# and VB.NET projects are supported.")
};
return Path.Combine(projectDirectory, $"{projectName}.{projectExtension}");
}

static void CreateNewProject(string templateName, string outputDirectory, string languageName, ITestOutputHelper output)
{
var language = languageName switch
{
LanguageNames.CSharp => "C#",
LanguageNames.VisualBasic => "VB",
_ => throw new ArgumentOutOfRangeException(nameof(languageName), actualValue: languageName, message: "Only C# and VB.NET projects are supported.")
};

TryCopyGlobalJson(outputDirectory);

var newResult = RunDotNet($"new \"{templateName}\" -o \"{outputDirectory}\" --language \"{language}\"", output, outputDirectory);

// Most templates invoke restore as a post-creation action. However, some, like the
// Maui templates, do not run restore since they require additional workloads to be
// installed.
if (newResult.Output.Contains("Restoring"))
{
return;
}

try
{
// Attempt a restore and see if we are instructed to install additional workloads.
var restoreResult = RunDotNet($"restore", output, outputDirectory);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("command: dotnet workload restore"))
{
throw new InvalidOperationException($"The '{templateName}' template requires additional dotnet workloads to be installed. It should be excluded during template discovery. " + ex.Message);
}
}

static void TryCopyGlobalJson(string outputDirectory)
{
var tempGlobalJsonPath = Path.Combine(outputDirectory, "global.json");
try
{
File.Copy(s_globalJsonPath, tempGlobalJsonPath);
}
catch (FileNotFoundException)
{
}
}

static async Task AssertProjectLoadsCleanlyAsync(string projectFilePath, string[] ignoredDiagnostics)
{
using var workspace = CreateMSBuildWorkspace();
var project = await workspace.OpenProjectAsync(projectFilePath, cancellationToken: CancellationToken.None);

AssertEx.Empty(workspace.Diagnostics, $"The following workspace diagnostics are being reported for the template.");

var compilation = await project.GetRequiredCompilationAsync(CancellationToken.None);

// Unnecessary using directives are reported with a severity of Hidden
var nonHiddenDiagnostics = compilation!.GetDiagnostics()
.Where(diagnostic => diagnostic.Severity > DiagnosticSeverity.Hidden)
.ToImmutableArray();

// For good test hygiene lets ensure that all ignored diagnostics were actually reported.
var reportedDiagnosticIds = nonHiddenDiagnostics
.Select(diagnostic => diagnostic.Id)
.ToImmutableHashSet();
var unnecessaryIgnoreDiagnostics = ignoredDiagnostics
.Where(id => !reportedDiagnosticIds.Contains(id));

AssertEx.Empty(unnecessaryIgnoreDiagnostics, $"The following diagnostics are unnecessarily being ignored for the template.");

var filteredDiagnostics = nonHiddenDiagnostics
.Where(diagnostic => !ignoredDiagnostics.Contains(diagnostic.Id));

AssertEx.Empty(filteredDiagnostics, $"The following compiler diagnostics are being reported for the template.");
}
}

private static ProcessResult RunDotNet(string arguments, ITestOutputHelper? output, string? workingDirectory = null)
{
var dotNetExeName = "dotnet" + (Path.DirectorySeparatorChar == '/' ? "" : ".exe");

var result = ProcessUtilities.Run(dotNetExeName, arguments, workingDirectory, additionalEnvironmentVars: [new KeyValuePair<string, string>("DOTNET_CLI_UI_LANGUAGE", "en")]);

if (result.ExitCode != 0)
{
throw new InvalidOperationException(string.Join(Environment.NewLine,
[
$"`dotnet {arguments}` returned a non-zero exit code.",
"Output:",
result.Output,
"Error:",
result.Errors
]));
}

output?.WriteLine(result.Output);

return result;
}
}
}