Skip to content

Commit b4f7db8

Browse files
authored
Merge pull request #8 from icnocop/main
Uploading attachments from test run completed event
2 parents b0a876a + f6dc2b1 commit b4f7db8

7 files changed

Lines changed: 192 additions & 113 deletions

File tree

AzurePipelines.TestLogger.sln

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Microsoft Visual Studio Solution File, Format Version 12.00
2-
# Visual Studio 15
3-
VisualStudioVersion = 15.0.26124.0
4-
MinimumVisualStudioVersion = 15.0.26124.0
2+
# Visual Studio Version 16
3+
VisualStudioVersion = 16.0.31112.23
4+
MinimumVisualStudioVersion = 10.0.40219.1
55
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C95ECC05-F3E8-49F4-B7C5-A29CD7EACFC1}"
66
EndProject
77
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzurePipelines.TestLogger", "src\AzurePipelines.TestLogger\AzurePipelines.TestLogger.csproj", "{77CA5040-B4A0-4D0B-ADDD-09853A385007}"

src/AzurePipelines.TestLogger/ApiClient.cs

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ public async Task UpdateTestResults(int testRunId, Dictionary<string, TestResult
9797
await UploadTestResultFiles(testRunId, testCaseTestResults, testResultsByParent, cancellationToken);
9898
}
9999

