diff --git a/README.md b/README.md index 3ad8044..7353819 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ rules for your Test and your Prod C# projects. It also provides: * [Test helpers](#test-helpers) to use in your unit test projects helping with test logging or running processes. * A [Playwright Test Builder](#playwright-test-builder) to help you make Web application tests. -* And a [HttpClient mock](#httpclient-mocking) to be able to inject HttpClient when ever you need it in your unit tests; - - +* A [HttpClient mock](#httpclient-mocking) to be able to inject HttpClient when ever you need it in your unit tests. +* A Dotnet Solution builder that can help to process end to end tests on a project consuming the nuget package you + want to test. It can be very handy to test analyzer or code generation tools. Here is [an example](src/tests/SoloX.CodeQuality.Playwright.E2ETest/Package/PackageNugetTest.cs) where it's used. ## Project dashboard [![Build - CI](https://github.com/xaviersolau/CodeQuality/actions/workflows/build-ci.yml/badge.svg)](https://github.com/xaviersolau/CodeQuality/actions/workflows/build-ci.yml) @@ -52,26 +52,26 @@ You can checkout this Github repository or you can use the NuGet package: **Install using the command line from the Package Manager:** ```bash -Install-Package SoloX.CodeQuality.Prod -version 2.3.2 +Install-Package SoloX.CodeQuality.Prod -version 2.3.3 or -Install-Package SoloX.CodeQuality.Test -version 2.3.2 +Install-Package SoloX.CodeQuality.Test -version 2.3.3 ``` **Install using the .Net CLI:** ```bash -dotnet add package SoloX.CodeQuality.Prod --version 2.3.2 +dotnet add package SoloX.CodeQuality.Prod --version 2.3.3 or -dotnet add package SoloX.CodeQuality.Test --version 2.3.2 +dotnet add package SoloX.CodeQuality.Test --version 2.3.3 ``` **Install editing your project file (csproj):** ```xml - + all runtime; build; native; contentfiles; analyzers or - + all runtime; build; native; contentfiles; analyzers @@ -218,17 +218,17 @@ You can checkout this Github repository or use the NuGet package: **Install using the command line from the Package Manager:** ```bash -Install-Package SoloX.CodeQuality.Playwright -version 2.3.2 +Install-Package SoloX.CodeQuality.Playwright -version 2.3.3 ``` **Install using the .Net CLI:** ```bash -dotnet add package SoloX.CodeQuality.Playwright --version 2.3.2 +dotnet add package SoloX.CodeQuality.Playwright --version 2.3.3 ``` **Install editing your project file (csproj):** ```xml - + ``` * * * @@ -424,29 +424,29 @@ You can checkout this Github repository or you can use the NuGet package: **Install using the command line from the Package Manager:** ```bash -Install-Package SoloX.CodeQuality.Test.Helpers -version 2.3.2 +Install-Package SoloX.CodeQuality.Test.Helpers -version 2.3.3 -Install-Package SoloX.CodeQuality.Test.Helpers.XUnit -version 2.3.2 +Install-Package SoloX.CodeQuality.Test.Helpers.XUnit -version 2.3.3 -Install-Package SoloX.CodeQuality.Test.Helpers.NUnit -version 2.3.2 +Install-Package SoloX.CodeQuality.Test.Helpers.NUnit -version 2.3.3 ``` **Install using the .Net CLI:** ```bash -dotnet add package SoloX.CodeQuality.Test.Helpers --version 2.3.2 +dotnet add package SoloX.CodeQuality.Test.Helpers --version 2.3.3 -dotnet add package SoloX.CodeQuality.Test.Helpers.XUnit --version 2.3.2 +dotnet add package SoloX.CodeQuality.Test.Helpers.XUnit --version 2.3.3 -dotnet add package SoloX.CodeQuality.Test.Helpers.NUnit --version 2.3.2 +dotnet add package SoloX.CodeQuality.Test.Helpers.NUnit --version 2.3.3 ``` **Install editing your project file (csproj):** ```xml - + - + - + ``` * * * diff --git a/src/SharedProperties.props b/src/SharedProperties.props index 219f47b..1fc2487 100644 --- a/src/SharedProperties.props +++ b/src/SharedProperties.props @@ -1,7 +1,7 @@ - 2.3.2 + 2.3.3 Xavier Solau Copyright © 2021 Xavier Solau MIT @@ -14,7 +14,9 @@ $(PackageLicenseExpression) LICENSE - 2.3.2.0 + 2.3.3.0 + 12 + diff --git a/src/libs/SoloX.CodeQuality.Playwright/PlaywrightDriver.cs b/src/libs/SoloX.CodeQuality.Playwright/PlaywrightDriver.cs index 2e9f966..3c532e8 100644 --- a/src/libs/SoloX.CodeQuality.Playwright/PlaywrightDriver.cs +++ b/src/libs/SoloX.CodeQuality.Playwright/PlaywrightDriver.cs @@ -167,8 +167,7 @@ private static void InstallPlaywrightBin() /// BrowserNewContextOptions builder. public void SetupBrowserNewContextOptions(string? deviceName = null, Action? browserNewContextOptionsBuilder = null) { - BrowserNewContextOptions options; - if (string.IsNullOrEmpty(deviceName) || !Playwright.Devices.TryGetValue(deviceName, out options)) + if (string.IsNullOrEmpty(deviceName) || !Playwright.Devices.TryGetValue(deviceName, out var options)) { options = new BrowserNewContextOptions(); } @@ -247,7 +246,7 @@ private async Task GotoPageInternalAsync(string url, Func testHandl var gotoResult = await page.GotoAsync(url, new PageGotoOptions { WaitUntil = WaitUntilState.NetworkIdle, Timeout = 60000 }).ConfigureAwait(false); gotoResult.Should().NotBeNull(); - await gotoResult.FinishedAsync().ConfigureAwait(false); + await gotoResult!.FinishedAsync().ConfigureAwait(false); gotoResult.Ok.Should().BeTrue(); @@ -330,7 +329,7 @@ public async ValueTask DisposeAsync() } Playwright.Dispose(); - Playwright = null; + Playwright = default!; } GC.SuppressFinalize(this); diff --git a/src/libs/SoloX.CodeQuality.Playwright/PlaywrightTestBuilder.cs b/src/libs/SoloX.CodeQuality.Playwright/PlaywrightTestBuilder.cs index 2539dd8..90b68cc 100644 --- a/src/libs/SoloX.CodeQuality.Playwright/PlaywrightTestBuilder.cs +++ b/src/libs/SoloX.CodeQuality.Playwright/PlaywrightTestBuilder.cs @@ -303,20 +303,22 @@ private static string GetCallingName() public static MethodBase GetOriginalAsyncMethod(MethodBase method) { + var methodDeclaringType = method.DeclaringType!; + // Check if the method is part of a state machine - var asyncStateMachineAttribute = method.DeclaringType.GetCustomAttribute(); + var asyncStateMachineAttribute = methodDeclaringType.GetCustomAttribute(); - var compilerGeneratedAttribute = method.DeclaringType.GetCustomAttribute(); + var compilerGeneratedAttribute = methodDeclaringType.GetCustomAttribute(); if (asyncStateMachineAttribute != null || compilerGeneratedAttribute != null) { if (method.Name == "MoveNext") { // Get the original type - var declaringType = method.DeclaringType.DeclaringType; + var declaringType = methodDeclaringType.DeclaringType; // The class name will be something like "d__X" - var declaringTypeName = method.DeclaringType.Name; + var declaringTypeName = methodDeclaringType.Name; // Regex pattern to extract the original method name from "d__X" var match = Regex.Match(declaringTypeName, @"\<(?.+)\>d__\d+"); diff --git a/src/libs/SoloX.CodeQuality.Playwright/SoloX.CodeQuality.Playwright.csproj b/src/libs/SoloX.CodeQuality.Playwright/SoloX.CodeQuality.Playwright.csproj index d44a41c..1107789 100644 --- a/src/libs/SoloX.CodeQuality.Playwright/SoloX.CodeQuality.Playwright.csproj +++ b/src/libs/SoloX.CodeQuality.Playwright/SoloX.CodeQuality.Playwright.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers.NUnit/Logger/TestLogger.cs b/src/libs/SoloX.CodeQuality.Test.Helpers.NUnit/Logger/TestLogger.cs index dd5d1d1..d4dd6d9 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers.NUnit/Logger/TestLogger.cs +++ b/src/libs/SoloX.CodeQuality.Test.Helpers.NUnit/Logger/TestLogger.cs @@ -14,7 +14,7 @@ namespace SoloX.CodeQuality.Test.Helpers.NUnit.Logger { public class TestLogger : ILogger { - public IDisposable BeginScope(TState state) + public IDisposable? BeginScope(TState state) where TState : notnull { return new TestLoggerScope(this, state); } @@ -28,8 +28,8 @@ public void Log( LogLevel logLevel, EventId eventId, TState state, - Exception exception, - Func formatter) + Exception? exception, + Func formatter) { TestContext.Out.WriteLine($"{logLevel}: {formatter(state, exception)}"); } diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers.NUnit/SoloX.CodeQuality.Test.Helpers.NUnit.csproj b/src/libs/SoloX.CodeQuality.Test.Helpers.NUnit/SoloX.CodeQuality.Test.Helpers.NUnit.csproj index 320462a..b1baacc 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers.NUnit/SoloX.CodeQuality.Test.Helpers.NUnit.csproj +++ b/src/libs/SoloX.CodeQuality.Test.Helpers.NUnit/SoloX.CodeQuality.Test.Helpers.NUnit.csproj @@ -8,6 +8,7 @@ net8.0;net9.0 + enable SoloX.CodeQuality.Test.Helpers.NUnit @@ -30,8 +31,8 @@ - - + + diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/Logger/TestLogger.cs b/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/Logger/TestLogger.cs index ee0cf34..ff2fcf4 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/Logger/TestLogger.cs +++ b/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/Logger/TestLogger.cs @@ -21,7 +21,7 @@ public TestLogger(ITestOutputHelper testOutputHelper) this.testOutputHelper = testOutputHelper; } - public IDisposable BeginScope(TState state) + public IDisposable? BeginScope(TState state) where TState : notnull { return new TestLoggerScope(this, state); } @@ -35,8 +35,8 @@ public void Log( LogLevel logLevel, EventId eventId, TState state, - Exception exception, - Func formatter) + Exception? exception, + Func formatter) { this.testOutputHelper.WriteLine($"{logLevel}: {formatter(state, exception)}"); } diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/SnapshotHelper.cs b/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/SnapshotHelper.cs index f97fddb..affc75c 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/SnapshotHelper.cs +++ b/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/SnapshotHelper.cs @@ -6,6 +6,7 @@ // // ---------------------------------------------------------------------- +using System; using System.Diagnostics; using System.IO; using Xunit; @@ -62,8 +63,8 @@ public static void AssertSnapshot(string generated, string snapshotName, string } Assert.Equal( - generatedRef.Replace("\r\n", "\n"), - generated.Replace("\r\n", "\n")); + generatedRef.Replace("\r\n", "\n", StringComparison.InvariantCulture), + generated.Replace("\r\n", "\n", StringComparison.InvariantCulture)); } } @@ -74,9 +75,9 @@ public static void AssertSnapshot(string generated, string snapshotName, string /// Folder location in the project of the calling code assembly. public static string GetLocationFromCallingCodeProjectRoot(string folder) { - var callingAssembly = new StackTrace().GetFrame(1).GetMethod().DeclaringType.Assembly; + var callingAssembly = new StackTrace().GetFrame(1)!.GetMethod()!.DeclaringType!.Assembly; var assemblyFolder = Path.GetDirectoryName(callingAssembly.Location); - var projectRoot = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(assemblyFolder))); + var projectRoot = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(assemblyFolder)))!; return folder != null ? Path.Combine(projectRoot, folder) : projectRoot; } } diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/SoloX.CodeQuality.Test.Helpers.XUnit.csproj b/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/SoloX.CodeQuality.Test.Helpers.XUnit.csproj index 5a9adf6..a9ff908 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/SoloX.CodeQuality.Test.Helpers.XUnit.csproj +++ b/src/libs/SoloX.CodeQuality.Test.Helpers.XUnit/SoloX.CodeQuality.Test.Helpers.XUnit.csproj @@ -7,7 +7,8 @@ - netstandard2.0 + net8.0;net9.0 + enable SoloX.CodeQuality.Test.Helpers.XUnit diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/DotnetHelper.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/DotnetHelper.cs index 8c39f12..0d729e1 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers/DotnetHelper.cs +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/DotnetHelper.cs @@ -20,35 +20,71 @@ public static class DotnetHelper private const string PUBLISH = "publish"; private const string RUN = "run"; private const string TEST = "test"; + private const string NEW = "new"; + private const string SLN = "sln"; + private const string ADD = "add"; + private const string PACKAGE = "package"; + private const string REFERENCE = "reference "; - public static bool Restore(string projectPath, out string stdout, out string stderr) + public static bool Restore(string projectPath, out ProcessResult processResult) { - return ProcessHelper.Run(projectPath, DOTNET, RESTORE, out stdout, out stderr) == 0; + return Dotnet(projectPath, RESTORE, out processResult); } - public static bool Build(string projectPath, out string stdout, out string stderr) + public static bool Build(string projectPath, out ProcessResult processResult) { - return ProcessHelper.Run(projectPath, DOTNET, BUILD, out stdout, out stderr) == 0; + return Dotnet(projectPath, BUILD, out processResult); } - public static bool Test(string projectPath, out string stdout, out string stderr) + public static bool Test(string projectPath, out ProcessResult processResult) { - return ProcessHelper.Run(projectPath, DOTNET, TEST, out stdout, out stderr) == 0; + return Dotnet(projectPath, TEST, out processResult); } - public static bool Publish(string projectPath, out string stdout, out string stderr) + public static bool Publish(string projectPath, out ProcessResult processResult) { - return ProcessHelper.Run(projectPath, DOTNET, PUBLISH, out stdout, out stderr) == 0; + return Dotnet(projectPath, PUBLISH, out processResult); } - public static bool Run(string projectPath, out string stdout, out string stderr) + public static bool New(string path, string template, string output, out ProcessResult processResult) { - return Run(projectPath, string.Empty, out stdout, out stderr); + return Dotnet(path, $"{NEW} {template} --output {output}", out processResult); } - public static bool Run(string projectPath, string args, out string stdout, out string stderr) + public static bool NewSln(string path, string solutionName, out ProcessResult processResult) { - return ProcessHelper.Run(projectPath, DOTNET, $"{RUN} {args}", out stdout, out stderr) == 0; + return New(path, SLN, solutionName, out processResult); + } + + public static bool SlnAdd(string path, string project, out ProcessResult processResult) + { + return Dotnet(path, $"{SLN} {ADD} {project}", out processResult); + } + + public static bool AddPackage(string path, string projectFilePath, string packageName, out ProcessResult processResult) + { + return Dotnet(path, $"{ADD} {projectFilePath} {PACKAGE} {packageName}", out processResult); + } + + public static bool AddReference(string path, string projectFilePath, string projectReferenceFilePath, out ProcessResult processResult) + { + return Dotnet(path, $"{ADD} {projectFilePath} {REFERENCE} {projectReferenceFilePath}", out processResult); + } + + public static bool Run(string projectPath, out ProcessResult processResult) + { + return Run(projectPath, string.Empty, out processResult); + } + + public static bool Run(string projectPath, string args, out ProcessResult processResult) + { + return Dotnet(projectPath, $"{RUN} {args}", out processResult); + } + + public static bool Dotnet(string path, string args, out ProcessResult processResult) + { + processResult = ProcessHelper.Run(path, DOTNET, args); + return processResult.ExitCode == 0; } } #pragma warning restore CA1021 // Avoid out parameters diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Http/HttpClientMockBuilder.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Http/HttpClientMockBuilder.cs index 5fe927e..a48d529 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers/Http/HttpClientMockBuilder.cs +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Http/HttpClientMockBuilder.cs @@ -19,17 +19,14 @@ public class HttpClientMockBuilder : IHttpClientMockBuilder /// public IHttpClientRequestMockBuilder WithBaseAddress(Uri baseAddress) { - if (baseAddress is null) - { - throw new ArgumentNullException(nameof(baseAddress)); - } + ArgumentNullException.ThrowIfNull(baseAddress); if (!string.IsNullOrEmpty(baseAddress.Query)) { throw new ArgumentException($"{nameof(baseAddress)} should not contain query data"); } - if (!baseAddress.AbsolutePath.EndsWith("/", StringComparison.Ordinal)) + if (!baseAddress.AbsolutePath.EndsWith('/')) { baseAddress = new Uri($"{baseAddress}/"); } diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Http/IHttpClientRequestMockBuilder.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Http/IHttpClientRequestMockBuilder.cs index 2cd4ee1..33f6752 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers/Http/IHttpClientRequestMockBuilder.cs +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Http/IHttpClientRequestMockBuilder.cs @@ -21,7 +21,7 @@ public interface IHttpClientRequestMockBuilder /// Absolute path to mock. /// HTTP method to mock. /// The response mock builder. - IHttpClientResponseFromRequestMockBuilder WithRequest(string absolutePath, HttpMethod httpMethod = null); + IHttpClientResponseFromRequestMockBuilder WithRequest(string absolutePath, HttpMethod? httpMethod = null); /// /// Setup a HTTP request with a JSON content to be handled by the mock. @@ -30,7 +30,7 @@ public interface IHttpClientRequestMockBuilder /// Absolute path to mock. /// HTTP method to mock. /// The response mock builder. - IHttpClientResponseFromJsonRequestMockBuilder WithJsonContentRequest(string absolutePath, HttpMethod httpMethod = null); + IHttpClientResponseFromJsonRequestMockBuilder WithJsonContentRequest(string absolutePath, HttpMethod? httpMethod = null); /// /// Build the mocked HttpClient. diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/HttpRequestsMockBuilder.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/HttpRequestsMockBuilder.cs index 60d3449..ffcdb6e 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/HttpRequestsMockBuilder.cs +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/HttpRequestsMockBuilder.cs @@ -23,40 +23,33 @@ public HttpRequestsMockBuilder(Uri baseAddress) this.baseAddress = baseAddress; } - public IHttpClientResponseFromJsonRequestMockBuilder WithJsonContentRequest(string absolutePath, HttpMethod httpMethod = null) + public IHttpClientResponseFromJsonRequestMockBuilder WithJsonContentRequest(string absolutePath, HttpMethod? httpMethod = null) { return new JsonRequestBuilder(responseBuilderAsync => Register(absolutePath, httpMethod, responseBuilderAsync)); } - public IHttpClientResponseFromRequestMockBuilder WithRequest(string absolutePath, HttpMethod httpMethod = null) + public IHttpClientResponseFromRequestMockBuilder WithRequest(string absolutePath, HttpMethod? httpMethod = null) { return new RequestBuilder(responseBuilderAsync => Register(absolutePath, httpMethod, responseBuilderAsync)); } - private HttpRequestsMockBuilder Register(string absolutePath, HttpMethod httpMethod, Func> responseBuilderAsync) + private HttpRequestsMockBuilder Register(string absolutePath, HttpMethod? httpMethod, Func> responseBuilderAsync) { - if (absolutePath is null) - { - throw new ArgumentNullException(nameof(absolutePath)); - } + ArgumentNullException.ThrowIfNull(absolutePath); + ArgumentNullException.ThrowIfNull(responseBuilderAsync); if (httpMethod is null) { httpMethod = HttpMethod.Get; } - if (!absolutePath.StartsWith("/", StringComparison.Ordinal)) + if (!absolutePath.StartsWith('/')) { absolutePath = '/' + absolutePath; } var httpMethodName = httpMethod.Method.ToUpperInvariant(); - if (responseBuilderAsync is null) - { - throw new ArgumentNullException(nameof(responseBuilderAsync)); - } - if (!this.responseBuilderMap.TryGetValue(httpMethodName, out var map)) { map = new Dictionary>>(); diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/JsonRequestBuilder.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/JsonRequestBuilder.cs index 90ddf8a..a27a950 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/JsonRequestBuilder.cs +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/JsonRequestBuilder.cs @@ -23,10 +23,7 @@ public JsonRequestBuilder(Func> responseHandler) { - if (responseHandler is null) - { - throw new ArgumentNullException(nameof(responseHandler)); - } + ArgumentNullException.ThrowIfNull(responseHandler); return base.Responding(async request => { @@ -37,20 +34,14 @@ public IHttpClientRequestMockBuilder Responding(Func responseHandler) { - if (responseHandler is null) - { - throw new ArgumentNullException(nameof(responseHandler)); - } + ArgumentNullException.ThrowIfNull(responseHandler); return Responding(request => Task.FromResult(responseHandler(request))); } public IHttpClientRequestMockBuilder RespondingStatus(Func> responseHandler) { - if (responseHandler is null) - { - throw new ArgumentNullException(nameof(responseHandler)); - } + ArgumentNullException.ThrowIfNull(responseHandler); return Responding(async request => { @@ -62,20 +53,14 @@ public IHttpClientRequestMockBuilder RespondingStatus(Func responseHandler) { - if (responseHandler is null) - { - throw new ArgumentNullException(nameof(responseHandler)); - } + ArgumentNullException.ThrowIfNull(responseHandler); return RespondingStatus(request => Task.FromResult(responseHandler(request))); } public IHttpClientRequestMockBuilder RespondingJsonContent(Func> responseHandler, HttpStatusCode status = HttpStatusCode.OK) { - if (responseHandler is null) - { - throw new ArgumentNullException(nameof(responseHandler)); - } + ArgumentNullException.ThrowIfNull(responseHandler); return Responding(async request => { @@ -90,10 +75,7 @@ public IHttpClientRequestMockBuilder RespondingJsonContent(Fun public IHttpClientRequestMockBuilder RespondingJsonContent(Func responseHandler, HttpStatusCode status = HttpStatusCode.OK) { - if (responseHandler is null) - { - throw new ArgumentNullException(nameof(responseHandler)); - } + ArgumentNullException.ThrowIfNull(responseHandler); return RespondingJsonContent(request => Task.FromResult(responseHandler(request)), status); } diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/RequestBuilder.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/RequestBuilder.cs index d7fa3df..f308377 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/RequestBuilder.cs +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Http/Impl/RequestBuilder.cs @@ -30,10 +30,7 @@ public IHttpClientRequestMockBuilder Responding(Func responseHandler) { - if (responseHandler is null) - { - throw new ArgumentNullException(nameof(responseHandler)); - } + ArgumentNullException.ThrowIfNull(responseHandler); return Responding(request => Task.FromResult(responseHandler(request))); } @@ -50,10 +47,7 @@ public IHttpClientRequestMockBuilder RespondingJsonContent(TRe public IHttpClientRequestMockBuilder RespondingStatus(Func> responseHandler) { - if (responseHandler is null) - { - throw new ArgumentNullException(nameof(responseHandler)); - } + ArgumentNullException.ThrowIfNull(responseHandler); return Responding(async request => { @@ -65,20 +59,14 @@ public IHttpClientRequestMockBuilder RespondingStatus(Func responseHandler) { - if (responseHandler is null) - { - throw new ArgumentNullException(nameof(responseHandler)); - } + ArgumentNullException.ThrowIfNull(responseHandler); return RespondingStatus(request => Task.FromResult(responseHandler(request))); } public IHttpClientRequestMockBuilder RespondingJsonContent(Func> responseHandler, HttpStatusCode status = HttpStatusCode.OK) { - if (responseHandler is null) - { - throw new ArgumentNullException(nameof(responseHandler)); - } + ArgumentNullException.ThrowIfNull(responseHandler); return Responding(async request => { @@ -93,10 +81,7 @@ public IHttpClientRequestMockBuilder RespondingJsonContent(Fun public IHttpClientRequestMockBuilder RespondingJsonContent(Func responseHandler, HttpStatusCode status = HttpStatusCode.OK) { - if (responseHandler is null) - { - throw new ArgumentNullException(nameof(responseHandler)); - } + ArgumentNullException.ThrowIfNull(responseHandler); return RespondingJsonContent(request => Task.FromResult(responseHandler(request)), status); } diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/ProcessHelper.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/ProcessHelper.cs index c67f547..3dea878 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers/ProcessHelper.cs +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/ProcessHelper.cs @@ -6,7 +6,10 @@ // // ---------------------------------------------------------------------- +using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Text; using System.Threading; @@ -26,11 +29,51 @@ public static class ProcessHelper /// Standard output. /// Error output. /// The process exit code. - public static int Run(string workingDirectory, string command, string arguments, out string stdout, out string stderr) + public static int Run(string workingDirectory, string command, string arguments, + out string stdout, out string stderr) { - var output = new StringBuilder(); - var error = new StringBuilder(); + var outBuilder = new StringBuilder(); + var errBuilder = new StringBuilder(); + var exitCode = RunInternal( + workingDirectory, + command, + arguments, + s => outBuilder.Append(s), + s => errBuilder.Append(s)); + + stderr = errBuilder.ToString(); + stdout = outBuilder.ToString(); + + return exitCode; + } + + /// + /// Run a process with the given arguments and wait for exit. + /// + /// Working directory. + /// The command to run. + /// The command arguments. + /// The process result. + public static ProcessResult Run(string workingDirectory, string command, string arguments) + { + var processResult = new ProcessResult(); + + var exitCode = RunInternal( + workingDirectory, + command, + arguments, + processResult.AppendInfo, + processResult.AppendError); + + processResult.SetReturnCode(exitCode); + + return processResult; + } + + private static int RunInternal(string workingDirectory, string command, string arguments, + Action outCallback, Action errorCallback) + { using (var process = new Process()) using (var outputWaitHandle = new AutoResetEvent(false)) using (var errorWaitHandle = new AutoResetEvent(false)) @@ -43,7 +86,7 @@ public static int Run(string workingDirectory, string command, string arguments, } else { - output.AppendLine(e.Data); + outCallback(e.Data); } }; process.ErrorDataReceived += (sender, e) => @@ -54,7 +97,7 @@ public static int Run(string workingDirectory, string command, string arguments, } else { - error.AppendLine(e.Data); + errorCallback(e.Data); } }; @@ -78,11 +121,77 @@ public static int Run(string workingDirectory, string command, string arguments, outputWaitHandle.WaitOne(); errorWaitHandle.WaitOne(); - stdout = output.ToString(); - stderr = error.ToString(); - return process.ExitCode; } } } + + /// + /// A log message. + /// + /// Message log. + /// Tells if this is an error log. + public record Log(string Message, bool IsError) + { + public DateTime TimeStamp { get; } = DateTime.Now; + } + + /// + /// Process logs. + /// + public class ProcessResult + { + private readonly List logs = new List(); + + /// + /// Log messages. + /// + public IReadOnlyList LogMessages => this.logs; + + /// + /// Process exit code. + /// + public int ExitCode { get; private set; } + + internal void AppendInfo(string logMessage) + { + this.logs.Add(new Log(logMessage, false)); + } + + internal void AppendError(string logMessage) + { + this.logs.Add(new Log(logMessage, true)); + } + + internal void SetReturnCode(int exitCode) + { + this.ExitCode = exitCode; + } + + public string GetErrors() + { + return GetLogs(l => l.IsError); + } + + public string GetInfo() + { + return GetLogs(l => !l.IsError); + } + + public string GetLogs(Func? filter = null) + { + var stringBuilder = new StringBuilder(); + + var logItems = filter != null + ? this.logs.Where(filter) + : this.logs; + + foreach (var log in logItems) + { + stringBuilder.AppendLine(log.Message); + } + + return stringBuilder.ToString(); + } + } } diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/RandomGenerator.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/RandomGenerator.cs index f770ef4..2fd1ce0 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers/RandomGenerator.cs +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/RandomGenerator.cs @@ -11,6 +11,9 @@ namespace SoloX.CodeQuality.Test.Helpers { + /// + /// RandomGenerator class to generate random values for test purpose. + /// public class RandomGenerator { private readonly Random random = new Random(); diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/SoloX.CodeQuality.Test.Helpers.csproj b/src/libs/SoloX.CodeQuality.Test.Helpers/SoloX.CodeQuality.Test.Helpers.csproj index ecae80c..4baee94 100644 --- a/src/libs/SoloX.CodeQuality.Test.Helpers/SoloX.CodeQuality.Test.Helpers.csproj +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/SoloX.CodeQuality.Test.Helpers.csproj @@ -7,7 +7,8 @@ - netstandard2.0 + net8.0;net9.0 + enable SoloX.CodeQuality.Test.Helpers @@ -29,8 +30,14 @@ - - + + + + + + + + \ No newline at end of file diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Exceptions/ProjectBuilderException.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Exceptions/ProjectBuilderException.cs new file mode 100644 index 0000000..4f6076d --- /dev/null +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Exceptions/ProjectBuilderException.cs @@ -0,0 +1,40 @@ +// ---------------------------------------------------------------------- +// +// Copyright © 2021 Xavier Solau. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. +// +// ---------------------------------------------------------------------- + +using System; + +namespace SoloX.CodeQuality.Test.Helpers.Solution.Exceptions +{ + /// + /// Exception while building a project part. + /// + public class ProjectBuilderException : Exception + { + private readonly ProcessResult? processResult; + + public ProjectBuilderException() + { + } + + public ProjectBuilderException(string message) + : base(message) + { + } + + public ProjectBuilderException(ProcessResult processResult) + : this(processResult.GetLogs()) + { + this.processResult = processResult; + } + + public ProjectBuilderException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Exceptions/SolutionBuilderException.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Exceptions/SolutionBuilderException.cs new file mode 100644 index 0000000..7b9fa3d --- /dev/null +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Exceptions/SolutionBuilderException.cs @@ -0,0 +1,40 @@ +// ---------------------------------------------------------------------- +// +// Copyright © 2021 Xavier Solau. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. +// +// ---------------------------------------------------------------------- + +using System; + +namespace SoloX.CodeQuality.Test.Helpers.Solution.Exceptions +{ + /// + /// Exception while building solution part. + /// + public class SolutionBuilderException : Exception + { + private readonly ProcessResult? processResult; + + public SolutionBuilderException() + { + } + + public SolutionBuilderException(string message) + : base(message) + { + } + + public SolutionBuilderException(ProcessResult processResult) + : this(processResult.GetLogs()) + { + this.processResult = processResult; + } + + public SolutionBuilderException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/INugetConfigConfiguration.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/INugetConfigConfiguration.cs new file mode 100644 index 0000000..c9d6ca5 --- /dev/null +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/INugetConfigConfiguration.cs @@ -0,0 +1,25 @@ +// ---------------------------------------------------------------------- +// +// Copyright © 2021 Xavier Solau. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. +// +// ---------------------------------------------------------------------- + +using System; + +namespace SoloX.CodeQuality.Test.Helpers.Solution +{ + /// + /// Interface to make 'nuget.config' configuration. + /// + public interface INugetConfigConfiguration + { + /// + /// Use package sources. + /// + /// Package sources. + void UsePackageSources(Action packageSources); + } + +} diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/IPackageSources.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/IPackageSources.cs new file mode 100644 index 0000000..2d70ab6 --- /dev/null +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/IPackageSources.cs @@ -0,0 +1,36 @@ +// ---------------------------------------------------------------------- +// +// Copyright © 2021 Xavier Solau. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. +// +// ---------------------------------------------------------------------- + +namespace SoloX.CodeQuality.Test.Helpers.Solution +{ + /// + /// IPackageSources is the interface to configure Nuget package sources. + /// + public interface IPackageSources + { + /// + /// Clear the sources. + /// + /// Self. + IPackageSources Clear(); + + /// + /// Add source folder. + /// + /// Folder path where to load the packages from. + /// Relative path are based on the SolutionBuilder root path. (rooted path can also be used) + /// Self. + IPackageSources Add(string path); + + /// + /// Add the Nuget.org package source. + /// + /// Self. + IPackageSources AddNugetOrg(); + } +} diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/IProjectConfiguration.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/IProjectConfiguration.cs new file mode 100644 index 0000000..d495dfe --- /dev/null +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/IProjectConfiguration.cs @@ -0,0 +1,40 @@ +// ---------------------------------------------------------------------- +// +// Copyright © 2021 Xavier Solau. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. +// +// ---------------------------------------------------------------------- + +using System; + +namespace SoloX.CodeQuality.Test.Helpers.Solution +{ + /// + /// IProjectConfiguration is the interface to create a dotnet project. + /// + public interface IProjectConfiguration + { + /// + /// Use a package reference in the project. + /// + /// The package name. + /// Self. + IProjectConfiguration UsePackageReference(string packageName); + + /// + /// Use a global using in the project. + /// + /// The namespace to use. + /// Self. + IProjectConfiguration UseGlobalUsing(string usingNamespace); + + /// + /// Use files in the project. + /// + /// Project files configuration. + /// Self. + IProjectConfiguration UseFiles(Action value); + } + +} diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/IProjectFiles.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/IProjectFiles.cs new file mode 100644 index 0000000..7239133 --- /dev/null +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/IProjectFiles.cs @@ -0,0 +1,49 @@ +// ---------------------------------------------------------------------- +// +// Copyright © 2021 Xavier Solau. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. +// +// ---------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace SoloX.CodeQuality.Test.Helpers.Solution +{ + /// + /// IProjectFiles provides a way to register files onto the project. + /// + public interface IProjectFiles + { + /// + /// Add a source code file in the project folder. + /// + /// Source file path. Relative to the solution builder root folder. + /// Target file path. Relative to the project folder. + /// Text replace list. + /// Self. + IProjectFiles Add(string source, string target, IEnumerable<(string key, string value)>? replaceItems = null); + + /// + /// Add a content file in the project folder and update the project file. + /// + /// Source file path. Relative to the solution builder root folder. + /// Target file path. Relative to the project folder. + /// Option specifying how to copy the resource file. (PreserveNewest,...) + /// Text replace list. + /// Self. + IProjectFiles AddContent( + string source, + string target, + string? copyToOutputDirectory = null, + IEnumerable<(string key, string value)>? replaceItems = null); + + /// + /// Remove the target file from the project folder. + /// + /// Target file path. Relative to the project folder. + /// Self. + IProjectFiles Remove(string target); + } + +} diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/ISolution.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/ISolution.cs new file mode 100644 index 0000000..976332b --- /dev/null +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/ISolution.cs @@ -0,0 +1,34 @@ +// ---------------------------------------------------------------------- +// +// Copyright © 2021 Xavier Solau. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. +// +// ---------------------------------------------------------------------- + +namespace SoloX.CodeQuality.Test.Helpers.Solution +{ + /// + /// Dotnet Solution interface that helps to run dotnet commands. + /// + public interface ISolution + { + /// + /// Build solution. + /// + /// Project to build or null to build all solution. + void Build(string? project = null); + + /// + /// Test solution. + /// + /// Project to run. + void Run(string? project = null); + + /// + /// Test solution. + /// + /// Project to test or null to run all solution tests. + void Test(string? project = null); + } +} diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Impl/NugetConfigWriter.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Impl/NugetConfigWriter.cs new file mode 100644 index 0000000..4448511 --- /dev/null +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Impl/NugetConfigWriter.cs @@ -0,0 +1,86 @@ +// ---------------------------------------------------------------------- +// +// Copyright © 2021 Xavier Solau. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. +// +// ---------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace SoloX.CodeQuality.Test.Helpers.Solution.Impl +{ + internal class NugetConfigWriter : INugetConfigConfiguration, IPackageSources + { + private readonly SolutionBuilder solutionBuilder; + private readonly string globalPackagesFolder; + + private readonly List config = []; + + public NugetConfigWriter(SolutionBuilder solutionBuilder, string globalPackagesFolder) + { + this.solutionBuilder = solutionBuilder; + this.globalPackagesFolder = globalPackagesFolder; + } + + public IPackageSources Add(string path) + { + var keyName = $"source_{this.config.Count}"; + + var sourcePath = Path.IsPathRooted(path) + ? path + : Path.Combine("..", path); + + this.config.Add($" "); + return this; + } + + public IPackageSources AddNugetOrg() + { + var keyName = $"source_{this.config.Count}"; + this.config.Add($" "); + return this; + } + + public IPackageSources Clear() + { + this.config.Clear(); + + this.config.Add(" "); + return this; + } + + public void UsePackageSources(Action packageSources) + { + packageSources(this); + } + + public void Build() + { + var configFilePath = Path.Combine(this.solutionBuilder.SolutionPath, "nuget.config"); + + IEnumerable begin = + [ + "", + " ", + $" ", + " ", + " ", + ]; + + IEnumerable end = + [ + " ", + "", + ]; + + File.WriteAllLines(configFilePath, + begin + .Concat(this.config) + .Concat(end)); + } + } +} diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Impl/ProjectConfiguration.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Impl/ProjectConfiguration.cs new file mode 100644 index 0000000..e07a420 --- /dev/null +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/Impl/ProjectConfiguration.cs @@ -0,0 +1,165 @@ +// ---------------------------------------------------------------------- +// +// Copyright © 2021 Xavier Solau. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. +// +// ---------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Construction; +using SoloX.CodeQuality.Test.Helpers.Solution.Exceptions; + +namespace SoloX.CodeQuality.Test.Helpers.Solution.Impl +{ + internal class ProjectConfiguration : IProjectConfiguration, IProjectFiles + { + private readonly SolutionBuilder solutionBuilder; + private readonly Action configuration; + + public ProjectConfiguration( + SolutionBuilder solutionBuilder, + string projectName, + string template, + Action configuration) + { + this.solutionBuilder = solutionBuilder; + ProjectName = projectName; + Template = template; + + ProjectFilePath = Path.Combine(projectName, $"{projectName}.csproj"); + ProjectPath = projectName; + + this.configuration = configuration; + } + + /// + /// Project name. + /// + public string ProjectName { get; } + + /// + /// Path to the project folder. Relative to the solution. + /// + public string ProjectPath { get; } + + /// + /// Project file path. Relative to the solution. + /// + public string ProjectFilePath { get; } + + /// + /// Project Template. + /// + public string Template { get; } + + public void Build() + { + DotnetCall((out ProcessResult processResult) => + DotnetHelper.New(this.solutionBuilder.SolutionPath, Template, ProjectName, out processResult) + ); + + this.configuration(this); + } + + public IProjectConfiguration UseFiles(Action files) + { + files(this); + + return this; + } + + public IProjectFiles Add(string source, string target, IEnumerable<(string key, string value)>? replaceItems = null) + { + CopyResourceFile(source, target, replaceItems); + + return this; + } + + public IProjectFiles AddContent( + string source, + string target, + string? copyToOutputDirectory = null, + IEnumerable<(string key, string value)>? replaceItems = null) + { + Add(source, target, replaceItems); + + var projectRoot = ProjectRootElement.Open(Path.Combine(this.solutionBuilder.SolutionPath, ProjectFilePath)); + + var itemGroup = projectRoot.AddItemGroup(); + + var contentItem = itemGroup.AddItem("Content", target); + + if (!string.IsNullOrEmpty(copyToOutputDirectory)) + { + contentItem.AddMetadata("CopyToOutputDirectory", copyToOutputDirectory, expressAsAttribute: true); + } + + projectRoot.Save(); + + return this; + } + + public IProjectFiles Remove(string target) + { + var filePath = Path.Combine(this.solutionBuilder.SolutionPath, this.ProjectPath, target); + + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + + return this; + } + + public IProjectConfiguration UsePackageReference(string packageName) + { + DotnetCall((out ProcessResult processResult) => + DotnetHelper.AddPackage(this.solutionBuilder.SolutionPath, ProjectFilePath, packageName, out processResult) + ); + + return this; + } + + public IProjectConfiguration UseGlobalUsing(string usingNamespace) + { + var projectRoot = ProjectRootElement.Open(Path.Combine(this.solutionBuilder.SolutionPath, ProjectFilePath)); + + var itemGroup = projectRoot.AddItemGroup(); + + var contentItem = itemGroup.AddItem("Using", usingNamespace); + + projectRoot.Save(); + + return this; + } + + private void CopyResourceFile(string filePath, string targetPath, IEnumerable<(string key, string value)>? replaceItems = null) + { + var txt = File.ReadAllText(Path.Combine(this.solutionBuilder.Root, filePath)); + + if (replaceItems != null) + { + foreach (var item in replaceItems) + { + txt = txt.Replace(item.key, item.value, StringComparison.InvariantCulture); + } + } + + File.WriteAllText(Path.Combine(this.solutionBuilder.SolutionPath, this.ProjectPath, targetPath), txt); + } + + + private delegate bool DotnetCallHandler(out ProcessResult processResult); + + private static void DotnetCall(DotnetCallHandler handler) + { + if (!handler(out var processResult)) + { + throw new ProjectBuilderException(processResult); + } + } + } +} diff --git a/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/SolutionBuilder.cs b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/SolutionBuilder.cs new file mode 100644 index 0000000..2c12d89 --- /dev/null +++ b/src/libs/SoloX.CodeQuality.Test.Helpers/Solution/SolutionBuilder.cs @@ -0,0 +1,228 @@ +// ---------------------------------------------------------------------- +// +// Copyright © 2021 Xavier Solau. +// Licensed under the MIT license. +// See LICENSE file in the project root for full license information. +// +// ---------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using SoloX.CodeQuality.Test.Helpers.Solution.Exceptions; +using SoloX.CodeQuality.Test.Helpers.Solution.Impl; + +namespace SoloX.CodeQuality.Test.Helpers.Solution +{ + /// + /// Dotnet fluent solution builder. + /// It helps to create a solution and its projects to make E2E tests dedicated for testing: + /// - Your packaged Nugets; + /// - Your Dotnet tools + /// - Your Dotnet Analyzers; + /// + public class SolutionBuilder + { + private const string DefaultPackageFolder = "Packages"; + private static readonly Action DefaultNugetConfigConfiguration = + configuration => configuration.UsePackageSources(s => + { + s.Clear(); + s.AddNugetOrg(); + }); + + /// + /// Solution builder root folder where the solution will be created. + /// + public string Root { get; } + + /// + /// Name of the solution to build. + /// + public string SolutionName { get; } + + /// + /// Path of the solution to build. + /// + public string SolutionPath { get; } + + private bool withNugetConfig; + private string globalPackagesFolder = DefaultPackageFolder; + private Action nugetConfigConfiguration = DefaultNugetConfigConfiguration; + + private readonly Dictionary projectConfigurations = + new Dictionary(); + + /// + /// Build SolutionBuilder instance. + /// + /// Root folder. + /// Solution name. + public SolutionBuilder(string root, string solutionName) + { + this.Root = root; + this.SolutionName = solutionName; + this.SolutionPath = Path.Combine(this.Root, this.SolutionName); + } + + /// + /// Generate the solution with a nuget.config file. + /// + /// The globalPackagesFolder where Nugets cache is located. + /// Relative to the Solution builder Root folder. + /// nuget.config file configuration. + /// Self. + public SolutionBuilder WithNugetConfig( + string globalPackagesFolder = DefaultPackageFolder, + Action? configuration = null) + { + if (this.withNugetConfig) + { + throw new SolutionBuilderException("WithNugetConfig builder method already called."); + } + + this.withNugetConfig = true; + this.globalPackagesFolder = globalPackagesFolder; + this.nugetConfigConfiguration = configuration ?? DefaultNugetConfigConfiguration; + + return this; + } + + /// + /// Generate the solution with a project named with the given projectName. + /// + /// Name of the project to create. + /// Project Template to use (like 'classlib', 'xunit'...). + /// Project configuration. + /// Self. + public SolutionBuilder WithProject(string projectName, string template, Action configuration) + { + var projectConfiguration = new ProjectConfiguration(this, projectName, template, configuration); + + this.projectConfigurations.Add(projectName, projectConfiguration); + + return this; + } + + /// + /// Build the solution and all its projects. + /// + public ISolution Build() + { + Directory.CreateDirectory(this.Root); + + try + { + if (!Directory.Exists(Path.Combine(this.Root, this.globalPackagesFolder))) + { + Directory.CreateDirectory(Path.Combine(this.Root, this.globalPackagesFolder)); + } + + DotnetCall((out ProcessResult processResult) => + DotnetHelper.NewSln(this.Root, this.SolutionName, out processResult) + ); + + if (this.withNugetConfig) + { + var nugetConfigWriter = new NugetConfigWriter(this, this.globalPackagesFolder); + + this.nugetConfigConfiguration(nugetConfigWriter); + + nugetConfigWriter.Build(); + } + + var projectPathMap = new Dictionary(); + + foreach (var projectConfigurationItem in this.projectConfigurations) + { + var projectConfiguration = projectConfigurationItem.Value; + + projectPathMap.Add( + projectConfiguration.ProjectName, + projectConfiguration.ProjectPath); + + var projectFilePath = projectConfiguration.ProjectFilePath; + + projectConfiguration.Build(); + + DotnetCall((out ProcessResult processResult) => + DotnetHelper.SlnAdd(this.SolutionPath, projectFilePath, out processResult) + ); + } + + return new Solution(this.SolutionPath, projectPathMap); + } + catch (Exception) + { + Directory.Delete(this.Root, true); + + throw; + } + } + + private delegate bool DotnetCallHandler(out ProcessResult processResult); + + private static void DotnetCall(DotnetCallHandler handler) + { + if (!handler(out var processResult)) + { + throw new SolutionBuilderException(processResult); + } + } + + private class Solution : ISolution + { + private readonly string solutionPath; + private readonly IReadOnlyDictionary projectPathMap; + + public Solution(string solutionPath, IReadOnlyDictionary projectPathMap) + { + this.solutionPath = solutionPath; + this.projectPathMap = projectPathMap; + } + + public void Build(string? project = null) + { + var path = string.IsNullOrEmpty(project) + ? this.solutionPath + : GetProjectPath(project); + + DotnetCall((out ProcessResult processResult) => + DotnetHelper.Build(path, out processResult) + ); + } + + public void Run(string? project = null) + { + var path = string.IsNullOrEmpty(project) + ? this.solutionPath + : GetProjectPath(project); + + DotnetCall((out ProcessResult processResult) => + DotnetHelper.Run(path, out processResult) + ); + } + + public void Test(string? project = null) + { + var path = string.IsNullOrEmpty(project) + ? this.solutionPath + : GetProjectPath(project); + + DotnetCall((out ProcessResult processResult) => + DotnetHelper.Test(path, out processResult) + ); + } + + private string GetProjectPath(string project) + { + if (this.projectPathMap.TryGetValue(project, out var projectPath)) + { + return Path.Combine(this.solutionPath, projectPath); + } + + throw new SolutionBuilderException($"Could not find project {project}"); + } + } + } +} diff --git a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Package/PackageNugetTest.cs b/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Package/PackageNugetTest.cs index b5d97e5..03ba8e2 100644 --- a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Package/PackageNugetTest.cs +++ b/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Package/PackageNugetTest.cs @@ -8,43 +8,69 @@ using FluentAssertions; using SoloX.CodeQuality.Test.Helpers; +using SoloX.CodeQuality.Test.Helpers.Solution; namespace SoloX.CodeQuality.Playwright.E2ETest.Package { public class PackageNugetTest { + [Fact] public void IsShouldDeployNugetPackageAndRunTestWithEmbeddedWebHost() { - var configuration = ProbConfiguration(); + var configurationName = ProbConfiguration(); var root = new RandomGenerator().RandomString(4); - var nugetCache = Path.Combine(root, "PkgCache"); - var solutionPath = Path.Combine(root, "PackageNugetTestProject"); - var projectPath = Path.Combine(solutionPath, "PackageNugetTestProject"); - - Directory.CreateDirectory(root); + // Let's create a solution in the random root folder. + var solution = new SolutionBuilder(root, "TestSolution") + // Set up the nuget.config to use a dedicated nuget cache and to specify where to get the nuget + // packages we want to test (generated in the same solution). + .WithNugetConfig(@"PkgCache", configuration => + { + // We want to use the package generated by the projects SoloX.CodeQuality.WebHost and SoloX.CodeQuality.Playwright. + configuration + .UsePackageSources(src => + { + src.Clear() + .Add(@$"../../../../../../libs/SoloX.CodeQuality.WebHost/bin/{configurationName}") + .Add(@$"../../../../../../libs/SoloX.CodeQuality.Playwright/bin/{configurationName}") + .AddNugetOrg(); + }); + }) + // Set up a xunit project to use the nugets. + .WithProject("TestProject", "xunit", configuration => + { + configuration + // Configure the package reference on the package to test. In this case SoloX.CodeQuality.Playwright. + .UsePackageReference("SoloX.CodeQuality.Playwright") + // Add a global using + .UseGlobalUsing("SoloX.CodeQuality.Playwright") + // Create the files + .UseFiles(files => + { + files + .Remove("UnitTest1.cs") + .Add( + @"../PlaywrightTestBuilderLocalTest.cs", + "PlaywrightTestBuilderLocalTest.cs", + [("SoloX.CodeQuality.Playwright.E2ETest", "TestProject")]) + .AddContent(@"../home.html", "home.html", "PreserveNewest"); + }); + }) + // Finally create the solution. + .Build(); try { - Directory.CreateDirectory(nugetCache); - Directory.CreateDirectory(solutionPath); - Directory.CreateDirectory(projectPath); - CopyResourceFile(root, Path.Combine("PackageNugetTestProject", "nuget.config"), [("Configuration", configuration)]); - CopyResourceFile(root, Path.Combine("PackageNugetTestProject", "PackageNugetTestProject.sln")); - CopyResourceFile(root, Path.Combine("PackageNugetTestProject", "PackageNugetTestProject", "home.html")); - CopyResourceFile(root, Path.Combine("PackageNugetTestProject", "PackageNugetTestProject", "PackageNugetTestProject.csproj")); - CopyResourceFile(root, Path.Combine("PackageNugetTestProject", "PackageNugetTestProject", "PlaywrightTestBuilderLocalTest.cs")); + var actBuild = () => solution.Build(); - var build = DotnetHelper.Build(Path.Combine(root, "PackageNugetTestProject"), out var _, out var _); + actBuild.Should().NotThrow(); - build.Should().BeTrue(); + var actTest = () => solution.Test(); - var test = DotnetHelper.Test(Path.Combine(root, "PackageNugetTestProject"), out var _, out var _); - - test.Should().BeTrue(); + actTest.Should().NotThrow(); } finally { @@ -58,20 +84,5 @@ private static string ProbConfiguration() return Path.GetFileName(Path.GetDirectoryName(location)!); } - - private static void CopyResourceFile(string target, string filePath, IEnumerable<(string key, string value)> replaceItems = null) - { - var txt = File.ReadAllText(Path.Combine("Resources", filePath)); - - if (replaceItems != null) - { - foreach (var item in replaceItems) - { - txt = txt.Replace(item.key, item.value, StringComparison.InvariantCulture); - } - } - - File.WriteAllText(Path.Combine(target, filePath), txt); - } } } diff --git a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject.sln b/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject.sln deleted file mode 100644 index 0e6e8cb..0000000 --- a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.11.35312.102 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackageNugetTestProject", "PackageNugetTestProject\PackageNugetTestProject.csproj", "{2F1F30D8-222F-4038-816C-AEFB2EC5F366}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2F1F30D8-222F-4038-816C-AEFB2EC5F366}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2F1F30D8-222F-4038-816C-AEFB2EC5F366}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2F1F30D8-222F-4038-816C-AEFB2EC5F366}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2F1F30D8-222F-4038-816C-AEFB2EC5F366}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {3C050F19-F497-46D6-8BFD-DA61EE10FBBB} - EndGlobalSection -EndGlobal diff --git a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject/PlaywrightTestBuilderLocalTest.cs b/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject/PlaywrightTestBuilderLocalTest.cs deleted file mode 100644 index d8b70bc..0000000 --- a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject/PlaywrightTestBuilderLocalTest.cs +++ /dev/null @@ -1,72 +0,0 @@ -using FluentAssertions; -using SoloX.CodeQuality.Playwright; - -namespace PackageNugetTestProject -{ - public class PlaywrightTestBuilderLocalTest - { - private readonly IPlaywrightTestBuilder builder; - - public PlaywrightTestBuilderLocalTest() - { - var path = Path.GetFullPath(Path.GetDirectoryName(this.GetType().Assembly.Location)!); - - this.builder = PlaywrightTestBuilder.Create() - .WithLocalHost(localHostBuilder => - { - localHostBuilder - .UsePortRange(new PortRange(6000, 7000)) - .UseWebHostWithWwwRoot(path, "home.html") - .UseWebHostBuilder(builder => - { - //builder.ConfigureServices(services => - //{ - // services.AddTransient(); - //}) - //.ConfigureAppConfiguration((app, conf) => - //{ - // conf.AddJsonFile("appsettings.Test.json"); - //}) - //.UseSetting("SomeKey", "SomeValue"); - }); - }) - .WithPlaywrightOptions(opt => - { - //opt.Headless = false; - //opt.SlowMo = 1000; - //opt.Timeout = 60000; - }) - .WithPlaywrightNewContextOptions(opt => - { - //opt.ScreenSize = new Microsoft.Playwright.ScreenSize() { Height = 800, Width = 1000 }; - //opt.StorageStatePath = "State Json file"; - }); - } - - [Theory] - [InlineData(Browser.Chromium)] - public async Task ItShouldOpenTheHomePageFromStaticHomeFile(Browser browser) - { - var playwrightTest = await this.builder - .BuildAsync(browser) - .ConfigureAwait(true); - - await using var _ = playwrightTest.ConfigureAwait(false); - - await playwrightTest - .GotoPageAsync( - "home.html", - async (page) => - { - var body = page.Locator("body"); - - await body.WaitForAsync().ConfigureAwait(true); - - var title = await page.TitleAsync().ConfigureAwait(true); - - title.Should().Be("Home Title"); - - }).ConfigureAwait(true); - } - } -} \ No newline at end of file diff --git a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject/home.html b/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject/home.html deleted file mode 100644 index aef3194..0000000 --- a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject/home.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Home Title - - - - - -

