diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b5f26b..6e01e20 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,10 +44,10 @@ jobs: echo SEM_VERSION=${{ env.SEM_VERSION }} - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4.1.0 - name: Setup .NET Core 2.1 - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3.2.0 with: dotnet-version: 2.1 @@ -63,23 +63,19 @@ jobs: copy ".\src\AzurePipelines.TestLogger\bin\${{ matrix.Configuration }}\netstandard1.5\*.dll" .\src\AzurePipelines.TestLogger\contentFiles\any\any - name: Pack - run: dotnet pack .\src\AzurePipelines.TestLogger\AzurePipelines.TestLogger.csproj --configuration ${{ matrix.Configuration }} -p:NuspecProperties="Version=${{ env.SEM_VERSION }}" --no-restore --no-build --output:.\build -p:NuspecFile=AzurePipelines.TestLogger.nuspec + run: dotnet pack .\src\AzurePipelines.TestLogger\AzurePipelines.TestLogger.csproj --configuration ${{ matrix.Configuration }} -p:NuspecProperties="Version=${{ env.SEM_VERSION }}" --no-restore --no-build --output:.\build -p:NuspecFile=..\AzurePipelines.TestLogger\AzurePipelines.TestLogger.nuspec - name: Test run: dotnet test --no-build --no-restore --verbosity normal --configuration ${{ matrix.Configuration }} - - name: Create zip - run: | - mkdir AzurePipelines.TestLogger.${{ env.SEM_VERSION }} - copy ".\src\AzurePipelines.TestLogger\bin\${{ matrix.Configuration }}\netstandard1.5\*.*" .\AzurePipelines.TestLogger.${{ env.SEM_VERSION }} - copy "~\.nuget\packages\semver\2.0.6\lib\netstandard1.1\*.*" .\AzurePipelines.TestLogger.${{ env.SEM_VERSION }} - copy ".\LICENSE" .\AzurePipelines.TestLogger.${{ env.SEM_VERSION }} - copy ".\README.md" .\AzurePipelines.TestLogger.${{ env.SEM_VERSION }} - copy ".\ReleaseNotes.md" .\AzurePipelines.TestLogger.${{ env.SEM_VERSION }} - tar -cf AzurePipelines.TestLogger.${{ env.SEM_VERSION }}.zip .\AzurePipelines.TestLogger.${{ env.SEM_VERSION }} + - name: Create Zip + uses: vimtor/action-zip@v1.1 + with: + files: LICENSE README.md ReleaseNotes.md .\src\AzurePipelines.TestLogger\bin\${{ matrix.Configuration }}\netstandard1.5 + dest: AzurePipelines.TestLogger.${{ env.SEM_VERSION }}.zip - name: Upload Build Artifact - uses: actions/upload-artifact@v2.3.1 + uses: actions/upload-artifact@v3.1.3 with: name: AzurePipelines.TestLogger ${{ env.SEM_VERSION }} ${{ matrix.Configuration }} path: AzurePipelines.TestLogger.${{ env.SEM_VERSION }}.zip @@ -95,15 +91,15 @@ jobs: fail_on_unmatched_files: true files: | AzurePipelines.TestLogger.${{ env.SEM_VERSION }}.zip - .\src\AzurePipelines.TestLogger\build\AzurePipelines.TestLogger.${{ env.SEM_VERSION }}.nupkg + .\build\AzurePipelines.TestLogger.${{ env.SEM_VERSION }}.nupkg - name: Upload NuGet Package - uses: actions/upload-artifact@v2.3.1 + uses: actions/upload-artifact@v3.1.3 with: name: AzurePipelines.TestLogger ${{ env.SEM_VERSION }} ${{ matrix.Configuration }}.nupkg - path: .\src\AzurePipelines.TestLogger\build\AzurePipelines.TestLogger.${{ env.SEM_VERSION }}.nupkg + path: .\build\AzurePipelines.TestLogger.${{ env.SEM_VERSION }}.nupkg if-no-files-found: error - name: Publish NuGet Package if: ${{ matrix.Configuration == 'Release' && github.event_name != 'pull_request' }} - run: dotnet nuget push .\src\AzurePipelines.TestLogger\build\AzurePipelines.TestLogger.${{ env.SEM_VERSION }}.nupkg --api-key ${{ secrets.NUGET_KEY }} --source https://api.nuget.org/v3/index.json \ No newline at end of file + run: dotnet nuget push .\build\AzurePipelines.TestLogger.${{ env.SEM_VERSION }}.nupkg --api-key ${{ secrets.NUGET_KEY }} --source https://api.nuget.org/v3/index.json diff --git a/AzurePipelines.TestLogger.sln b/AzurePipelines.TestLogger.sln index a170b6e..27c4992 100644 --- a/AzurePipelines.TestLogger.sln +++ b/AzurePipelines.TestLogger.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31112.23 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34031.279 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C95ECC05-F3E8-49F4-B7C5-A29CD7EACFC1}" EndProject @@ -26,6 +26,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "misc", "misc", "{B8315F74-E stylecop.json = stylecop.json EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleUnitTestProject", "tests\SampleUnitTestProject\SampleUnitTestProject.csproj", "{AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -60,6 +62,18 @@ Global {8C42EBD4-FF36-44B6-A70E-7D83CB0626F8}.Release|x64.Build.0 = Release|Any CPU {8C42EBD4-FF36-44B6-A70E-7D83CB0626F8}.Release|x86.ActiveCfg = Release|Any CPU {8C42EBD4-FF36-44B6-A70E-7D83CB0626F8}.Release|x86.Build.0 = Release|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Debug|x64.ActiveCfg = Debug|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Debug|x64.Build.0 = Debug|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Debug|x86.ActiveCfg = Debug|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Debug|x86.Build.0 = Debug|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Release|Any CPU.Build.0 = Release|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Release|x64.ActiveCfg = Release|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Release|x64.Build.0 = Release|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Release|x86.ActiveCfg = Release|Any CPU + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -67,6 +81,7 @@ Global GlobalSection(NestedProjects) = preSolution {77CA5040-B4A0-4D0B-ADDD-09853A385007} = {C95ECC05-F3E8-49F4-B7C5-A29CD7EACFC1} {8C42EBD4-FF36-44B6-A70E-7D83CB0626F8} = {FA92AD98-1291-4A90-A3AA-ED81A9BBC86E} + {AFB35FB3-F22D-436A-84F7-A5DEFD879D3D} = {FA92AD98-1291-4A90-A3AA-ED81A9BBC86E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A7517899-6171-4E6B-BDD7-DBE01B34E83A} diff --git a/root b/root new file mode 100644 index 0000000..e69de29 diff --git a/src/AzurePipelines.TestLogger/ApiClient.cs b/src/AzurePipelines.TestLogger/ApiClient.cs index 1b40b12..9bf9aa6 100644 --- a/src/AzurePipelines.TestLogger/ApiClient.cs +++ b/src/AzurePipelines.TestLogger/ApiClient.cs @@ -248,7 +248,7 @@ internal virtual async Task SendAsync(HttpMethod method, string endpoint if (Verbose) { - Console.WriteLine($"Request:\n{method} {requestUri}\n{body}\n\nResponse:\n{response.StatusCode}\n{responseBody}"); + Console.WriteLine($"Request:\n{method} {requestUri}\n{body.Indented()}\n\nResponse:\n{response.StatusCode}\n{responseBody.Indented()}\n"); } try diff --git a/src/AzurePipelines.TestLogger/ApiClientV5.cs b/src/AzurePipelines.TestLogger/ApiClientV5.cs index 67e8328..d704e6c 100644 --- a/src/AzurePipelines.TestLogger/ApiClientV5.cs +++ b/src/AzurePipelines.TestLogger/ApiClientV5.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using AzurePipelines.TestLogger.Json; using Microsoft.VisualStudio.TestPlatform.ObjectModel; diff --git a/src/AzurePipelines.TestLogger/AzurePipelines.TestLogger.csproj b/src/AzurePipelines.TestLogger/AzurePipelines.TestLogger.csproj index 33a95fa..b0f3b86 100644 --- a/src/AzurePipelines.TestLogger/AzurePipelines.TestLogger.csproj +++ b/src/AzurePipelines.TestLogger/AzurePipelines.TestLogger.csproj @@ -1,27 +1,27 @@ - - - netstandard1.5 - 1.0.0 - 1.0.0.0 - 1.0.0.0 - + + + netstandard1.5 + 1.0.0 + 1.0.0.0 + 1.0.0.0 + true + + - - - - - - - - - - - - - <_Parameter1>AzurePipelines.TestLogger.Tests - - - <_Parameter1>DynamicProxyGenAssembly2 - - - + + + + + + + <_Parameter1>AzurePipelines.TestLogger.Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + + + + diff --git a/src/AzurePipelines.TestLogger/AzurePipelinesTestLogger.cs b/src/AzurePipelines.TestLogger/AzurePipelinesTestLogger.cs index b5db4a4..da5420a 100644 --- a/src/AzurePipelines.TestLogger/AzurePipelinesTestLogger.cs +++ b/src/AzurePipelines.TestLogger/AzurePipelinesTestLogger.cs @@ -28,6 +28,8 @@ public class AzurePipelinesTestLogger : ITestLoggerWithParameters public AzurePipelinesTestLogger() { + // For debugging purposes + // System.Diagnostics.Debugger.Launch(); _environmentVariableProvider = new EnvironmentVariableProvider(); _apiClientFactory = new ApiClientFactory(); } @@ -137,10 +139,28 @@ private void TestMessageHandler(object sender, TestRunMessageEventArgs e) // Add code to handle message } - private void TestResultHandler(object sender, TestResultEventArgs e) => - _queue.Enqueue(new VstpTestResult(e.Result)); + private void TestResultHandler(object sender, TestResultEventArgs e) + { + try + { + _queue.Enqueue(new VstpTestResult(e.Result)); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } + } - private void TestRunCompleteHandler(object sender, TestRunCompleteEventArgs e) => - _queue.Flush(new VstpTestRunComplete(e.IsAborted || e.IsCanceled, e.AttachmentSets)); + private void TestRunCompleteHandler(object sender, TestRunCompleteEventArgs e) + { + try + { + _queue.Flush(new VstpTestRunComplete(e.IsAborted || e.IsCanceled, e.AttachmentSets)); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + } + } } } diff --git a/src/AzurePipelines.TestLogger/Json/JsonExtensions.cs b/src/AzurePipelines.TestLogger/Json/JsonExtensions.cs index 3a6bb34..f5dbd58 100644 --- a/src/AzurePipelines.TestLogger/Json/JsonExtensions.cs +++ b/src/AzurePipelines.TestLogger/Json/JsonExtensions.cs @@ -83,5 +83,46 @@ private static string JsonEscape(string value) return sb.ToString(); } + + /// + /// Indent JSON string. + /// + /// The JSON string. + /// The indented JSON string. + public static string Indented(this string input) + { + int level = 0; + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < input.Length; i++) + { + char c = input[i]; + + if (c == '{' || c == '[') + { + result.Append(c); + result.AppendLine(); + result.Append(new string(' ', ++level * 2)); + } + else if (c == '}' || c == ']') + { + result.AppendLine(); + result.Append(new string(' ', --level * 2)); + result.Append(c); + } + else if (c == ',') + { + result.Append(c); + result.AppendLine(); + result.Append(new string(' ', level * 2)); + } + else + { + result.Append(c); + } + } + + return result.ToString(); + } } } diff --git a/src/AzurePipelines.TestLogger/LoggerQueue.cs b/src/AzurePipelines.TestLogger/LoggerQueue.cs index a5cc2eb..ca17eea 100644 --- a/src/AzurePipelines.TestLogger/LoggerQueue.cs +++ b/src/AzurePipelines.TestLogger/LoggerQueue.cs @@ -43,24 +43,17 @@ public void Flush(VstpTestRunComplete testRunComplete) // Cancel any idle consumers and let them return _queue.Cancel(); - try - { - // Any active consumer will circle back around and batch post the remaining queue - _consumeTask.Wait(TimeSpan.FromSeconds(60)); + // Any active consumer will circle back around and batch post the remaining queue + _consumeTask.Wait(TimeSpan.FromSeconds(60)); - // Update the run and parents to a completed state - SendTestsCompleted(testRunComplete, _consumeTaskCancellationSource.Token).Wait(TimeSpan.FromSeconds(60)); + // Update the run and parents to a completed state + SendTestsCompleted(testRunComplete, _consumeTaskCancellationSource.Token).Wait(TimeSpan.FromSeconds(60)); - // Cancel any active HTTP requests if still hasn't finished flushing - _consumeTaskCancellationSource.Cancel(); - if (!_consumeTask.Wait(TimeSpan.FromSeconds(10))) - { - throw new TimeoutException("Cancellation didn't happen quickly"); - } - } - catch (Exception ex) + // Cancel any active HTTP requests if still hasn't finished flushing + _consumeTaskCancellationSource.Cancel(); + if (!_consumeTask.Wait(TimeSpan.FromSeconds(10))) { - Console.WriteLine(ex); + throw new TimeoutException("Cancellation didn't happen quickly"); } } @@ -68,47 +61,47 @@ private async Task ConsumeItemsAsync(CancellationToken cancellationToken) { while (true) { - ITestResult[] nextItems = await _queue.TakeAsync().ConfigureAwait(false); - - if (nextItems == null || nextItems.Length == 0) + try { - // Queue is canceling and is empty - return; - } + ITestResult[] nextItems = await _queue.TakeAsync().ConfigureAwait(false); + + if (nextItems == null || nextItems.Length == 0) + { + // Queue is canceling and is empty + return; + } - await SendResultsAsync(nextItems, cancellationToken).ConfigureAwait(false); + await SendResultsAsync(nextItems, cancellationToken).ConfigureAwait(false); - if (cancellationToken.IsCancellationRequested) + if (cancellationToken.IsCancellationRequested) + { + return; + } + } + catch (Exception ex) { - return; + Console.WriteLine(ex); } } } private async Task SendResultsAsync(ITestResult[] testResults, CancellationToken cancellationToken) { - try + // Create a test run if we need it + if (RunId == 0) { - // Create a test run if we need it - if (RunId == 0) - { - Source = GetSource(testResults); - RunId = await CreateTestRun(cancellationToken).ConfigureAwait(false); - } + Source = GetSource(testResults); + RunId = await CreateTestRun(cancellationToken).ConfigureAwait(false); + } - // Group results by their parent - IEnumerable> testResultsByParent = GroupTestResultsByParent(testResults); + // Group results by their parent + IEnumerable> testResultsByParent = GroupTestResultsByParent(testResults); - // Create any required parent nodes - await CreateParents(testResultsByParent, cancellationToken).ConfigureAwait(false); + // Create any required parent nodes + await CreateParents(testResultsByParent, cancellationToken).ConfigureAwait(false); - // Update parents with the test results - await SendTestResults(testResultsByParent, cancellationToken).ConfigureAwait(false); - } - catch (Exception) - { - // Eat any communications exceptions - } + // Update parents with the test results + await SendTestResults(testResultsByParent, cancellationToken).ConfigureAwait(false); } // Internal for testing diff --git a/tests/AzurePipelines.TestLogger.Tests/AzurePipelines.TestLogger.Tests.csproj b/tests/AzurePipelines.TestLogger.Tests/AzurePipelines.TestLogger.Tests.csproj index fcb8ca3..6d83ca0 100644 --- a/tests/AzurePipelines.TestLogger.Tests/AzurePipelines.TestLogger.Tests.csproj +++ b/tests/AzurePipelines.TestLogger.Tests/AzurePipelines.TestLogger.Tests.csproj @@ -2,11 +2,16 @@ netcoreapp2.1 + false - + + + + + @@ -14,6 +19,7 @@ + diff --git a/tests/AzurePipelines.TestLogger.Tests/CaptureRequestsMiddleware.cs b/tests/AzurePipelines.TestLogger.Tests/CaptureRequestsMiddleware.cs new file mode 100644 index 0000000..55cd0e4 --- /dev/null +++ b/tests/AzurePipelines.TestLogger.Tests/CaptureRequestsMiddleware.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace AzurePipelines.TestLogger.Tests +{ + public class CaptureRequestsMiddleware + { + private readonly RequestDelegate _next; + + public CaptureRequestsMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + // Capture the incoming request and store it + IRequestStore requestStore = context.RequestServices.GetService(); + requestStore.Add(context.Request); + + // Call the next middleware in the pipeline + await _next(context); + } + } +} \ No newline at end of file diff --git a/tests/AzurePipelines.TestLogger.Tests/ClientMessage.cs b/tests/AzurePipelines.TestLogger.Tests/ClientMessage.cs index b8748f0..c51a544 100644 --- a/tests/AzurePipelines.TestLogger.Tests/ClientMessage.cs +++ b/tests/AzurePipelines.TestLogger.Tests/ClientMessage.cs @@ -47,5 +47,10 @@ public bool Equals(ClientMessage other) => private static string RemoveWhiteSpace(string str) => string.Concat(str.Split(default(string[]), StringSplitOptions.RemoveEmptyEntries)); + + public override string ToString() + { + return $"Method: {Method}, Endpoint: {Endpoint}, ApiVersion: {ApiVersion}, Body: {Body}"; + } } } diff --git a/tests/AzurePipelines.TestLogger.Tests/IRequestStore.cs b/tests/AzurePipelines.TestLogger.Tests/IRequestStore.cs new file mode 100644 index 0000000..2971d86 --- /dev/null +++ b/tests/AzurePipelines.TestLogger.Tests/IRequestStore.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Http; + +namespace AzurePipelines.TestLogger.Tests +{ + internal interface IRequestStore + { + void Add(HttpRequest item); + } +} \ No newline at end of file diff --git a/tests/AzurePipelines.TestLogger.Tests/IntegrationTests.cs b/tests/AzurePipelines.TestLogger.Tests/IntegrationTests.cs new file mode 100644 index 0000000..88ec029 --- /dev/null +++ b/tests/AzurePipelines.TestLogger.Tests/IntegrationTests.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using SampleUnitTestProject; + +namespace AzurePipelines.TestLogger.Tests +{ + [TestFixture] + public class IntegrationTests + { + private string _vsTestExeFilePath; + private string _sampleUnitTestProjectDllFilePath; + private string _vsTestLoggerDllPath; + + [OneTimeSetUp] + public void SetUpFixture() + { + _vsTestExeFilePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "Microsoft Visual Studio", + "2022", + "Enterprise", + "Common7", + "IDE", + "CommonExtensions", + "Microsoft", + "TestWindow", + "vstest.console.exe"); + + string configuration = "Debug"; + +#if RELEASE + configuration = "Release"; +#endif + + string rootRepositoryPath = GetRootRepositoryPath(); + _sampleUnitTestProjectDllFilePath = Path.Combine(rootRepositoryPath, $@"tests\SampleUnitTestProject\bin\{configuration}\netcoreapp2.1\SampleUnitTestProject.dll"); + _vsTestLoggerDllPath = Path.Combine(rootRepositoryPath, $@"src\AzurePipelines.TestLogger\bin\{configuration}\netstandard1.5"); + } + + [Test] + public void ExecuteTest_WithInvalidAzureDevopsCollectionUri_ContinuesTestExecution() + { + // Given + string fullyQualifiedTestMethodName = GetFullyQualifiedTestMethodName( + typeof(UnitTest1), + nameof(UnitTest1.TestMethod)); + + const string collectionUri = "collectionUri"; + + // When + int exitCode = ExecuteUnitTestWithLogger( + testMethod: fullyQualifiedTestMethodName, + collectionUri: collectionUri); + + // Then + Assert.AreEqual(0, exitCode); + } + + [Test] + public async Task ExecuteTest_WithDataTestMethod_LogsEachDataRow() + { + // Given + string fullyQualifiedTestMethodName = GetFullyQualifiedTestMethodName( + typeof(UnitTest1), + nameof(UnitTest1.DataTestMethod)); + + // When + TestResults testResults = await StartServerAndExecuteUnitTestWithLoggerAsync( + fullyQualifiedTestMethodName); + + // Then + Assert.AreEqual(0, testResults.ExitCode); + Assert.AreEqual(2, testResults.CapturedRequests.Count); + } + + private async Task StartServerAndExecuteUnitTestWithLoggerAsync( + string fullyQualifiedTestMethodName) + { + // Create the Server + IRequestStore requestStore = new RequestStore(); + + IWebHost host = WebHost.CreateDefaultBuilder() + .UseKestrel() + .ConfigureServices(configureServices => + { + configureServices.AddSingleton(requestStore); + }) + .UseUrls("http://127.0.0.1:0") // listen on a random available port + .UseStartup() + .Build(); + + await host.StartAsync(); + + try + { + // Get the server's listening address + IServerAddressesFeature serverAddresses = host.Services + .GetRequiredService() + .Features.Get(); + + string serverUrl = serverAddresses.Addresses.First(); + + Console.WriteLine($"Server is listening on: {serverUrl}"); + + int exitCode = ExecuteUnitTestWithLogger( + testMethod: fullyQualifiedTestMethodName, + collectionUri: $"{serverUrl}/"); + + List capturedRequests = (List)requestStore; + + return new TestResults + { + ExitCode = exitCode, + CapturedRequests = capturedRequests, + }; + } + finally + { + await host.StopAsync(); + } + } + + private class TestResults + { + public int ExitCode { get; set; } + public List CapturedRequests { get; set; } + } + + private static string GetFullyQualifiedTestMethodName(Type type, string methodName) + { + MethodInfo methodInfo = type.GetMethod(methodName); + return $"{type.Namespace}.{type.Name}.{methodInfo.Name}"; + } + + private int ExecuteUnitTestWithLogger( + bool verbose = true, + bool useDefaultCredentials = true, + string apiVersion = "3.0-preview.2", + bool groupTestResultsByClassName = false, + string testMethod = "SampleUnitTestProject.UnitTest1.TestMethod1", + string collectionUri = "collectionUri", + string teamProject = "teamProject", + string buildId = "buildId", + string buildRequestedFor = "buildRequestedFor", + string agentName = "agentName", + string agentJobName = "jobName") + { + List loggerArguments = new List + { + "AzurePipelines", + $"Verbose={verbose}", + $"UseDefaultCredentials={useDefaultCredentials}", + $"ApiVersion={apiVersion}", + $"GroupTestResultsByClassName={groupTestResultsByClassName}" + }; + + List arguments = new List + { + $"\"{_sampleUnitTestProjectDllFilePath}\"", + $"/Tests:{testMethod}", + $"/logger:\"{string.Join(";", loggerArguments)}\"", + $"/TestAdapterPath:\"{_vsTestLoggerDllPath}\"" + }; + + Dictionary environmentVariables = new Dictionary + { + { EnvironmentVariableNames.TeamFoundationCollectionUri, collectionUri }, + { EnvironmentVariableNames.TeamProject, teamProject }, + { EnvironmentVariableNames.BuildId, buildId }, + { EnvironmentVariableNames.BuildRequestedFor, buildRequestedFor }, + { EnvironmentVariableNames.AgentName, agentName }, + { EnvironmentVariableNames.AgentJobName, agentJobName }, + }; + + ProcessRunner processRunner = new ProcessRunner(); + return processRunner.Run(_vsTestExeFilePath, arguments, environmentVariables); + } + + private static string GetRootRepositoryPath() + { + string currentDirectory = Directory.GetCurrentDirectory(); + string fileNameToFind = "root"; + + // Start from the current directory and move up the directory tree + while (!File.Exists(Path.Combine(currentDirectory, fileNameToFind))) + { + string parentDirectory = Directory.GetParent(currentDirectory)?.FullName; + if (parentDirectory == null || parentDirectory == currentDirectory) + { + throw new Exception($"Failed to find file '{fileNameToFind}' in the directory tree."); + } + + currentDirectory = parentDirectory; + } + + return currentDirectory; + } + } +} diff --git a/tests/AzurePipelines.TestLogger.Tests/MockAzureDevOpsTestRunLogCollectorServer.cs b/tests/AzurePipelines.TestLogger.Tests/MockAzureDevOpsTestRunLogCollectorServer.cs new file mode 100644 index 0000000..91a7edf --- /dev/null +++ b/tests/AzurePipelines.TestLogger.Tests/MockAzureDevOpsTestRunLogCollectorServer.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace AzurePipelines.TestLogger.Tests +{ + internal class MockAzureDevOpsTestRunLogCollectorServer + { + public void ConfigureServices(IServiceCollection services) + { + services.AddMvcCore(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseMvc(routes => + { + routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}"); + }); + + app.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/tests/AzurePipelines.TestLogger.Tests/ProcessRunner.cs b/tests/AzurePipelines.TestLogger.Tests/ProcessRunner.cs new file mode 100644 index 0000000..bb68367 --- /dev/null +++ b/tests/AzurePipelines.TestLogger.Tests/ProcessRunner.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading; + +namespace AzurePipelines.TestLogger.Tests +{ + internal class ProcessRunner + { + private StringBuilder _outputAndError; + + public int Run( + string fileName, + List arguments, + IEnumerable> environmentVariables) + { + _outputAndError?.Clear(); + _outputAndError = new StringBuilder(); + + Console.WriteLine($"\"{fileName}\" {string.Join(" ", arguments)}"); + + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = string.Join(" ", arguments), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + // Set environment variables + foreach (KeyValuePair environmentVariable in environmentVariables) + { + if (environmentVariable.Value != null) + { + startInfo.EnvironmentVariables[environmentVariable.Key] = environmentVariable.Value; + } + } + + StringBuilder output = new StringBuilder(); + StringBuilder error = new StringBuilder(); + StringBuilder outputAndError = new StringBuilder(); + + // Start the process + using (Process process = new Process { StartInfo = startInfo }) + { + process.Start(); + + Thread stdOutReaderThread = null; + Thread stdErrReaderThread = null; + + // Invoke stdOut and stdErr readers - each + // has its own thread to guarantee that they aren't + // blocked by, or cause a block to, the actual + // process running (or the gui). + stdOutReaderThread = new Thread(this.ReadStdOut); + stdOutReaderThread.Start(process); + stdErrReaderThread = new Thread(this.ReadStdErr); + stdErrReaderThread.Start(process); + + process.WaitForExit(); + int exitCode = process.ExitCode; + + if (stdOutReaderThread != null) + { + // wait for thread + stdOutReaderThread.Join(); + } + + if (stdErrReaderThread != null) + { + // wait for thread + stdErrReaderThread.Join(); + } + + Console.WriteLine($"Output:\n{_outputAndError.ToString()}"); + + // Check the exit code + Console.WriteLine($"Exit Code: {exitCode}"); + + return exitCode; + } + } + + private void ReadStdOut(object processObj) + { + try + { + string str; + while ((str = ((Process)processObj).StandardOutput.ReadLine()) != null) + { + _outputAndError.AppendLine(str); + } + } + catch + { + } + } + + private void ReadStdErr(object processObj) + { + try + { + string str; + while ((str = ((Process)processObj).StandardError.ReadLine()) != null) + { + _outputAndError.AppendLine(str); + } + } + catch + { + } + } + } +} diff --git a/tests/AzurePipelines.TestLogger.Tests/RequestStore.cs b/tests/AzurePipelines.TestLogger.Tests/RequestStore.cs new file mode 100644 index 0000000..5e23bd9 --- /dev/null +++ b/tests/AzurePipelines.TestLogger.Tests/RequestStore.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace AzurePipelines.TestLogger.Tests +{ + internal class RequestStore : List, IRequestStore + { + } +} diff --git a/tests/SampleUnitTestProject/SampleUnitTestProject.csproj b/tests/SampleUnitTestProject/SampleUnitTestProject.csproj new file mode 100644 index 0000000..76f6681 --- /dev/null +++ b/tests/SampleUnitTestProject/SampleUnitTestProject.csproj @@ -0,0 +1,17 @@ + + + netcoreapp2.1 + false + Library + false + + + + + 2.2.10 + + + 2.2.10 + + + \ No newline at end of file diff --git a/tests/SampleUnitTestProject/UnitTest1.cs b/tests/SampleUnitTestProject/UnitTest1.cs new file mode 100644 index 0000000..8bc54dc --- /dev/null +++ b/tests/SampleUnitTestProject/UnitTest1.cs @@ -0,0 +1,20 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SampleUnitTestProject +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod() + { + } + + [DataTestMethod] + [DataRow(1)] + [DataRow(2)] + public void DataTestMethod(int parameter) + { + } + } +}