100+
public async Task UpdateTestResults(int testRunId, VstpTestRunComplete testRunComplete, CancellationToken cancellationToken)
101+
{
102+
await UploadTestResultFiles(testRunId, null, testRunComplete.Attachments, cancellationToken);
103+
}
104+
100105
public async Task<int[]> AddTestCases(int testRunId, string[] testCaseNames, DateTime startedDate, string source, CancellationToken cancellationToken)
101106
{
102107
string requestBody = "[ " + string.Join(", ", testCaseNames.Select(x =>
@@ -154,9 +159,33 @@ public async Task MarkTestRunCompleted(int testRunId, DateTime startedDate, Date
154159

155160
protected Dictionary<string, object> GetTestResultProperties(ITestResult testResult)
156161
{
162+
// https://docs.microsoft.com/en-us/rest/api/azure/devops/test/results/list?view=azure-devops-rest-6.0#testcaseresult
163+
// outcome valid values = (Unspecified, None, Passed, Failed, Inconclusive, Timeout, Aborted, Blocked, NotExecuted, Warning, Error, NotApplicable, Paused, InProgress, NotImpacted)
164+
string testOutcome;
165+
switch (testResult.Outcome)
166+
{
167+
case TestOutcome.None:
168+
testOutcome = "None";
169+
break;
170+
case TestOutcome.Passed:
171+
testOutcome = "Passed";
172+
break;
173+
case TestOutcome.Failed:
174+
testOutcome = "Failed";
175+
break;
176+
case TestOutcome.Skipped:
177+
testOutcome = "Inconclusive";
178+
break;
179+
case TestOutcome.NotFound:
180+
testOutcome = "NotExecuted";
181+
break;
182+
default:
183+
throw new ArgumentOutOfRangeException(nameof(testResult.Outcome), testResult.Outcome.ToString());
184+
}
185+
157186
Dictionary<string, object> properties = new Dictionary<string, object>
158187
{
159-
{ "outcome", testResult.Outcome.ToString() },
188+
{ "outcome", testOutcome },
160189
{ "computerName", testResult.ComputerName },
161190
{ "runBy", new Dictionary<string, object> { { "displayName", BuildRequestedFor } } }
162191
};
@@ -285,25 +314,39 @@ private async Task UploadTestResultFiles(int testRunId, Dictionary<string, TestR
285314

286315
foreach (ITestResult testResult in testResultByParent.Select(x => x))
287316
{
288-
if (testResult.Attachments.Count > 0)
289-
{
290-
Console.WriteLine($"Attaching files to test run {testRunId} and test result {parent.Id}...");
291-
}
317+
await UploadTestResultFiles(testRunId, parent.Id, testResult.Attachments, cancellationToken);
318+
}
319+
}
320+
}
292321

293-
foreach (AttachmentSet attachmentSet in testResult.Attachments)
294-
{
295-
if (attachmentSet.Attachments.Count > 0)
296-
{
297-
Console.WriteLine($"Attaching files in set {attachmentSet.DisplayName} {attachmentSet.Uri}...");
298-
}
322+
private async Task UploadTestResultFiles(int testRunId, int? testResultId, ICollection<AttachmentSet> attachmentSets, CancellationToken cancellationToken)
323+
{
324+
if (attachmentSets.Count > 0)
325+
{
326+
string message = $"Attaching files to test run {testRunId}";
299327

300-
foreach (UriDataAttachment attachment in attachmentSet.Attachments)
301-
{
302-
Console.WriteLine($"Attaching file {attachment.Description} {attachment.Uri.LocalPath}...");
328+
if (testResultId != null)
329+
{
330+
message += $" and test result {testResultId}";
331+
}
303332

304-
await AttachFile(testRunId, parent.Id, attachment.Uri.LocalPath, attachment.Description, cancellationToken);
305-
}
306-
}
333+
message += "...";
334+
335+
Console.WriteLine(message);
336+
}
337+
338+
foreach (AttachmentSet attachmentSet in attachmentSets)
339+
{
340+
if (attachmentSet.Attachments.Count > 0)
341+
{
342+
Console.WriteLine($"Attaching files in set {attachmentSet.DisplayName} {attachmentSet.Uri}...");
343+
}
344+
345+
foreach (UriDataAttachment attachment in attachmentSet.Attachments)
346+
{
347+
Console.WriteLine($"Attaching file {attachment.Description} {attachment.Uri.LocalPath}...");
348+
349+
await AttachFile(testRunId, testResultId, attachment.Uri.LocalPath, attachment.Description, cancellationToken);
307350
}
308351
}
309352
}
@@ -314,29 +357,46 @@ private async Task AttachTextAsFile(int testRunId, int testResultId, string file
314357
await AttachFile(testRunId, testResultId, contentAsBytes, fileName, comment, cancellationToken);
315358
}
316359

317-
private async Task AttachFile(int testRunId, int testResultId, string filePath, string comment, CancellationToken cancellationToken)
360+
private async Task AttachFile(int testRunId, int? testResultId, string filePath, string comment, CancellationToken cancellationToken)
318361
{
319362
byte[] contentAsBytes = File.ReadAllBytes(filePath);
320363
string fileName = Path.GetFileName(filePath);
321364
await AttachFile(testRunId, testResultId, contentAsBytes, fileName, comment, cancellationToken);
322365
}
323366

324-
private async Task AttachFile(int testRunId, int testResultId, byte[] fileContents, string fileName, string comment, CancellationToken cancellationToken)
367+
private async Task AttachFile(int testRunId, int? testResultId, byte[] fileContents, string fileName, string comment, CancellationToken cancellationToken)
325368
{
326-
// https://docs.microsoft.com/en-us/rest/api/azure/devops/test/attachments/create%20test%20result%20attachment
327-
// https://docs.microsoft.com/en-us/azure/devops/integrate/previous-apis/test/attachments?view=tfs-2015#attach-a-file-to-a-test-result
328369
string contentAsBase64 = Convert.ToBase64String(fileContents);
329370

371+
string attachmentType = "GeneralAttachment";
372+
373+
if (fileName.EndsWith(".coverage", StringComparison.OrdinalIgnoreCase))
374+
{
375+
attachmentType = "CodeCoverage";
376+
}
377+
330378
Dictionary<string, object> props = new Dictionary<string, object>
331379
{
332380
{ "stream", contentAsBase64 },
333381
{ "fileName", fileName },
334382
{ "comment", comment },
335-
{ "attachmentType", "GeneralAttachment" }
383+
{ "attachmentType", attachmentType }
336384
};
337385

338386
string requestBody = props.ToJson();
339-
await SendAsync(new HttpMethod("POST"), $"/{testRunId}/results/{testResultId}/attachments", requestBody, cancellationToken, "2.0-preview").ConfigureAwait(false);
387+
388+
if (testResultId == null)
389+
{
390+
// https://docs.microsoft.com/en-us/rest/api/azure/devops/test/attachments/create%20test%20run%20attachment
391+
// https://docs.microsoft.com/en-us/previous-versions/azure/devops/integrate/previous-apis/test/attachments?view=tfs-2015#attach-a-file-to-a-test-run
392+
await SendAsync(new HttpMethod("POST"), $"/{testRunId}/attachments", requestBody, cancellationToken, "2.0-preview").ConfigureAwait(false);
393+
}
394+
else
395+
{
396+
// https://docs.microsoft.com/en-us/rest/api/azure/devops/test/attachments/create%20test%20result%20attachment
397+
// https://docs.microsoft.com/en-us/azure/devops/integrate/previous-apis/test/attachments?view=tfs-2015#attach-a-file-to-a-test-result
398+
await SendAsync(new HttpMethod("POST"), $"/{testRunId}/results/{testResultId}/attachments", requestBody, cancellationToken, "2.0-preview").ConfigureAwait(false);
399+
}
340400
}
341401
}
342402
}

src/AzurePipelines.TestLogger/ApiClientV5.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,9 @@ internal override string GetTestResults(
3737
TestResultParent parent = testCaseTestResults[x.Key];
3838
string subResults = "[ " + string.Join(", ", x.Select(y => GetTestResultProperties(y).ToJson())) + " ]";
3939
string failedOutcome = x.Any(t => t.Outcome == TestOutcome.Failed) ? "\"outcome\": \"Failed\"," : null;
40-
parent.Duration += Convert.ToInt64(x.Sum(t => t.Duration.TotalMilliseconds));
40+
4141
return $@"{{
4242
""id"": {parent.Id},
43-
""durationInMs"": {parent.Duration.ToString(CultureInfo.InvariantCulture)},
4443
""completedDate"": ""{completedDate.ToString(_dateFormatString)}"",
4544
{failedOutcome}
4645
""subResults"": {subResults}
Lines changed: 78 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,38 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
3-
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
4-
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client;
5-
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
6-
7-
namespace AzurePipelines.TestLogger
8-
{
9-
[FriendlyName(AzurePipelinesTestLogger.FriendlyName)]
10-
[ExtensionUri(AzurePipelinesTestLogger.ExtensionUri)]
11-
public class AzurePipelinesTestLogger : ITestLoggerWithParameters
12-
{
13-
/// <summary>
14-
/// Uri used to uniquely identify the logger.
15-
/// </summary>
16-
public const string ExtensionUri = "logger://Microsoft/TestPlatform/AzurePiplinesTestLogger/v1";
17-
18-
/// <summary>
19-
/// Alternate user friendly string to uniquely identify the logger.
20-
/// </summary>
21-
public const string FriendlyName = "AzurePipelines";
22-
23-
private readonly IEnvironmentVariableProvider _environmentVariableProvider;
24-
private readonly IApiClientFactory _apiClientFactory;
25-
private IApiClient _apiClient;
26-
private LoggerQueue _queue;
27-
private bool _groupTestResultsByClassName = true;
28-
3+
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
4+
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client;
5+
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
6+
7+
namespace AzurePipelines.TestLogger
8+
{
9+
[FriendlyName(AzurePipelinesTestLogger.FriendlyName)]
10+
[ExtensionUri(AzurePipelinesTestLogger.ExtensionUri)]
11+
public class AzurePipelinesTestLogger : ITestLoggerWithParameters
12+
{
13+
/// <summary>
14+
/// Uri used to uniquely identify the logger.
15+
/// </summary>
16+
public const string ExtensionUri = "logger://Microsoft/TestPlatform/AzurePiplinesTestLogger/v1";
17+
18+
/// <summary>
19+
/// Alternate user friendly string to uniquely identify the logger.
20+
/// </summary>
21+
public const string FriendlyName = "AzurePipelines";
22+
23+
private readonly IEnvironmentVariableProvider _environmentVariableProvider;
24+
private readonly IApiClientFactory _apiClientFactory;
25+
private IApiClient _apiClient;
26+
private LoggerQueue _queue;
27+
private bool _groupTestResultsByClassName = true;
28+
2929
public AzurePipelinesTestLogger()
3030
{
3131
_environmentVariableProvider = new EnvironmentVariableProvider();
3232
_apiClientFactory = new ApiClientFactory();
33-
}
34-
35-
// Used for testing
33+
}
34+
35+
// Used for testing
3636
internal AzurePipelinesTestLogger(IEnvironmentVariableProvider environmentVariableProvider, IApiClientFactory apiClientFactory)
3737
{
3838
_environmentVariableProvider = environmentVariableProvider;
@@ -42,28 +42,28 @@ internal AzurePipelinesTestLogger(IEnvironmentVariableProvider environmentVariab
4242
public void Initialize(TestLoggerEvents events, string testRunDirectory)
4343
{
4444
Initialize(events, new Dictionary<string, string>());
45-
}
46-
47-
public void Initialize(TestLoggerEvents events, Dictionary<string, string> parameters)
48-
{
45+
}
46+
47+
public void Initialize(TestLoggerEvents events, Dictionary<string, string> parameters)
48+
{
4949
if (events == null)
5050
{
5151
throw new ArgumentNullException(nameof(events));
52-
}
53-
52+
}
53+
5454
if (parameters == null)
5555
{
5656
throw new ArgumentNullException(nameof(parameters));
57-
}
58-
59-
if (!GetRequiredVariable(EnvironmentVariableNames.TeamFoundationCollectionUri, out string collectionUri)
60-
|| !GetRequiredVariable(EnvironmentVariableNames.TeamProject, out string teamProject)
61-
|| !GetRequiredVariable(EnvironmentVariableNames.BuildId, out string buildId)
62-
|| !GetRequiredVariable(EnvironmentVariableNames.BuildRequestedFor, out string buildRequestedFor)
63-
|| !GetRequiredVariable(EnvironmentVariableNames.AgentName, out string agentName)
64-
|| !GetRequiredVariable(EnvironmentVariableNames.AgentJobName, out string jobName))
65-
{
66-
return;
57+
}
58+
59+
if (!GetRequiredVariable(EnvironmentVariableNames.TeamFoundationCollectionUri, out string collectionUri)
60+
|| !GetRequiredVariable(EnvironmentVariableNames.TeamProject, out string teamProject)
61+
|| !GetRequiredVariable(EnvironmentVariableNames.BuildId, out string buildId)
62+
|| !GetRequiredVariable(EnvironmentVariableNames.BuildRequestedFor, out string buildRequestedFor)
63+
|| !GetRequiredVariable(EnvironmentVariableNames.AgentName, out string agentName)
64+
|| !GetRequiredVariable(EnvironmentVariableNames.AgentJobName, out string jobName))
65+
{
66+
return;
6767
}
6868

6969
if (_apiClient == null)
@@ -101,41 +101,42 @@ public void Initialize(TestLoggerEvents events, Dictionary<string, string> param
101101
}
102102

103103
_apiClient.BuildRequestedFor = buildRequestedFor;
104-
}
105-
104+
}
105+
106106
if (parameters.TryGetValue(TestLoggerParameters.GroupTestResultsByClassName, out string groupTestResultsByClassNameString)
107107
&& bool.TryParse(groupTestResultsByClassNameString, out bool groupTestResultsByClassName))
108108
{
109109
_groupTestResultsByClassName = groupTestResultsByClassName;
110110
}
111-
112-
_queue = new LoggerQueue(_apiClient, buildId, agentName, jobName, _groupTestResultsByClassName);
113-
114-
// Register for the events
115-
events.TestRunMessage += TestMessageHandler;
116-
events.TestResult += TestResultHandler;
117-
events.TestRunComplete += TestRunCompleteHandler;
111+
112+
_queue = new LoggerQueue(_apiClient, buildId, agentName, jobName, _groupTestResultsByClassName);
113+
114+
// Register for the events
115+
events.TestRunMessage += TestMessageHandler;
116+
events.TestResult += TestResultHandler;
117+
events.TestRunComplete += TestRunCompleteHandler;
118118
}
119119

120-
private bool GetRequiredVariable(string name, out string value)
121-
{
122-
value = _environmentVariableProvider.GetEnvironmentVariable(name);
123-
if (string.IsNullOrEmpty(value))
124-
{
125-
Console.WriteLine($"AzurePipelines.TestLogger: Not an Azure Pipelines test run, environment variable {name} not set.");
126-
return false;
127-
}
128-
return true;
129-
}
130-
131-
private void TestMessageHandler(object sender, TestRunMessageEventArgs e)
132-
{
133-
// Add code to handle message
134-
}
135-
136-
private void TestResultHandler(object sender, TestResultEventArgs e) =>
137-
_queue.Enqueue(new VstpTestResult(e.Result));
138-
139-
private void TestRunCompleteHandler(object sender, TestRunCompleteEventArgs e) => _queue.Flush();
140-
}
141-
}
120+
private bool GetRequiredVariable(string name, out string value)
121+
{
122+
value = _environmentVariableProvider.GetEnvironmentVariable(name);
123+
if (string.IsNullOrEmpty(value))
124+
{
125+
Console.WriteLine($"AzurePipelines.TestLogger: Not an Azure Pipelines test run, environment variable {name} not set.");
126+
return false;
127+
}
128+
return true;
129+
}
130+
131+
private void TestMessageHandler(object sender, TestRunMessageEventArgs e)
132+
{
133+
// Add code to handle message
134+
}
135+
136+
private void TestResultHandler(object sender, TestResultEventArgs e) =>
137+
_queue.Enqueue(new VstpTestResult(e.Result));
138+
139+
private void TestRunCompleteHandler(object sender, TestRunCompleteEventArgs e) =>
140+
_queue.Flush(new VstpTestRunComplete(e.AttachmentSets));
141+
}
142+
}

src/AzurePipelines.TestLogger/IApiClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ internal interface IApiClient
2222

2323
Task UpdateTestResults(int testRunId, Dictionary<string, TestResultParent> parents, IEnumerable<IGrouping<string, ITestResult>> testResultsByParent, CancellationToken cancellationToken);
2424

25+
Task UpdateTestResults(int runId, VstpTestRunComplete testRunComplete, CancellationToken cancellationToken);
26+
2527
Task<int[]> AddTestCases(int testRunId, string[] testCaseNames, DateTime startedDate, string source, CancellationToken cancellationToken);
2628

2729
Task MarkTestRunCompleted(int testRunId, DateTime startedDate, DateTime completedDate, CancellationToken cancellationToken);

0 commit comments

Comments
 (0)