This is a test.

- - - \ No newline at end of file diff --git a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/nuget.config b/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/nuget.config deleted file mode 100644 index 5ff084c..0000000 --- a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/nuget.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/SoloX.CodeQuality.Playwright.E2ETest.csproj b/src/tests/SoloX.CodeQuality.Playwright.E2ETest/SoloX.CodeQuality.Playwright.E2ETest.csproj index e642841..cab050a 100644 --- a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/SoloX.CodeQuality.Playwright.E2ETest.csproj +++ b/src/tests/SoloX.CodeQuality.Playwright.E2ETest/SoloX.CodeQuality.Playwright.E2ETest.csproj @@ -49,17 +49,9 @@
- - - - - - - - - + PreserveNewest - + diff --git a/src/tests/SoloX.CodeQuality.Test.Helpers.UTest/ProcessHelperTest.cs b/src/tests/SoloX.CodeQuality.Test.Helpers.UTest/ProcessHelperTest.cs index ba475f3..710449d 100644 --- a/src/tests/SoloX.CodeQuality.Test.Helpers.UTest/ProcessHelperTest.cs +++ b/src/tests/SoloX.CodeQuality.Test.Helpers.UTest/ProcessHelperTest.cs @@ -6,6 +6,7 @@ // // ---------------------------------------------------------------------- +using System.Linq; using FluentAssertions; using Xunit; @@ -26,5 +27,21 @@ public void IsShouldRunACommandAndGetTheOutput() stdout.Should().Contain("Microsoft."); stdout.Should().Contain(version); } + + [Fact] + public void IsShouldRunACommandAndGetTheLogs() + { + var version = System.Environment.Version.ToString(); + + var processResult = ProcessHelper.Run(".", "dotnet", "--list-runtimes"); + + processResult.ExitCode.Should().Be(0); + + processResult.LogMessages.Where(x => x.IsError).Should().BeEmpty(); + + var logs = processResult.GetLogs(); + logs.Should().Contain("Microsoft."); + logs.Should().Contain(version); + } } }