diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 6e692d7..0000000 --- a/.appveyor.yml +++ /dev/null @@ -1,20 +0,0 @@ -version: '2.1.{build}' -image: Visual Studio 2017 - -clone_depth: 1 - -build_script: - - cmd: dotnet pack -p:PackageVersion=%APPVEYOR_BUILD_VERSION% - - cmd: dotnet pack -c Release -p:PackageVersion=%APPVEYOR_BUILD_VERSION% - -artifacts: - - path: 'src\package\bin\Release\*.nupkg' - -deploy: - provider: NuGet - server: https://www.myget.org/F/spekt/api/v2 - api_key: - secure: 2C7HbSlU1kcOJ3nzZCpKR97cfWAg/8t38XDf8ywCbJI1ymt93ulfPqT67ugWuMla - artifact: /.*\.nupkg/ - skip_symbols: true - diff --git a/README.md b/README.md index 50465ee..f32f597 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,4 @@ PipelinesTestLogger can report test results automatically to the CI build. ## Credit -This project is based on [appveyor.testlogger](https://github.com/spekt/appveyor.testlogger). \ No newline at end of file +This project is based on [appveyor.testlogger](https://github.com/spekt/appveyor.testlogger) and [xunit](https://github.com/xunit/xunit/blob/master/src/xunit.runner.reporters/VstsReporter.cs). \ No newline at end of file diff --git a/src/PipelinesTestLogger/AsyncProducerConsumerCollection.cs b/src/PipelinesTestLogger/AsyncProducerConsumerCollection.cs index c62a41d..7e7514b 100644 --- a/src/PipelinesTestLogger/AsyncProducerConsumerCollection.cs +++ b/src/PipelinesTestLogger/AsyncProducerConsumerCollection.cs @@ -23,7 +23,7 @@ public void Cancel() waiting.Clear(); } - foreach (var tcs in allWaiting) + foreach (TaskCompletionSource tcs in allWaiting) { tcs.TrySetResult(new T[] { }); } @@ -34,8 +34,14 @@ public void Add(T item) TaskCompletionSource tcs = null; lock (collection) { - if (waiting.Count > 0) tcs = waiting.Dequeue(); - else collection.Enqueue(item); + if (waiting.Count > 0) + { + tcs = waiting.Dequeue(); + } + else + { + collection.Enqueue(item); + } } tcs?.TrySetResult(new [] {item}); @@ -51,13 +57,13 @@ public Task TakeAsync() { if (collection.Count > 0) { - var result = Task.FromResult(collection.ToArray()); + Task result = Task.FromResult(collection.ToArray()); collection.Clear(); return result; } else if (canceled == false) { - var tcs = new TaskCompletionSource(); + TaskCompletionSource tcs = new TaskCompletionSource(); waiting.Enqueue(tcs); return tcs.Task; } diff --git a/src/PipelinesTestLogger/LoggerQueue.cs b/src/PipelinesTestLogger/LoggerQueue.cs index 1cc8604..5ac5b90 100644 --- a/src/PipelinesTestLogger/LoggerQueue.cs +++ b/src/PipelinesTestLogger/LoggerQueue.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -9,45 +10,43 @@ namespace PipelinesTestLogger { internal class LoggerQueue { - private static readonly HttpClient client = new HttpClient(); + private static readonly HttpClient _client = new HttpClient(); - /// - /// it is localhost with a random port, e.g. http://localhost:9023/ - /// - private readonly string appveyorApiUrl; + private readonly string _apiUrl; - private readonly AsyncProducerConsumerCollection queue = new AsyncProducerConsumerCollection(); - private readonly Task consumeTask; - private readonly CancellationTokenSource consumeTaskCancellationSource = new CancellationTokenSource(); + private readonly AsyncProducerConsumerCollection _queue = new AsyncProducerConsumerCollection(); + private readonly Task _consumeTask; + private readonly CancellationTokenSource _consumeTaskCancellationSource = new CancellationTokenSource(); private int totalEnqueued = 0; private int totalSent = 0; - public LoggerQueue(string appveyorApiUrl) + public LoggerQueue(string accessToken, string apiUrl) { - this.appveyorApiUrl = appveyorApiUrl; - this.consumeTask = ConsumeItemsAsync(consumeTaskCancellationSource.Token); + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + _apiUrl = apiUrl; + _consumeTask = ConsumeItemsAsync(_consumeTaskCancellationSource.Token); } public void Enqueue(string json) { - queue.Add(json); + _queue.Add(json); totalEnqueued++; } public void Flush() { // Cancel any idle consumers and let them return - queue.Cancel(); + _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)); // Cancel any active HTTP requests if still hasn't finished flushing - consumeTaskCancellationSource.Cancel(); - if (!consumeTask.Wait(TimeSpan.FromSeconds(10))) + _consumeTaskCancellationSource.Cancel(); + if (!_consumeTask.Wait(TimeSpan.FromSeconds(10))) { throw new TimeoutException("cancellation didn't happen quickly"); } @@ -56,48 +55,36 @@ public void Flush() { Console.WriteLine(ex); } - -#if DEBUG - Console.WriteLine("PipelinesTestLogger: {0} test results reported ({1} enqueued).", totalSent, totalEnqueued); -#endif } private async Task ConsumeItemsAsync(CancellationToken cancellationToken) { while (true) { - string[] nextItems = await this.queue.TakeAsync(); - if (nextItems == null || nextItems.Length == 0) return; // Queue is cancelling and and empty. - - if (nextItems.Length == 1) await PostItemAsync(nextItems[0], cancellationToken); - else if (nextItems.Length > 1) await PostBatchAsync(nextItems, cancellationToken); + string[] nextItems = await _queue.TakeAsync(); - if (cancellationToken.IsCancellationRequested) return; - } - } - - private async Task PostItemAsync(string json, CancellationToken cancellationToken) - { - HttpContent content = new StringContent(json, Encoding.UTF8, "application/json"); - try - { - var response = await client.PostAsync(appveyorApiUrl + "api/tests", content, cancellationToken); - response.EnsureSuccessStatusCode(); - totalSent += 1; - } - catch (Exception e) - { - Console.WriteLine(e); + if (nextItems == null || nextItems.Length == 0) + { + // Queue is canceling and is empty + return; + } + + await PostResultsAsync(nextItems, cancellationToken); + + if (cancellationToken.IsCancellationRequested) + { + return; + } } } - private async Task PostBatchAsync(ICollection jsonEntities, CancellationToken cancellationToken) + private async Task PostResultsAsync(ICollection jsonEntities, CancellationToken cancellationToken) { - var jsonArray = "[" + string.Join(",", jsonEntities) + "]"; + string jsonArray = "[" + string.Join(",", jsonEntities) + "]"; HttpContent content = new StringContent(jsonArray, Encoding.UTF8, "application/json"); try { - var response = await client.PostAsync(appveyorApiUrl + "api/tests/batch", content, cancellationToken); + HttpResponseMessage response = await _client.PostAsync(_apiUrl, content, cancellationToken); response.EnsureSuccessStatusCode(); totalSent += jsonEntities.Count; } diff --git a/src/PipelinesTestLogger/PipelinesTestLogger.cs b/src/PipelinesTestLogger/PipelinesTestLogger.cs index 86c6a3a..25ae50b 100644 --- a/src/PipelinesTestLogger/PipelinesTestLogger.cs +++ b/src/PipelinesTestLogger/PipelinesTestLogger.cs @@ -23,90 +23,77 @@ public class PipelinesTestLogger : ITestLogger /// public const string FriendlyName = "PipelinesTestLogger"; - private LoggerQueue queue; + private LoggerQueue _queue; - /// - /// Initializes the Test Logger. - /// - /// Events that can be registered for. - /// Test Run Directory public void Initialize(TestLoggerEvents events, string testRunDirectory) - { - NotNull(events, nameof(events)); + { + if(!GetRequiredVariable("SYSTEM_ACCESSTOKEN", out string accessToken) + || !GetRequiredVariable("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI", out string collectionUri) + || !GetRequiredVariable("SYSTEM_TEAMPROJECT", out string teamProject) + || !GetRequiredVariable("BUILD_BUILDID", out string buildId)) + { + return; + } + + string apiUrl = $"{collectionUri}{teamProject}/_apis/test/runs/{buildId}/results?api-version=5.0-preview.5"; + _queue = new LoggerQueue(accessToken, apiUrl); - string appveyorApiUrl = Environment.GetEnvironmentVariable("APPVEYOR_API_URL"); + // Register for the events. + events.TestRunMessage += TestMessageHandler; + events.TestResult += TestResultHandler; + events.TestRunComplete += TestRunCompleteHandler; + } - if (appveyorApiUrl == null) + private bool GetRequiredVariable(string name, out string value) + { + value = Environment.GetEnvironmentVariable(name); + if(string.IsNullOrEmpty(value)) { - Console.WriteLine("PipelinesTestLogger: Not an AppVeyor run. Environment variable 'APPVEYOR_API_URL' not set."); - return; - } - -#if DEBUG - Console.WriteLine("PipelinesTestLogger: Logging to {0}", appveyorApiUrl); -#endif - - queue = new LoggerQueue(appveyorApiUrl); - - // Register for the events. - events.TestRunMessage += this.TestMessageHandler; - events.TestResult += this.TestResultHandler; - events.TestRunComplete += this.TestRunCompleteHandler; + Console.WriteLine($"PipelinesTestLogger: Not an Azure Pipelines test run, environment variable { name } not set."); + return false; + } + return true; } - /// - /// Called when a test message is received. - /// - /// - /// The sender. - /// - /// - /// Event args - /// private void TestMessageHandler(object sender, TestRunMessageEventArgs e) { - NotNull(sender, nameof(sender)); - NotNull(e, nameof(e)); - // Add code to handle message } - /// - /// Called when a test result is received. - /// - /// - /// The sender. - /// - /// - /// The eventArgs. - /// private void TestResultHandler(object sender, TestResultEventArgs e) { - string name = e.Result.TestCase.FullyQualifiedName; string filename = string.IsNullOrEmpty(e.Result.TestCase.Source) ? string.Empty : Path.GetFileName(e.Result.TestCase.Source); - string outcome = e.Result.Outcome.ToString(); - var testResult = new Dictionary(); - testResult.Add("testName", name); - testResult.Add("testFramework", e.Result.TestCase.ExecutorUri.ToString()); - testResult.Add("outcome", outcome); + Dictionary testResult = new Dictionary() + { + { "testCaseTitle", e.Result.TestCase.DisplayName }, + { "automatedTestName", e.Result.TestCase.FullyQualifiedName }, + { "outcome", e.Result.Outcome.ToString() }, + { "state", "Completed" }, + { "automatedTestType", "UnitTest" }, + { "automatedTestTypeId", "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b" }, // This is used in the sample response and also appears in web searches + }; if (!string.IsNullOrEmpty(filename)) { - testResult.Add("fileName", filename); + testResult.Add("automatedTestStorage", filename); } if (e.Result.Outcome == TestOutcome.Passed || e.Result.Outcome == TestOutcome.Failed) { int duration = Convert.ToInt32(e.Result.Duration.TotalMilliseconds); + testResult.Add("durationInMs", duration.ToString(CultureInfo.InvariantCulture)); - string errorMessage = e.Result.ErrorMessage; string errorStackTrace = e.Result.ErrorStackTrace; + if (!string.IsNullOrEmpty(errorStackTrace)) + { + testResult.Add("stackTrace", errorStackTrace); + } + string errorMessage = e.Result.ErrorMessage; StringBuilder stdErr = new StringBuilder(); StringBuilder stdOut = new StringBuilder(); - - foreach (var m in e.Result.Messages) + foreach (TestResultMessage m in e.Result.Messages) { if (TestResultMessage.StandardOutCategory.Equals(m.Category, StringComparison.OrdinalIgnoreCase)) { @@ -118,23 +105,9 @@ private void TestResultHandler(object sender, TestResultEventArgs e) } } - testResult.Add("durationMilliseconds", duration.ToString(CultureInfo.InvariantCulture)); - - if (!string.IsNullOrEmpty(errorMessage)) - { - testResult.Add("ErrorMessage", errorMessage); - } - if (!string.IsNullOrEmpty(errorStackTrace)) - { - testResult.Add("ErrorStackTrace", errorStackTrace); - } - if (!string.IsNullOrEmpty(stdOut.ToString())) - { - testResult.Add("StdOut", stdOut.ToString()); - } - if (!string.IsNullOrEmpty(stdErr.ToString())) + if (!string.IsNullOrEmpty(errorMessage) || stdErr.Length > 0 || stdOut.Length > 0) { - testResult.Add("StdErr", stdErr.ToString()); + testResult.Add("errorMessage", $"{errorMessage}\n{stdErr}\n{stdOut}"); } } else @@ -145,28 +118,18 @@ private void TestResultHandler(object sender, TestResultEventArgs e) PublishTestResult(testResult); } - - /// - /// Called when a test run is completed. - /// - /// - /// The sender. - /// - /// - /// Test run complete events arguments. - /// private void TestRunCompleteHandler(object sender, TestRunCompleteEventArgs e) { - queue.Flush(); + _queue.Flush(); } private void PublishTestResult(Dictionary testResult) { - var jsonSb = new StringBuilder(); + StringBuilder jsonSb = new StringBuilder(); jsonSb.Append("{"); bool firstItem = true; - foreach (var field in testResult) + foreach (KeyValuePair field in testResult) { if (!firstItem) { @@ -179,17 +142,7 @@ private void PublishTestResult(Dictionary testResult) jsonSb.Append("}"); - queue.Enqueue(jsonSb.ToString()); - } - - private static T NotNull(T arg, string parameterName) - { - if (arg == null) - { - throw new ArgumentNullException(parameterName); - } - - return arg; + _queue.Enqueue(jsonSb.ToString()); } } }