diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/CommandResultAssertions.MSTest.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/CommandResultAssertions.MSTest.cs new file mode 100644 index 000000000000..afc231a5f934 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/CommandResultAssertions.MSTest.cs @@ -0,0 +1,185 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// MSTest-friendly mirror of the shared +// src\TemplateEngine\Tools\Shared\Microsoft.TemplateEngine.CommandUtils\CommandResultAssertions.cs. +// +// The shared file uses Xunit's Assert.True / Assert.False / Assert.NotNull APIs. When this +// project runs under MSTest.Sdk those names resolve to MSTest's Assert (via its implicit +// global using of Microsoft.VisualStudio.TestTools.UnitTesting) which does not expose those +// methods. Rather than dragging the Xunit global using in (which would conflict with MSTest's +// Assert), we exclude the shared file in the csproj and provide this local copy that uses the +// equivalent MSTest APIs: Assert.IsTrue / Assert.IsFalse / Assert.IsNotNull. + +using System.Text.RegularExpressions; + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal class CommandResultAssertions + { + private readonly CommandResult _commandResult; + + internal CommandResultAssertions(CommandResult commandResult) + { + _commandResult = commandResult; + } + + internal CommandResultAssertions And => this; + + internal CommandResultAssertions ExitWith(int expectedExitCode) + { + Assert.IsTrue(expectedExitCode == _commandResult.ExitCode, AppendDiagnosticsTo($"Expected command to exit with {expectedExitCode} but it did not.")); + return this; + } + + internal CommandResultAssertions Pass() + { + Assert.IsTrue(_commandResult.ExitCode == 0, AppendDiagnosticsTo("Expected command to pass but it did not.")); + return this; + } + + internal CommandResultAssertions Fail() + { + Assert.IsFalse(_commandResult.ExitCode == 0, AppendDiagnosticsTo("Expected command to fail but it passed.")); + return this; + } + + internal CommandResultAssertions HaveStdOut() + { + Assert.IsFalse(string.IsNullOrEmpty(_commandResult.StdOut), AppendDiagnosticsTo("Expected command to have standard output but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdOut(string expectedOutput) + { + Assert.IsNotNull(_commandResult.StdOut); + Assert.IsTrue(expectedOutput == _commandResult.StdOut, AppendDiagnosticsTo($"Expected standard output to be '{expectedOutput}' but it was not.")); + return this; + } + + internal CommandResultAssertions HaveStdOutContaining(string pattern) + { + Assert.IsNotNull(_commandResult.StdOut); + Assert.IsTrue(_commandResult.StdOut.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to contain '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdOutContaining(Func predicate, string description = "") + { + Assert.IsNotNull(_commandResult.StdOut); + Assert.IsTrue(predicate(_commandResult.StdOut), $"The command output did not contain expected result: {description} {Environment.NewLine}"); + return this; + } + + internal CommandResultAssertions NotHaveStdOutContaining(string pattern) + { + Assert.IsNotNull(_commandResult.StdOut); + Assert.IsTrue(!_commandResult.StdOut.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to not contain '{pattern}' but it did.")); + return this; + } + + internal CommandResultAssertions HaveStdOutContainingIgnoreSpaces(string pattern) + { + Assert.IsNotNull(_commandResult.StdOut); + string commandResultNoSpaces = _commandResult.StdOut.Replace(" ", string.Empty); + Assert.IsTrue(commandResultNoSpaces.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to contain '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdOutContainingIgnoreCase(string pattern) + { + Assert.IsNotNull(_commandResult.StdOut); + Assert.IsTrue(_commandResult.StdOut.Contains(pattern, StringComparison.OrdinalIgnoreCase), AppendDiagnosticsTo($"Expected standard output to contain '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdOutMatching(string pattern, RegexOptions options = RegexOptions.None) + { + Assert.IsNotNull(_commandResult.StdOut); + Assert.IsTrue(Regex.Match(_commandResult.StdOut, pattern, options).Success, AppendDiagnosticsTo($"Expected standard output to match pattern '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions NotHaveStdOutMatching(string pattern, RegexOptions options = RegexOptions.None) + { + Assert.IsNotNull(_commandResult.StdOut); + Assert.IsTrue(!Regex.Match(_commandResult.StdOut, pattern, options).Success, AppendDiagnosticsTo($"Expected standard output to not match pattern '{pattern}' but it did.")); + return this; + } + + internal CommandResultAssertions HaveStdErr() + { + Assert.IsNotNull(_commandResult.StdErr); + Assert.IsFalse(string.IsNullOrEmpty(_commandResult.StdErr), AppendDiagnosticsTo("Expected command to have standard error but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdErr(string expectedOutput) + { + Assert.IsNotNull(_commandResult.StdErr); + Assert.IsTrue(expectedOutput == _commandResult.StdErr, AppendDiagnosticsTo($"Expected standard error to be '{expectedOutput}' but it was not.")); + return this; + } + + internal CommandResultAssertions HaveStdErrContaining(string pattern) + { + Assert.IsNotNull(_commandResult.StdErr); + Assert.IsTrue(_commandResult.StdErr.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard error to contain '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions NotHaveStdErrContaining(string pattern) + { + Assert.IsNotNull(_commandResult.StdErr); + Assert.IsTrue(!_commandResult.StdErr.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard error to contain '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdErrMatching(string pattern, RegexOptions options = RegexOptions.None) + { + Assert.IsNotNull(_commandResult.StdErr); + Assert.IsTrue(Regex.Match(_commandResult.StdErr, pattern, options).Success, AppendDiagnosticsTo($"Expected standard error to match pattern '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions NotHaveStdOut() + { + Assert.IsNotNull(_commandResult.StdOut); + Assert.IsTrue(string.IsNullOrEmpty(_commandResult.StdOut), AppendDiagnosticsTo("Expected command to not have standard output but it did.")); + return this; + } + + internal CommandResultAssertions NotHaveStdErr() + { + Assert.IsNotNull(_commandResult.StdErr); + Assert.IsTrue(string.IsNullOrEmpty(_commandResult.StdErr), AppendDiagnosticsTo("Expected command to not have standard error but it did.")); + return this; + } + + internal CommandResultAssertions HaveSkippedProjectCompilation(string skippedProject, string frameworkFullName) + { + Assert.IsNotNull(_commandResult.StdOut); + Assert.IsTrue(_commandResult.StdOut.Contains($"Project {skippedProject} ({frameworkFullName}) was previously compiled. Skipping compilation.", StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to contain 'Project {skippedProject} ({frameworkFullName}) was previously compiled. Skipping compilation.' but it did not.")); + return this; + } + + internal CommandResultAssertions HaveCompiledProject(string compiledProject, string frameworkFullName) + { + Assert.IsNotNull(_commandResult.StdOut); + Assert.IsTrue(_commandResult.StdOut.Contains($"Project {compiledProject} ({frameworkFullName}) will be compiled", StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to contain 'Project {compiledProject} ({frameworkFullName}) will be compiled' but it did not.")); + return this; + } + + private string AppendDiagnosticsTo(string s) + { + return (s + $"{Environment.NewLine}" + + $"File Name: {_commandResult.StartInfo.FileName}{Environment.NewLine}" + + $"Arguments: {_commandResult.StartInfo.Arguments}{Environment.NewLine}" + + $"Exit Code: {_commandResult.ExitCode}{Environment.NewLine}" + + $"StdOut:{Environment.NewLine}{_commandResult.StdOut}{Environment.NewLine}" + + $"StdErr:{Environment.NewLine}{_commandResult.StdErr}{Environment.NewLine}") + //escape curly braces for String.Format + .Replace("{", "{{").Replace("}", "}}"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/GlobalUsings.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/GlobalUsings.cs new file mode 100644 index 000000000000..ffde48b08cc6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/GlobalUsings.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// The shared Microsoft.TemplateEngine.CommandUtils sources (compiled into this project +// via ) reference +// xUnit's ITestOutputHelper by its short name. The Xunit type is provided +// transitively (compile-only) through the Microsoft.TemplateEngine.TestHelper project +// reference, but the global using normally added by xunit.v3.extensibility.core's +// build targets does not flow through ProjectReferences. We add the minimal alias +// here so the shared file compiles without dragging the entire Xunit global using +// (which would conflict with MSTest's Assert). +global using ITestOutputHelper = Xunit.ITestOutputHelper; diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/LocalizeTemplateTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/LocalizeTemplateTests.cs index 21488c848eec..f960e6b0d6ea 100644 --- a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/LocalizeTemplateTests.cs +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/LocalizeTemplateTests.cs @@ -1,37 +1,35 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; using Microsoft.TemplateEngine.CommandUtils; using Microsoft.TemplateEngine.TestHelper; using Microsoft.TemplateEngine.Tests; -using Xunit; namespace Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests { + [TestClass] public class LocalizeTemplateTests : TestBase { - private readonly ITestOutputHelper _log; + public TestContext TestContext { get; set; } = null!; - public LocalizeTemplateTests(ITestOutputHelper log) - { - _log = log; - } + private ILogger Log => new TestContextLogger(TestContext); - [Fact] + [TestMethod] public void CanRunTask() { string tmpDir = TestUtils.CreateTemporaryFolder(); TestUtils.DirectoryCopy("Resources/BasicTemplatePackage", tmpDir, true); SetupNuGetConfigForPackagesLocation(tmpDir); - new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + new DotnetCommand(Log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute() .Should() .Pass(); - new DotnetCommand(_log, "build") + new DotnetCommand(Log, "build") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute() @@ -40,28 +38,28 @@ public void CanRunTask() string locFolder = Path.Combine(tmpDir, "content/TemplateWithSourceName/.template.config/localize"); - Assert.True(Directory.Exists(locFolder)); - Assert.Equal(14, Directory.GetFiles(locFolder).Length); - Assert.True(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); + Assert.IsTrue(Directory.Exists(locFolder)); + Assert.AreEqual(14, Directory.GetFiles(locFolder).Length); + Assert.IsTrue(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); Directory.Delete(tmpDir, true); } - [Fact] + [TestMethod] public void CanRunTaskSelectedLangs() { string tmpDir = TestUtils.CreateTemporaryFolder(); TestUtils.DirectoryCopy("Resources/TemplatePackageEnDe", tmpDir, true); SetupNuGetConfigForPackagesLocation(tmpDir); - new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + new DotnetCommand(Log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute() .Should() .Pass(); - new DotnetCommand(_log, "build") + new DotnetCommand(Log, "build") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute() @@ -70,29 +68,29 @@ public void CanRunTaskSelectedLangs() string locFolder = Path.Combine(tmpDir, "content/TemplateWithSourceName/.template.config/localize"); - Assert.True(Directory.Exists(locFolder)); - Assert.Equal(2, Directory.GetFiles(locFolder).Length); - Assert.True(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); - Assert.False(File.Exists(Path.Combine(locFolder, "templatestrings.fr.json"))); + Assert.IsTrue(Directory.Exists(locFolder)); + Assert.AreEqual(2, Directory.GetFiles(locFolder).Length); + Assert.IsTrue(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); + Assert.IsFalse(File.Exists(Path.Combine(locFolder, "templatestrings.fr.json"))); Directory.Delete(tmpDir, true); } - [Fact] + [TestMethod] public void CanRunTaskSelectedTemplates() { string tmpDir = TestUtils.CreateTemporaryFolder(); TestUtils.DirectoryCopy("Resources/TemplatePackagePartiallyLocalized", tmpDir, true); SetupNuGetConfigForPackagesLocation(tmpDir); - new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + new DotnetCommand(Log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute() .Should() .Pass(); - new DotnetCommand(_log, "build") + new DotnetCommand(Log, "build") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute() @@ -102,29 +100,29 @@ public void CanRunTaskSelectedTemplates() string locFolder = Path.Combine(tmpDir, "content/localized/.template.config/localize"); string noLocFolder = Path.Combine(tmpDir, "content/non-localized/.template.config/localize"); - Assert.True(Directory.Exists(locFolder)); - Assert.Equal(14, Directory.GetFiles(locFolder).Length); - Assert.True(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); - Assert.False(Directory.Exists(noLocFolder)); + Assert.IsTrue(Directory.Exists(locFolder)); + Assert.AreEqual(14, Directory.GetFiles(locFolder).Length); + Assert.IsTrue(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); + Assert.IsFalse(Directory.Exists(noLocFolder)); Directory.Delete(tmpDir, true); } - [Fact] + [TestMethod] public void CanRunTaskAndDetectError() { string tmpDir = TestUtils.CreateTemporaryFolder(); TestUtils.DirectoryCopy("Resources/InvalidTemplatePackage", tmpDir, true); SetupNuGetConfigForPackagesLocation(tmpDir); - new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + new DotnetCommand(Log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute() .Should() .Pass(); - new DotnetCommand(_log, "build") + new DotnetCommand(Log, "build") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute() @@ -135,9 +133,8 @@ public void CanRunTaskAndDetectError() string locFolder = Path.Combine(tmpDir, "content/TemplateWithSourceName/.template.config/localize"); - Assert.False(Directory.Exists(locFolder)); + Assert.IsFalse(Directory.Exists(locFolder)); Directory.Delete(tmpDir, true); } - } } diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests.csproj index 4175c1f8f032..ec1219207985 100644 --- a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests.csproj +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests.csproj @@ -1,4 +1,4 @@ - + $(NetCurrent) @@ -9,11 +9,6 @@ - - - - - @@ -25,6 +20,9 @@ + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/TestContextLogger.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/TestContextLogger.cs new file mode 100644 index 000000000000..4fbfcd55fe28 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/TestContextLogger.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests; + +/// +/// Forwards messages to +/// so that shared helpers expecting an can surface their output in +/// MSTest's per-test log. +/// +internal sealed class TestContextLogger(TestContext testContext) : ILogger +{ + public IDisposable? BeginScope(TState state) where TState : notnull => NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + testContext.WriteLine($"{logLevel}: {formatter(state, exception)}"); + if (exception is not null) + { + testContext.WriteLine(exception.ToString()); + } + } + + private sealed class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + + public void Dispose() + { + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/ValidateTemplatesTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/ValidateTemplatesTests.cs index ba51860cb551..126fc564f9e9 100644 --- a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/ValidateTemplatesTests.cs +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/ValidateTemplatesTests.cs @@ -1,37 +1,35 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Logging; using Microsoft.TemplateEngine.CommandUtils; using Microsoft.TemplateEngine.TestHelper; using Microsoft.TemplateEngine.Tests; -using Xunit; namespace Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests { + [TestClass] public class ValidateTemplatesTests : TestBase { - private readonly ITestOutputHelper _log; + public TestContext TestContext { get; set; } = null!; - public ValidateTemplatesTests(ITestOutputHelper log) - { - _log = log; - } + private ILogger Log => new TestContextLogger(TestContext); - [Fact] + [TestMethod] public void CanRunValidateTask_OnError() { string tmpDir = TestUtils.CreateTemporaryFolder(); TestUtils.DirectoryCopy("Resources/InvalidTemplatePackage_MissingName", tmpDir, true); SetupNuGetConfigForPackagesLocation(tmpDir); - new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + new DotnetCommand(Log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute() .Should() .Pass(); - new DotnetCommand(_log, "build") + new DotnetCommand(Log, "build") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute() @@ -41,21 +39,21 @@ public void CanRunValidateTask_OnError() .And.HaveStdOutContaining("Template configuration error MV003: Missing 'shortName'."); } - [Fact] + [TestMethod] public void CanRunValidateTask_OnInfo() { string tmpDir = TestUtils.CreateTemporaryFolder(); TestUtils.DirectoryCopy("Resources/InvalidTemplatePackage_MissingOptionalData", tmpDir, true); SetupNuGetConfigForPackagesLocation(tmpDir); - new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + new DotnetCommand(Log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute() .Should() .Pass(); - new DotnetCommand(_log, "build") + new DotnetCommand(Log, "build") .WithoutTelemetry() .WithWorkingDirectory(tmpDir) .Execute()