diff --git a/Directory.Packages.props b/Directory.Packages.props
index 25e873c44f..00779f72ca 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -59,6 +59,7 @@
+
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineOptions.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineOptions.cs
index bfe0b50025..e4723f9b22 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineOptions.cs
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineOptions.cs
@@ -7,4 +7,6 @@ internal static class AzureDevOpsCommandLineOptions
{
public const string AzureDevOpsOptionName = "report-azdo";
public const string AzureDevOpsReportSeverity = "report-azdo-severity";
+ public const string PublishAzureDevOpsRunNameOptionName = "publish-azdo-run-name";
+ public const string PublishAzureDevOpsTestResultsOptionName = "publish-azdo-test-results";
}
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineProvider.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineProvider.cs
index 85598bc581..b1e162ac8b 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineProvider.cs
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineProvider.cs
@@ -27,6 +27,8 @@ public IReadOnlyCollection GetCommandLineOptions()
[
new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsOptionName, AzureDevOpsResources.OptionDescription, ArgumentArity.Zero, false),
new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity, AzureDevOpsResources.SeverityOptionDescription, ArgumentArity.ExactlyOne, false),
+ new CommandLineOption(AzureDevOpsCommandLineOptions.PublishAzureDevOpsRunNameOptionName, AzureDevOpsResources.PublishAzdoRunNameOptionDescription, ArgumentArity.ExactlyOne, false),
+ new CommandLineOption(AzureDevOpsCommandLineOptions.PublishAzureDevOpsTestResultsOptionName, AzureDevOpsResources.PublishAzdoTestResultsOptionDescription, ArgumentArity.Zero, false),
];
public Task ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments)
@@ -51,6 +53,14 @@ public Task ValidateCommandLineOptionsAsync(ICommandLineOption
return ValidationResult.InvalidTask(AzureDevOpsResources.AzureDevOpsReportSeverityRequiresAzureDevOps);
}
- return ValidationResult.ValidTask;
+ if (!commandLineOptions.IsOptionSet(AzureDevOpsCommandLineOptions.PublishAzureDevOpsRunNameOptionName))
+ {
+ return ValidationResult.ValidTask;
+ }
+
+ bool isPublishResultsEnabled = commandLineOptions.IsOptionSet(AzureDevOpsCommandLineOptions.PublishAzureDevOpsTestResultsOptionName);
+ return isPublishResultsEnabled
+ ? ValidationResult.ValidTask
+ : ValidationResult.InvalidTask(AzureDevOpsResources.PublishAzdoRunNameRequiresPublishAzdoTestResults);
}
}
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs
index 728a257520..6b77aeadf3 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs
@@ -15,13 +15,13 @@ namespace Microsoft.Testing.Extensions;
public static class AzureDevOpsExtensions
{
///
- /// Adds support to the test application builder.
+ /// Adds support to the test application builder.
///
/// The test application builder.
public static void AddAzureDevOpsProvider(this ITestApplicationBuilder builder)
{
var compositeTestSessionAzDoService =
- new CompositeExtensionFactory(serviceProvider =>
+ new CompositeExtensionFactory(serviceProvider =>
new AzureDevOpsReporter(
serviceProvider.GetCommandLineOptions(),
serviceProvider.GetEnvironment(),
@@ -29,7 +29,23 @@ public static void AddAzureDevOpsProvider(this ITestApplicationBuilder builder)
serviceProvider.GetOutputDevice(),
serviceProvider.GetLoggerFactory()));
+ var compositeTestResultsPublisher =
+ new CompositeExtensionFactory(serviceProvider =>
+ new AzureDevOpsTestResultsPublisher(
+ serviceProvider.GetCommandLineOptions(),
+ serviceProvider.GetConfiguration(),
+ serviceProvider.GetEnvironment(),
+ serviceProvider.GetFileSystem(),
+ serviceProvider.GetTestApplicationModuleInfo(),
+ serviceProvider.GetTestApplicationProcessExitCode(),
+ new AzureDevOpsTestResultsClient(serviceProvider.GetTask(), serviceProvider.GetClock()),
+ serviceProvider.GetTask(),
+ serviceProvider.GetClock(),
+ serviceProvider.GetLoggerFactory()));
+
builder.TestHost.AddDataConsumer(compositeTestSessionAzDoService);
+ builder.TestHost.AddDataConsumer(compositeTestResultsPublisher);
+ builder.TestHost.AddTestSessionLifetimeHandler(compositeTestResultsPublisher);
builder.CommandLine.AddProvider(() => new AzureDevOpsCommandLineProvider());
}
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsLivePublishingModels.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsLivePublishingModels.cs
new file mode 100644
index 0000000000..b939985146
--- /dev/null
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsLivePublishingModels.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text.Json.Serialization;
+
+namespace Microsoft.Testing.Extensions.AzureDevOpsReport;
+
+internal static class AzureDevOpsLivePublishingConstants
+{
+ public const string AbortedTestOutcome = "Aborted";
+ public const string AbortedTestRunState = "Aborted";
+ public const string CompletedTestRunState = "Completed";
+ public const string FailedTestOutcome = "Failed";
+ public const string InProgressTestRunState = "InProgress";
+ public const int MaxRunNameLength = 256;
+ public const string NotExecutedTestOutcome = "NotExecuted";
+ public const string PassedTestOutcome = "Passed";
+}
+
+internal sealed record AzureDevOpsPublishConfiguration(
+ string CollectionUri,
+ string Project,
+ string AccessToken,
+ int BuildId,
+ string RunName,
+ string AutomatedTestStorage,
+ string ResultsDirectory);
+
+internal sealed record AzureDevOpsTestCaseResult(
+ [property: JsonPropertyName("automatedTestName")] string AutomatedTestName,
+ [property: JsonPropertyName("automatedTestStorage")] string AutomatedTestStorage,
+ [property: JsonPropertyName("testCaseTitle")] string TestCaseTitle,
+ [property: JsonPropertyName("outcome")] string Outcome,
+ [property: JsonPropertyName("durationInMs")] long? DurationInMs,
+ [property: JsonPropertyName("errorMessage")] string? ErrorMessage,
+ [property: JsonPropertyName("stackTrace")] string? StackTrace,
+ [property: JsonPropertyName("startedDate")] DateTimeOffset? StartedDate,
+ [property: JsonPropertyName("completedDate")] DateTimeOffset? CompletedDate);
+
+internal sealed record AzureDevOpsTestResultsPublisherOptions(
+ int BatchSize,
+ TimeSpan FlushInterval,
+ int CoordinationReadRetryCount,
+ TimeSpan CoordinationReadRetryDelay,
+ TimeSpan CoordinationFinalizeTimeout,
+ TimeSpan CoordinationFileExpiration,
+ TimeSpan CoordinationJoinerMaxWaitTime)
+{
+ public AzureDevOpsTestResultsPublisherOptions(int batchSize, TimeSpan flushInterval, int coordinationReadRetryCount, TimeSpan coordinationReadRetryDelay)
+ : this(batchSize, flushInterval, coordinationReadRetryCount, coordinationReadRetryDelay, TimeSpan.FromSeconds(30), TimeSpan.FromHours(4), TimeSpan.FromMinutes(2))
+ {
+ }
+
+ public AzureDevOpsTestResultsPublisherOptions(int batchSize, TimeSpan flushInterval, int coordinationReadRetryCount, TimeSpan coordinationReadRetryDelay, TimeSpan coordinationFinalizeTimeout, TimeSpan coordinationFileExpiration)
+ : this(batchSize, flushInterval, coordinationReadRetryCount, coordinationReadRetryDelay, coordinationFinalizeTimeout, coordinationFileExpiration, TimeSpan.FromMinutes(2))
+ {
+ }
+
+ public static AzureDevOpsTestResultsPublisherOptions Default { get; } = new(100, TimeSpan.FromSeconds(5), 40, TimeSpan.FromMilliseconds(250), TimeSpan.FromSeconds(30), TimeSpan.FromHours(4), TimeSpan.FromMinutes(2));
+}
+
+internal enum LeaseFileStatus
+{
+ /// The lease file is not present on disk.
+ NotFound,
+
+ /// The lease file was parsed and the lease is still valid.
+ Active,
+
+ /// The lease file was parsed and the lease has expired.
+ Expired,
+
+ /// The lease file is present but could not be read or parsed; it may be mid-write by another process.
+ TransientReadError,
+}
+
+internal readonly record struct LeaseReadResult(LeaseFileStatus Status, AzureDevOpsLeaseFile? Lease);
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsRunIdCoordinator.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsRunIdCoordinator.cs
new file mode 100644
index 0000000000..79233269d1
--- /dev/null
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsRunIdCoordinator.cs
@@ -0,0 +1,473 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+using Microsoft.Testing.Extensions.AzureDevOpsReport.Resources;
+using Microsoft.Testing.Platform.Helpers;
+using Microsoft.Testing.Platform.Logging;
+
+namespace Microsoft.Testing.Extensions.AzureDevOpsReport;
+
+internal sealed class AzureDevOpsRunIdCoordinator
+{
+ private const string CoordinationFilePrefix = "azdo-runid";
+ private static readonly JsonSerializerOptions JsonSerializerOptions = new()
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ private static readonly UTF8Encoding Utf8EncodingWithoutBom = new(encoderShouldEmitUTF8Identifier: false);
+
+ private readonly IFileSystem _fileSystem;
+ private readonly ITask _task;
+ private readonly IClock _clock;
+ private readonly IEnvironment _environment;
+ private readonly ILogger _logger;
+ private readonly AzureDevOpsTestResultsPublisherOptions _options;
+
+ public AzureDevOpsRunIdCoordinator(IFileSystem fileSystem, ITask task, IClock clock, IEnvironment environment, ILogger logger, AzureDevOpsTestResultsPublisherOptions options)
+ {
+ _fileSystem = fileSystem;
+ _task = task;
+ _clock = clock;
+ _environment = environment;
+ _logger = logger;
+ _options = options;
+ }
+
+ public async Task AcquireRunAsync(AzureDevOpsPublishConfiguration configuration, Func> createRunAsync, CancellationToken cancellationToken)
+ {
+ _fileSystem.CreateDirectory(configuration.ResultsDirectory);
+
+ string runIdFilePath = Path.Combine(configuration.ResultsDirectory, GetRunIdFileName(configuration.BuildId));
+ string ownerFilePath = Path.Combine(configuration.ResultsDirectory, GetOwnerFileName(configuration.BuildId));
+ string participantFilePath = Path.Combine(configuration.ResultsDirectory, GetParticipantFileName(configuration.BuildId, _environment.ProcessId));
+ bool ownsOwnerFile = false;
+
+ try
+ {
+ await WriteParticipantLeaseAsync(participantFilePath, configuration.BuildId, cancellationToken).ConfigureAwait(false);
+
+ ownsOwnerFile = await TryAcquireOwnerAsync(ownerFilePath, configuration.BuildId, cancellationToken).ConfigureAwait(false);
+ if (ownsOwnerFile)
+ {
+ int runId = await createRunAsync(cancellationToken).ConfigureAwait(false);
+ await WriteRunIdFileAsync(runIdFilePath, configuration, runId, cancellationToken).ConfigureAwait(false);
+ return new AzureDevOpsCoordinatedRun(runId, true, configuration.BuildId, configuration.ResultsDirectory, runIdFilePath, ownerFilePath, participantFilePath);
+ }
+
+ AzureDevOpsRunIdFile? runIdFile = await WaitForRunIdFileAsync(runIdFilePath, ownerFilePath, configuration.BuildId, cancellationToken).ConfigureAwait(false);
+ if (runIdFile is null)
+ {
+ ownsOwnerFile = await TryAcquireOwnerAsync(ownerFilePath, configuration.BuildId, cancellationToken).ConfigureAwait(false);
+ if (ownsOwnerFile)
+ {
+ int runId = await createRunAsync(cancellationToken).ConfigureAwait(false);
+ await WriteRunIdFileAsync(runIdFilePath, configuration, runId, cancellationToken).ConfigureAwait(false);
+ return new AzureDevOpsCoordinatedRun(runId, true, configuration.BuildId, configuration.ResultsDirectory, runIdFilePath, ownerFilePath, participantFilePath);
+ }
+
+ runIdFile = await WaitForRunIdFileAsync(runIdFilePath, ownerFilePath, configuration.BuildId, cancellationToken).ConfigureAwait(false);
+ }
+
+ if (runIdFile is not null
+ && string.Equals(runIdFile.CollectionUri, configuration.CollectionUri, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(runIdFile.Project, configuration.Project, StringComparison.Ordinal))
+ {
+ return new AzureDevOpsCoordinatedRun(runIdFile.RunId, false, configuration.BuildId, configuration.ResultsDirectory, runIdFilePath, ownerFilePath, participantFilePath);
+ }
+
+ if (runIdFile is not null)
+ {
+ throw new InvalidOperationException(AzureDevOpsResources.AzureDevOpsLivePublishingRunIdFileMismatch);
+ }
+
+ // TODO: Elect a deterministic surviving participant when the owner lease expires before writing azdo-runid..json.
+ throw new InvalidOperationException(AzureDevOpsResources.AzureDevOpsLivePublishingMissingRunIdFile);
+ }
+ catch
+ {
+ TryDeleteFile(participantFilePath);
+ if (ownsOwnerFile)
+ {
+ TryDeleteFile(runIdFilePath);
+ TryDeleteFile(ownerFilePath);
+ }
+
+ throw;
+ }
+ }
+
+ public async Task RenewLeaseAsync(AzureDevOpsCoordinatedRun coordinatedRun, CancellationToken cancellationToken)
+ {
+ await WriteLeaseFileAsync(coordinatedRun.ParticipantFilePath, CreateLeaseFile(coordinatedRun.BuildId), overwrite: true, cancellationToken).ConfigureAwait(false);
+
+ if (coordinatedRun.IsOwner)
+ {
+ await WriteLeaseFileAsync(coordinatedRun.OwnerFilePath, CreateLeaseFile(coordinatedRun.BuildId), overwrite: true, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ public async Task FinalizeRunAsync(AzureDevOpsCoordinatedRun coordinatedRun, Func finalizeRunAsync, CancellationToken cancellationToken)
+ {
+ TryDeleteFile(coordinatedRun.ParticipantFilePath);
+
+ if (!coordinatedRun.IsOwner)
+ {
+ return;
+ }
+
+ DateTimeOffset timeoutAt = _clock.UtcNow + _options.CoordinationFinalizeTimeout;
+
+ while (true)
+ {
+ string[] participantFiles = _fileSystem.GetFiles(coordinatedRun.ResultsDirectory, GetParticipantSearchPattern(coordinatedRun.BuildId), SearchOption.TopDirectoryOnly);
+ participantFiles = CleanupStaleParticipants(participantFiles);
+
+ if (participantFiles.Length == 0)
+ {
+ break;
+ }
+
+ if (_clock.UtcNow >= timeoutAt)
+ {
+ _logger.LogWarning(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.AzureDevOpsLivePublishingFinalizeWaitTimedOut, _options.CoordinationFinalizeTimeout, participantFiles.Length));
+ break;
+ }
+
+ await _task.Delay(_options.CoordinationReadRetryDelay, cancellationToken).ConfigureAwait(false);
+ }
+
+ try
+ {
+ await finalizeRunAsync(cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ TryDeleteFile(coordinatedRun.OwnerFilePath);
+ TryDeleteFile(coordinatedRun.RunIdFilePath);
+ }
+ }
+
+ private string[] CleanupStaleParticipants(string[] participantFiles)
+ {
+ var activeParticipants = new List();
+
+ foreach (string participantFile in participantFiles)
+ {
+ LeaseReadResult result = ReadLease(participantFile);
+
+ // Treat a participant whose file we couldn't read (e.g. mid-write) as active to avoid
+ // racing with a process that is still updating its lease.
+ if (result.Status is LeaseFileStatus.TransientReadError or LeaseFileStatus.Active)
+ {
+ AzureDevOpsLeaseFile? lease = result.Lease;
+ if (lease is null || (lease.ExpiresAt > _clock.UtcNow && IsProcessAlive(lease.ProcessId)))
+ {
+ activeParticipants.Add(participantFile);
+ continue;
+ }
+ }
+ else if (result.Status == LeaseFileStatus.Expired
+ && result.Lease is { } expiredLease
+ && IsProcessAlive(expiredLease.ProcessId))
+ {
+ // Lease has expired according to wall-clock but the participant process is still
+ // running (its renewal may simply be stuck). Keep waiting rather than deleting.
+ activeParticipants.Add(participantFile);
+ continue;
+ }
+ else if (result.Status == LeaseFileStatus.NotFound)
+ {
+ continue;
+ }
+ else if (TryGetPid(participantFile) is int processId && IsProcessAlive(processId))
+ {
+ activeParticipants.Add(participantFile);
+ continue;
+ }
+
+ TryDeleteFile(participantFile);
+ }
+
+ return [.. activeParticipants];
+ }
+
+ private static bool IsProcessAlive(int processId)
+ {
+ try
+ {
+ using var process = Process.GetProcessById(processId);
+ return !process.HasExited;
+ }
+ catch (ArgumentException)
+ {
+ return false;
+ }
+ catch (InvalidOperationException)
+ {
+ return false;
+ }
+ }
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ [UnconditionalSuppressMessage("Aot", "IL3050", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ private async Task WaitForRunIdFileAsync(string runIdFilePath, string ownerFilePath, int buildId, CancellationToken cancellationToken)
+ {
+ DateTimeOffset joinerDeadline = _clock.UtcNow + _options.CoordinationJoinerMaxWaitTime;
+
+ for (int attempt = 0; ; attempt++)
+ {
+ if (_fileSystem.ExistFile(runIdFilePath))
+ {
+ try
+ {
+ string content = await _fileSystem.ReadAllTextAsync(runIdFilePath).ConfigureAwait(false);
+ AzureDevOpsRunIdFile? runIdFile = JsonSerializer.Deserialize(content, JsonSerializerOptions);
+ if (runIdFile is not null)
+ {
+ if (runIdFile.ExpiresAt <= _clock.UtcNow || runIdFile.BuildId != buildId)
+ {
+ TryDeleteFile(runIdFilePath);
+ }
+ else
+ {
+ return runIdFile;
+ }
+ }
+ }
+ catch (IOException)
+ {
+ }
+ catch (UnauthorizedAccessException)
+ {
+ }
+ catch (JsonException)
+ {
+ }
+ }
+
+ // After the base retry budget is exhausted, only keep waiting as long as the owner lease
+ // still looks active (or temporarily unreadable). A long CreateTestRunAsync can outlast
+ // CoordinationReadRetryCount * CoordinationReadRetryDelay, but we still want to bound the
+ // wait by CoordinationJoinerMaxWaitTime so a crashed owner doesn't stall joiners forever.
+ if (attempt >= _options.CoordinationReadRetryCount)
+ {
+ if (_clock.UtcNow >= joinerDeadline)
+ {
+ return null;
+ }
+
+ LeaseReadResult ownerLease = ReadLease(ownerFilePath);
+ bool ownerStillActive = ownerLease.Status is LeaseFileStatus.Active or LeaseFileStatus.TransientReadError
+ || (ownerLease.Status == LeaseFileStatus.Expired
+ && ownerLease.Lease is { } expiredLease
+ && IsProcessAlive(expiredLease.ProcessId));
+
+ if (!ownerStillActive)
+ {
+ return null;
+ }
+ }
+
+ await _task.Delay(_options.CoordinationReadRetryDelay, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ [UnconditionalSuppressMessage("Aot", "IL3050", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ private async Task WriteParticipantLeaseAsync(string participantFilePath, int buildId, CancellationToken cancellationToken)
+ {
+ AzureDevOpsLeaseFile lease = CreateLeaseFile(buildId);
+ if (await TryWriteLeaseFileAsync(participantFilePath, lease, overwrite: false, cancellationToken).ConfigureAwait(false))
+ {
+ return;
+ }
+
+ await WriteLeaseFileAsync(participantFilePath, lease, overwrite: true, cancellationToken).ConfigureAwait(false);
+ }
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ [UnconditionalSuppressMessage("Aot", "IL3050", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ private async Task TryAcquireOwnerAsync(string ownerFilePath, int buildId, CancellationToken cancellationToken)
+ {
+ AzureDevOpsLeaseFile lease = CreateLeaseFile(buildId);
+ if (await TryWriteLeaseFileAsync(ownerFilePath, lease, overwrite: false, cancellationToken).ConfigureAwait(false))
+ {
+ return true;
+ }
+
+ LeaseReadResult existing = ReadLease(ownerFilePath);
+
+ // If the file exists but we couldn't read it (likely partial write from the current owner),
+ // refuse to take over — otherwise we'd race and create a duplicate Azure DevOps run.
+ if (existing.Status == LeaseFileStatus.TransientReadError)
+ {
+ return false;
+ }
+
+ if (existing.Status is LeaseFileStatus.NotFound or LeaseFileStatus.Expired)
+ {
+ TryDeleteFile(ownerFilePath);
+ return await TryWriteLeaseFileAsync(ownerFilePath, lease, overwrite: false, cancellationToken).ConfigureAwait(false);
+ }
+
+ return false;
+ }
+
+ private AzureDevOpsLeaseFile CreateLeaseFile(int buildId)
+ => new(_environment.ProcessId, buildId, _clock.UtcNow + _options.CoordinationFileExpiration);
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ [UnconditionalSuppressMessage("Aot", "IL3050", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ private LeaseReadResult ReadLease(string path)
+ {
+ if (!_fileSystem.ExistFile(path))
+ {
+ return new LeaseReadResult(LeaseFileStatus.NotFound, null);
+ }
+
+ try
+ {
+ string content = _fileSystem.ReadAllText(path);
+ AzureDevOpsLeaseFile? lease = JsonSerializer.Deserialize(content, JsonSerializerOptions);
+ if (lease is not null)
+ {
+ return new LeaseReadResult(
+ lease.ExpiresAt > _clock.UtcNow ? LeaseFileStatus.Active : LeaseFileStatus.Expired,
+ lease);
+ }
+
+ if (int.TryParse(content, NumberStyles.Integer, CultureInfo.InvariantCulture, out int processId))
+ {
+ // Legacy plain-PID lease format: treat as expired so the caller can take over.
+ return new LeaseReadResult(LeaseFileStatus.Expired, new AzureDevOpsLeaseFile(processId, 0, DateTimeOffset.MinValue));
+ }
+
+ // The file exists but neither parser yielded a usable value. It might be mid-write —
+ // surface this as a transient read error so the caller doesn't race the writer.
+ return new LeaseReadResult(LeaseFileStatus.TransientReadError, null);
+ }
+ catch (IOException)
+ {
+ }
+ catch (UnauthorizedAccessException)
+ {
+ }
+ catch (JsonException)
+ {
+ }
+
+ return new LeaseReadResult(LeaseFileStatus.TransientReadError, null);
+ }
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ [UnconditionalSuppressMessage("Aot", "IL3050", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ private async Task WriteRunIdFileAsync(string runIdFilePath, AzureDevOpsPublishConfiguration configuration, int runId, CancellationToken cancellationToken)
+ => await WriteJsonFileAsync(runIdFilePath, new AzureDevOpsRunIdFile(runId, configuration.BuildId, configuration.CollectionUri, configuration.Project, _clock.UtcNow + _options.CoordinationFileExpiration), overwrite: true, cancellationToken).ConfigureAwait(false);
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ [UnconditionalSuppressMessage("Aot", "IL3050", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ private Task WriteLeaseFileAsync(string path, AzureDevOpsLeaseFile payload, bool overwrite, CancellationToken cancellationToken)
+ => WriteJsonFileAsync(path, payload, overwrite, cancellationToken);
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ [UnconditionalSuppressMessage("Aot", "IL3050", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ private async Task TryWriteLeaseFileAsync(string path, AzureDevOpsLeaseFile payload, bool overwrite, CancellationToken cancellationToken)
+ {
+ try
+ {
+ await WriteLeaseFileAsync(path, payload, overwrite, cancellationToken).ConfigureAwait(false);
+ return true;
+ }
+ catch (IOException)
+ {
+ return false;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return false;
+ }
+ }
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ [UnconditionalSuppressMessage("Aot", "IL3050", Justification = "The coordination payload type is internal, fixed, and controlled by this extension.")]
+ private async Task WriteJsonFileAsync(string path, TPayload payload, bool overwrite, CancellationToken cancellationToken)
+ {
+ string json = JsonSerializer.Serialize(payload, JsonSerializerOptions);
+ using IFileStream stream = _fileSystem.NewFileStream(path, overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write, FileShare.Read);
+ using StreamWriter writer = new(stream.Stream, Utf8EncodingWithoutBom, 1024, leaveOpen: true);
+#if NET
+ await writer.WriteAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false);
+ await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
+#else
+ await writer.WriteAsync(json).ConfigureAwait(false);
+ await writer.FlushAsync().ConfigureAwait(false);
+#endif
+ }
+
+ private void TryDeleteFile(string path)
+ {
+ try
+ {
+ if (_fileSystem.ExistFile(path))
+ {
+ _fileSystem.DeleteFile(path);
+ }
+ }
+ catch (IOException ex)
+ {
+ _logger.LogWarning($"{AzureDevOpsResources.AzureDevOpsLivePublishingFailedToDeleteCoordinationFile} {path}: {ex.Message}");
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ _logger.LogWarning($"{AzureDevOpsResources.AzureDevOpsLivePublishingFailedToDeleteCoordinationFile} {path}: {ex.Message}");
+ }
+ }
+
+ private static int? TryGetPid(string participantFile)
+ {
+ string fileName = Path.GetFileNameWithoutExtension(participantFile);
+ int lastSeparator = fileName.LastIndexOf('.');
+ return lastSeparator < 0
+ ? null
+ : int.TryParse(fileName[(lastSeparator + 1)..], NumberStyles.Integer, CultureInfo.InvariantCulture, out int processId)
+ ? processId
+ : null;
+ }
+
+ private static string GetOwnerFileName(int buildId)
+ => $"{CoordinationFilePrefix}.{buildId}.owner";
+
+ private static string GetParticipantFileName(int buildId, int processId)
+ => $"{CoordinationFilePrefix}.{buildId}.participant.{processId}.json";
+
+ private static string GetParticipantSearchPattern(int buildId)
+ => $"{CoordinationFilePrefix}.{buildId}.participant.*.json";
+
+ private static string GetRunIdFileName(int buildId)
+ => $"{CoordinationFilePrefix}.{buildId}.json";
+}
+
+internal sealed record AzureDevOpsCoordinatedRun(
+ int RunId,
+ bool IsOwner,
+ int BuildId,
+ string ResultsDirectory,
+ string RunIdFilePath,
+ string OwnerFilePath,
+ string ParticipantFilePath);
+
+internal sealed record AzureDevOpsRunIdFile(
+ [property: JsonPropertyName("runId")] int RunId,
+ [property: JsonPropertyName("buildId")] int BuildId,
+ [property: JsonPropertyName("collectionUri")] string CollectionUri,
+ [property: JsonPropertyName("project")] string Project,
+ [property: JsonPropertyName("expiresAt")] DateTimeOffset ExpiresAt);
+
+internal sealed record AzureDevOpsLeaseFile(
+ [property: JsonPropertyName("processId")] int ProcessId,
+ [property: JsonPropertyName("buildId")] int BuildId,
+ [property: JsonPropertyName("expiresAt")] DateTimeOffset ExpiresAt);
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsClient.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsClient.cs
new file mode 100644
index 0000000000..32b6e55a98
--- /dev/null
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsTestResultsClient.cs
@@ -0,0 +1,286 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Net;
+using System.Net.Http.Headers;
+using System.Net.Sockets;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+using Microsoft.Testing.Extensions.AzureDevOpsReport.Resources;
+using Microsoft.Testing.Platform.Helpers;
+
+namespace Microsoft.Testing.Extensions.AzureDevOpsReport;
+
+internal sealed class AzureDevOpsTestResultsClient : IAzureDevOpsTestResultsClient
+{
+ private const string ApiVersion = "7.1";
+ private const int MaxAttempts = 3;
+ private const int BaseDelayMilliseconds = 500;
+ private static readonly TimeSpan AttemptTimeout = TimeSpan.FromSeconds(15);
+ private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(30);
+ private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(60);
+ private static readonly HttpMethod PatchMethod = new("PATCH");
+ private static readonly JsonSerializerOptions JsonSerializerOptions = new()
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ private static readonly HttpClient SharedHttpClient = CreateHttpClient();
+
+ private readonly HttpClient _httpClient;
+ private readonly ITask _task;
+ private readonly IClock _clock;
+
+ public AzureDevOpsTestResultsClient(ITask task, IClock clock)
+ : this(SharedHttpClient, task, clock)
+ {
+ }
+
+ internal AzureDevOpsTestResultsClient(HttpClient httpClient, ITask task, IClock clock)
+ {
+ _httpClient = httpClient;
+ _task = task;
+ _clock = clock;
+ }
+
+ public async Task CreateTestRunAsync(AzureDevOpsPublishConfiguration configuration, CancellationToken cancellationToken)
+ {
+ using HttpRequestMessage request = CreateRequest(
+ HttpMethod.Post,
+ BuildRunsUri(configuration.CollectionUri, configuration.Project),
+ configuration.AccessToken,
+ new CreateTestRunRequest(configuration.RunName, true, new BuildReference(configuration.BuildId), AzureDevOpsLivePublishingConstants.InProgressTestRunState));
+
+ CreateTestRunResponse response = await SendAsync(request, cancellationToken).ConfigureAwait(false);
+ return response.Id > 0
+ ? response.Id
+ : throw new InvalidOperationException(AzureDevOpsResources.AzureDevOpsLivePublishingInvalidResponse);
+ }
+
+ public Task PublishTestResultsAsync(AzureDevOpsPublishConfiguration configuration, int runId, IReadOnlyList results, CancellationToken cancellationToken)
+ {
+ using HttpRequestMessage request = CreateRequest(
+ HttpMethod.Post,
+ BuildResultsUri(configuration.CollectionUri, configuration.Project, runId),
+ configuration.AccessToken,
+ results);
+
+ return SendAsync(request, cancellationToken);
+ }
+
+ public Task UpdateTestRunStateAsync(AzureDevOpsPublishConfiguration configuration, int runId, string state, CancellationToken cancellationToken)
+ {
+ using HttpRequestMessage request = CreateRequest(
+ PatchMethod,
+ BuildRunUri(configuration.CollectionUri, configuration.Project, runId),
+ configuration.AccessToken,
+ new UpdateTestRunStateRequest(state));
+
+ return SendAsync(request, cancellationToken);
+ }
+
+ private static HttpClient CreateHttpClient()
+ {
+ HttpClientHandler handler = new()
+ {
+ AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
+ };
+
+ return new HttpClient(handler, disposeHandler: false)
+ {
+ Timeout = HttpClientTimeout,
+ };
+ }
+
+ private static Uri BuildRunsUri(string collectionUri, string project)
+ => new(new Uri(collectionUri, UriKind.Absolute), $"{Uri.EscapeDataString(project)}/_apis/test/runs?api-version={ApiVersion}");
+
+ private static Uri BuildRunUri(string collectionUri, string project, int runId)
+ => new(new Uri(collectionUri, UriKind.Absolute), $"{Uri.EscapeDataString(project)}/_apis/test/runs/{runId}?api-version={ApiVersion}");
+
+ private static Uri BuildResultsUri(string collectionUri, string project, int runId)
+ => new(new Uri(collectionUri, UriKind.Absolute), $"{Uri.EscapeDataString(project)}/_apis/test/runs/{runId}/results?api-version={ApiVersion}");
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "Payload types are internal, fixed, and controlled by this extension.")]
+ [UnconditionalSuppressMessage("Aot", "IL3050", Justification = "Payload types are internal, fixed, and controlled by this extension.")]
+ private static HttpRequestMessage CreateRequest(HttpMethod method, Uri uri, string accessToken, TPayload payload)
+ {
+ HttpRequestMessage request = CreateRequest(method, uri, accessToken);
+ request.Content = new StringContent(JsonSerializer.Serialize(payload, JsonSerializerOptions), Encoding.UTF8, "application/json");
+ return request;
+ }
+
+ private static HttpRequestMessage CreateRequest(HttpMethod method, Uri uri, string accessToken)
+ {
+ HttpRequestMessage request = new(method, uri);
+ request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($":{accessToken}")));
+ request.Headers.Accept.ParseAdd($"application/json; api-version={ApiVersion}");
+ return request;
+ }
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "Response types are internal, fixed, and controlled by this extension.")]
+ [UnconditionalSuppressMessage("Aot", "IL3050", Justification = "Response types are internal, fixed, and controlled by this extension.")]
+ private async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ using var requestTimeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ requestTimeoutSource.CancelAfter(RequestTimeout);
+
+ using HttpResponseMessage response = await SendCoreAsync(request, requestTimeoutSource.Token, cancellationToken).ConfigureAwait(false);
+ string payload = await ReadAsStringAsync(response.Content, requestTimeoutSource.Token).ConfigureAwait(false);
+ TResponse? deserialized = JsonSerializer.Deserialize(payload, JsonSerializerOptions);
+ return deserialized ?? throw new InvalidOperationException(AzureDevOpsResources.AzureDevOpsLivePublishingInvalidResponse);
+ }
+
+ private async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ using var requestTimeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ requestTimeoutSource.CancelAfter(RequestTimeout);
+ using HttpResponseMessage ignoredResponse = await SendCoreAsync(request, requestTimeoutSource.Token, cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task SendCoreAsync(HttpRequestMessage request, CancellationToken requestCancellationToken, CancellationToken userCancellationToken)
+ {
+ Exception? lastException = null;
+
+ try
+ {
+ for (int attempt = 1; attempt <= MaxAttempts; attempt++)
+ {
+ using HttpRequestMessage currentRequest = await CloneAsync(request, requestCancellationToken).ConfigureAwait(false);
+ using var attemptTimeoutSource = CancellationTokenSource.CreateLinkedTokenSource(requestCancellationToken);
+ attemptTimeoutSource.CancelAfter(AttemptTimeout);
+
+ try
+ {
+ HttpResponseMessage response = await _httpClient.SendAsync(currentRequest, attemptTimeoutSource.Token).ConfigureAwait(false);
+ if (response.IsSuccessStatusCode)
+ {
+ return response;
+ }
+
+ if (!ShouldRetry(response.StatusCode, attempt))
+ {
+ string responseBody = await ReadAsStringAsync(response.Content, requestCancellationToken).ConfigureAwait(false);
+ response.Dispose();
+ throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.AzureDevOpsLivePublishingHttpError, (int)response.StatusCode, responseBody));
+ }
+
+ TimeSpan delay = GetDelay(response, attempt);
+ response.Dispose();
+ await _task.Delay(delay, requestCancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex) when (ShouldRetry(ex, userCancellationToken, requestCancellationToken, attempt))
+ {
+ lastException = ex;
+ await _task.Delay(GetExponentialBackoffDelay(attempt), requestCancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ catch (OperationCanceledException) when (!userCancellationToken.IsCancellationRequested)
+ {
+ // An internal timeout (per-attempt or request-level) fired on the final retry attempt.
+ // Convert to a non-cancellation exception so publishing failures never propagate as
+ // OperationCanceledException and fault the data consumer.
+ throw new InvalidOperationException(AzureDevOpsResources.AzureDevOpsLivePublishingRequestFailed, lastException);
+ }
+
+ throw new InvalidOperationException(AzureDevOpsResources.AzureDevOpsLivePublishingRequestFailed, lastException);
+ }
+
+ private static bool ShouldRetry(HttpStatusCode statusCode, int attempt)
+ => attempt < MaxAttempts && ((int)statusCode is >= 500 or 429);
+
+ private static bool ShouldRetry(Exception exception, CancellationToken userCancellationToken, CancellationToken requestCancellationToken, int attempt)
+ => attempt < MaxAttempts
+ && !userCancellationToken.IsCancellationRequested
+ && !requestCancellationToken.IsCancellationRequested
+ && exception is HttpRequestException or IOException or SocketException or TaskCanceledException;
+
+ private TimeSpan GetDelay(HttpResponseMessage response, int attempt)
+ {
+ if (response.StatusCode == (HttpStatusCode)429 && response.Headers.RetryAfter is { } retryAfter)
+ {
+ if (retryAfter.Delta is { } delta && delta > TimeSpan.Zero)
+ {
+ return delta;
+ }
+
+ if (retryAfter.Date is { } date)
+ {
+ TimeSpan delay = date - _clock.UtcNow;
+ if (delay > TimeSpan.Zero)
+ {
+ return delay;
+ }
+ }
+ }
+
+ return GetExponentialBackoffDelay(attempt);
+ }
+
+ private static TimeSpan GetExponentialBackoffDelay(int attempt)
+ => TimeSpan.FromMilliseconds(BaseDelayMilliseconds * Math.Pow(2, attempt - 1));
+
+ private static async Task CloneAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ HttpRequestMessage clone = new(request.Method, request.RequestUri)
+ {
+ Version = request.Version,
+#if NET
+ VersionPolicy = request.VersionPolicy,
+#endif
+ };
+
+#if NET
+ foreach (KeyValuePair option in request.Options)
+ {
+ clone.Options.Set(new HttpRequestOptionsKey
+
+
+
+
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resx b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resx
index 516d5d4382..d3b641204f 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resx
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resx
@@ -119,9 +119,10 @@
'--report-azdo-severity' requires '--report-azdo' to be enabled
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.Azure DevOps report generator
@@ -130,10 +131,62 @@
Invalid option {0}.
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.Severity to use for the reported event. Options are: error (default) and warning.{Locked="error"}{Locked="warning"}
+
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+
+
+ Azure DevOps live publishing failed to complete the test run.
+
+
+ Azure DevOps live publishing failed to create the test run.
+
+
+ Azure DevOps live publishing failed to delete coordination file
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+
+
+ Azure DevOps REST API request failed after all retries.
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+
+
+ Timeout: {0}
+
\ No newline at end of file
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlf
index 076a3037b9..8709f804cf 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- --report-azdo-severity vyžaduje, aby byla povolena možnost --report-azdo
-
+ --report-azdo-severity vyžaduje, aby byla povolena možnost --report-azdo
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Generátor sestav Azure DevOps pro zápis chyb do výstupu způsobem, který je pro AzureDev Ops srozumitelný.
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Generátor sestav Azure DevOps pro zápis chyb do výstupu způsobem, který je pro AzureDev Ops srozumitelný.
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Umožňuje generátoru sestav Azure DevOps zapisovat chyby do výstupu způsobem, který je pro AzureDev Ops srozumitelný.
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Umožňuje generátoru sestav Azure DevOps zapisovat chyby do výstupu způsobem, který je pro AzureDev Ops srozumitelný.
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlf
index 87be6a6ce5..c2e53aaf2f 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- Für „--report-azdo-severity“ muss „--report-azdo“ aktiviert sein.
-
+ Für „--report-azdo-severity“ muss „--report-azdo“ aktiviert sein.
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Azure DevOps-Berichts-Generator, um Fehler auf eine Weise in die Ausgabe zu schreiben, die AzureDev Ops versteht.
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Azure DevOps-Berichts-Generator, um Fehler auf eine Weise in die Ausgabe zu schreiben, die AzureDev Ops versteht.
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Azure DevOps-Berichts-Generator aktivieren, um Fehler auf eine Weise in die Ausgabe zu schreiben, die AzureDev Ops versteht.
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Azure DevOps-Berichts-Generator aktivieren, um Fehler auf eine Weise in die Ausgabe zu schreiben, die AzureDev Ops versteht.
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlf
index c4a61c952f..4be488b60a 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- "--report-azdo-severity" requiere que se habilite "--report-azdo"
-
+ "--report-azdo-severity" requiere que se habilite "--report-azdo"
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- El generador de informes de Azure DevOps escribe errores en la salida de una manera que AzureDev Ops comprende.
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ El generador de informes de Azure DevOps escribe errores en la salida de una manera que AzureDev Ops comprende.
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Habilite el generador de informes de Azure DevOps para escribir errores en la salida de una manera que AzureDev Ops comprenda.
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Habilite el generador de informes de Azure DevOps para escribir errores en la salida de una manera que AzureDev Ops comprenda.
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlf
index 4feb6d0ab7..9358deebd1 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- « --report-azdo-severity » nécessite l’activation de « --report-azdo »
-
+ « --report-azdo-severity » nécessite l’activation de « --report-azdo »
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Générateur de rapports Azure DevOps pour écrire les erreurs dans la sortie d’une manière comprise par Azure DevOps.
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Générateur de rapports Azure DevOps pour écrire les erreurs dans la sortie d’une manière comprise par Azure DevOps.
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Activez le générateur de rapports Azure DevOps pour écrire les erreurs dans la sortie d’une manière comprise par Azure DevOps.
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Activez le générateur de rapports Azure DevOps pour écrire les erreurs dans la sortie d’une manière comprise par Azure DevOps.
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlf
index d9756f675c..5ffeb707cb 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- '--report-azdo-severity' richiede l'autenticazione di '--report-azdo'
-
+ '--report-azdo-severity' richiede l'autenticazione di '--report-azdo'
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Il generatore di report di Azure DevOps per scrivere gli errori nell'output in un modo che Azure DevOps comprenda.
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Il generatore di report di Azure DevOps per scrivere gli errori nell'output in un modo che Azure DevOps comprenda.
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Abilitare il generatore di report di Azure DevOps per scrivere gli errori nell'output in un modo che Azure DevOps comprenda.
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Abilitare il generatore di report di Azure DevOps per scrivere gli errori nell'output in un modo che Azure DevOps comprenda.
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlf
index ea3529945b..a58d714021 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- '--report-azdo-severity' では、'--report-azdo' を有効にする必要があります
-
+ '--report-azdo-severity' では、'--report-azdo' を有効にする必要があります
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- AzureDev Ops が理解する方法で出力にエラーを書き込む Azure DevOps レポート生成プログラム。
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ AzureDev Ops が理解する方法で出力にエラーを書き込む Azure DevOps レポート生成プログラム。
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Azure DevOps レポート生成プログラムを有効にして、AzureDev Ops が理解する方法で出力にエラーを書き込みます。
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Azure DevOps レポート生成プログラムを有効にして、AzureDev Ops が理解する方法で出力にエラーを書き込みます。
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlf
index f74caa7f6e..10189551eb 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- '--report-azdo-severity'를 사용하려면 '--report-azdo'를 사용 설정 해야합니다.
-
+ '--report-azdo-severity'를 사용하려면 '--report-azdo'를 사용 설정 해야합니다.
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Azure DevOps가 이해할 수 있는 방식으로 출력에 오류를 기록하는 Azure DevOps 보고서 생성기입니다.
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Azure DevOps가 이해할 수 있는 방식으로 출력에 오류를 기록하는 Azure DevOps 보고서 생성기입니다.
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Azure DevOps 보고서 생성기를 활성화하여 Azure DevOps가 이해할 수 있는 방식으로 출력에 오류를 기록합니다.
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Azure DevOps 보고서 생성기를 활성화하여 Azure DevOps가 이해할 수 있는 방식으로 출력에 오류를 기록합니다.
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlf
index 8890f27f70..90c76a7bdd 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- Element „--report-azdo-severity” wymaga włączenia polecenia „--report-azdo”
-
+ Element „--report-azdo-severity” wymaga włączenia polecenia „--report-azdo”
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Generator raportów usługi Azure DevOps umożliwiający zapisywanie błędów w danych wyjściowych w sposób zrozumiały dla usługi Azure DevOps.
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Generator raportów usługi Azure DevOps umożliwiający zapisywanie błędów w danych wyjściowych w sposób zrozumiały dla usługi Azure DevOps.
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Włącz generator raportów usługi Azure DevOps, aby zapisywać błędy w danych wyjściowych w sposób zrozumiały dla usługi Azure DevOps.
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Włącz generator raportów usługi Azure DevOps, aby zapisywać błędy w danych wyjściowych w sposób zrozumiały dla usługi Azure DevOps.
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlf
index d660d0e56a..f8f258ad16 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- '--report-azdo-severity' requer que '--report-azdo' esteja habilitado
-
+ '--report-azdo-severity' requer que '--report-azdo' esteja habilitado
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Gerador de relatórios do Azure DevOps para gravar erros na saída de uma forma que o Azure DevOps entenda.
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Gerador de relatórios do Azure DevOps para gravar erros na saída de uma forma que o Azure DevOps entenda.
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Habilitar o gerador de relatórios do Azure DevOps para gravar erros na saída de uma forma que o Azure DevOps entenda.
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Habilitar o gerador de relatórios do Azure DevOps para gravar erros na saída de uma forma que o Azure DevOps entenda.
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlf
index 756d7c9cae..0180172ff3 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- "--report-azdo-severity" требует включения "--report-azdo"
-
+ "--report-azdo-severity" требует включения "--report-azdo"
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Генератор отчетов Azure DevOps для записи ошибок в выходные данные в формате, который понимает Azure DevOps.
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Генератор отчетов Azure DevOps для записи ошибок в выходные данные в формате, который понимает Azure DevOps.
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Включите генератор отчетов Azure DevOps для записи ошибок в выходные данные в формате, который понимает Azure DevOps.
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Включите генератор отчетов Azure DevOps для записи ошибок в выходные данные в формате, который понимает Azure DevOps.
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlf
index 69207860a2..19d7f729e2 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- '--report-azdo-severity' öğesi '--report-azdo' öğesinin etkinleştirilmesini gerektirir
-
+ '--report-azdo-severity' öğesi '--report-azdo' öğesinin etkinleştirilmesini gerektirir
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Hataları Azure DevOps'un anlayacağı şekilde çıktıya yazan Azure DevOps rapor oluşturucusu.
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Hataları Azure DevOps'un anlayacağı şekilde çıktıya yazan Azure DevOps rapor oluşturucusu.
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Azure DevOps rapor oluşturucusunu, hataları Azure DevOps'un anlayacağı şekilde çıktıya yazması için etkinleştirin.
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Azure DevOps rapor oluşturucusunu, hataları Azure DevOps'un anlayacağı şekilde çıktıya yazması için etkinleştirin.
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlf
index 2b5199f4ad..81685b8009 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- '--report-azdo-severity' 要求启用 '--report-azdo'
-
+ '--report-azdo-severity' 要求启用 '--report-azdo'
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Azure DevOps报表生成器,可通过 Azure DevOps 可理解的方式将错误写入输出。
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Azure DevOps报表生成器,可通过 Azure DevOps 可理解的方式将错误写入输出。
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- 启用 Azure DevOps 报表生成器,以通过 Azure DevOps 可理解的方式将错误写入输出。
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ 启用 Azure DevOps 报表生成器,以通过 Azure DevOps 可理解的方式将错误写入输出。
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlf
index 6c61f0fb8f..f724b78177 100644
--- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlf
+++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlf
@@ -2,14 +2,79 @@
+
+ Azure DevOps live publishing failed to complete the test run.
+ Azure DevOps live publishing failed to complete the test run.
+
+
+
+ Azure DevOps live publishing failed to create the test run.
+ Azure DevOps live publishing failed to create the test run.
+
+
+
+ Azure DevOps live publishing failed to delete coordination file
+ Azure DevOps live publishing failed to delete coordination file
+
+
+
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+ Azure DevOps live publishing timed out after {0} waiting for {1} participant file(s); finalizing the run anyway.
+
+
+
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+ Azure DevOps REST API request failed with status code {0}. Response: {1}
+
+
+
+ Azure DevOps REST API returned an unexpected response payload.
+ Azure DevOps REST API returned an unexpected response payload.
+
+
+
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ '--publish-azdo-test-results' was set, but Azure DevOps live publishing is disabled because the following environment variables are missing or invalid: {0}.
+ {Locked='--publish-azdo-test-results'}
+
+
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ Azure DevOps live publishing could not locate azdo-runid.<buildId>.json for the shared run.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Azure DevOps live publishing failed to publish test results.
+ Azure DevOps live publishing failed to publish test results.
+
+
+
+ Azure DevOps REST API request failed after all retries.
+ Azure DevOps REST API request failed after all retries.
+
+
+
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ Azure DevOps live publishing found an azdo-runid.<buildId>.json file for a different Azure DevOps project.
+ {Locked="azdo-runid.<buildId>.json"}
+
+
+ Timeout
+ Timeout
+
+
+
+ Timeout: {0}
+ Timeout: {0}
+
+ '--report-azdo-severity' requires '--report-azdo' to be enabled
- '--report-azdo-severity' 需要啟用 '--report-azdo'
-
+ '--report-azdo-severity' 需要啟用 '--report-azdo'
+ {Locked='--report-azdo-severity'}{Locked='--report-azdo'}
- Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- Azure DevOps 報告產生器以 Azure DevOps 可理解的方式將錯誤寫入輸出。
+ Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Azure DevOps 報告產生器以 Azure DevOps 可理解的方式將錯誤寫入輸出。
@@ -23,8 +88,23 @@
- Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands.
- 啟用 Azure DevOps 報告產生器,以 Azure DevOps 可理解的方式將錯誤寫入輸出。
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ 啟用 Azure DevOps 報告產生器,以 Azure DevOps 可理解的方式將錯誤寫入輸出。
+
+
+
+ Custom Azure DevOps test run name for live test-result publishing.
+ Custom Azure DevOps test run name for live test-result publishing.
+
+
+
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ '--publish-azdo-run-name' requires '--publish-azdo-test-results' to be enabled
+ {Locked='--publish-azdo-run-name'}{Locked='--publish-azdo-test-results'}
+
+
+ Publish test results live to the Azure DevOps Tests tab.
+ Publish test results live to the Azure DevOps Tests tab.
diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs
index d989315655..6fb95254fa 100644
--- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs
+++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs
@@ -103,6 +103,14 @@ Disable reporting progress to screen.
--output
Output verbosity when reporting tests.
Valid values are 'Normal', 'Detailed'. Default is 'Normal'.
+ --publish-azdo-run-name
+ Custom Azure DevOps test run name for live test-result publishing.
+ --publish-azdo-test-results
+ Publish test results live to the Azure DevOps Tests tab.
+ --report-azdo
+ Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ --report-azdo-severity
+ Severity to use for the reported event. Options are: error (default) and warning.
--report-trx
Enable generating TRX report
--report-trx-filename
@@ -354,6 +362,27 @@ Default type is 'Full'
Arity: 1
Hidden: False
Description: Disable retry mechanism if the number of failed tests is greater than the specified value
+ AzureDevOpsCommandLineProvider
+ Name: Azure DevOps report generator
+ Version: *
+ Description: Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ Options:
+ --publish-azdo-run-name
+ Arity: 1
+ Hidden: False
+ Description: Custom Azure DevOps test run name for live test-result publishing.
+ --publish-azdo-test-results
+ Arity: 0
+ Hidden: False
+ Description: Publish test results live to the Azure DevOps Tests tab.
+ --report-azdo
+ Arity: 0
+ Hidden: False
+ Description: Enable Azure DevOps report generator to write errors to the output in a way that Azure DevOps understands.
+ --report-azdo-severity
+ Arity: 1
+ Hidden: False
+ Description: Severity to use for the reported event. Options are: error (default) and warning.
TerminalTestReporterCommandLineOptionsProvider
Name: Terminal test reporter
Version: *
diff --git a/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsLivePublishingTests.cs b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsLivePublishingTests.cs
new file mode 100644
index 0000000000..12d222510d
--- /dev/null
+++ b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsLivePublishingTests.cs
@@ -0,0 +1,740 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json;
+
+using Microsoft.Testing.Extensions.AzureDevOpsReport;
+using Microsoft.Testing.Extensions.AzureDevOpsReport.Resources;
+using Microsoft.Testing.Extensions.Reporting;
+using Microsoft.Testing.Platform.CommandLine;
+using Microsoft.Testing.Platform.Configurations;
+using Microsoft.Testing.Platform.Extensions;
+using Microsoft.Testing.Platform.Extensions.Messages;
+using Microsoft.Testing.Platform.Helpers;
+using Microsoft.Testing.Platform.Logging;
+using Microsoft.Testing.Platform.Services;
+using Microsoft.Testing.Platform.TestHost;
+
+using Moq;
+
+namespace Microsoft.Testing.Extensions.UnitTests;
+
+[TestClass]
+public sealed class AzureDevOpsLivePublishingTests
+{
+ private readonly List _directoriesToDelete = [];
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ foreach (string path in _directoriesToDelete)
+ {
+ TryDeleteDirectory(path);
+ }
+ }
+
+ [TestMethod]
+ public async Task OnTestSessionStartingAsync_CreatesRunAndStoresRunId()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ AzureDevOpsTestResultsPublisher publisher = CreatePublisher(directory.Path, options: new(2, TimeSpan.FromMinutes(1), 4, TimeSpan.FromMilliseconds(1)), out FakeAzureDevOpsTestResultsClient client, out _, out _);
+ client.CreateTestRunAsyncFunc = (_, _) => Task.FromResult(42);
+
+ await StartPublisherAsync(publisher);
+
+ Assert.AreEqual(42, publisher.RunId);
+ }
+
+ [TestMethod]
+ public async Task OnTestSessionStartingAsync_JsonExceptionLogsWarningAndDoesNotThrow()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ AzureDevOpsTestResultsPublisher publisher = CreatePublisher(directory.Path, options: new(2, TimeSpan.FromMinutes(1), 4, TimeSpan.FromMilliseconds(1)), out FakeAzureDevOpsTestResultsClient client, out _, out CollectingLogger logger);
+ client.CreateTestRunAsyncFunc = (_, _) => Task.FromException(new JsonException("broken payload"));
+
+ Assert.IsTrue(await publisher.IsEnabledAsync());
+ await publisher.OnTestSessionStartingAsync(new Microsoft.Testing.Platform.Services.TestSessionContext(CancellationToken.None));
+
+ Assert.IsNull(publisher.RunId);
+ Assert.Contains(AzureDevOpsResources.AzureDevOpsLivePublishingCreateRunFailed, string.Join(Environment.NewLine, logger.Logs));
+ }
+
+ [TestMethod]
+ public async Task ConsumeAsync_FlushesWhenBatchSizeIsReached()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ AzureDevOpsTestResultsPublisher publisher = CreatePublisher(directory.Path, options: new(2, TimeSpan.FromMinutes(1), 4, TimeSpan.FromMilliseconds(1)), out FakeAzureDevOpsTestResultsClient client, out FakeClock clock, out _);
+ client.CreateTestRunAsyncFunc = (_, _) => Task.FromResult(100);
+
+ List> publishedBatches = [];
+ client.PublishTestResultsAsyncFunc = (_, _, results, _) =>
+ {
+ publishedBatches.Add(results.ToArray());
+ return Task.CompletedTask;
+ };
+
+ await StartPublisherAsync(publisher);
+ await publisher.ConsumeAsync(Mock.Of(), CreateMessage(CreateNode("test-1", new PassedTestNodeStateProperty(), clock.UtcNow)), CancellationToken.None);
+ await publisher.ConsumeAsync(Mock.Of(), CreateMessage(CreateNode("test-2", new PassedTestNodeStateProperty(), clock.UtcNow)), CancellationToken.None);
+
+ Assert.HasCount(1, publishedBatches);
+ Assert.HasCount(2, publishedBatches[0]);
+ }
+
+ [TestMethod]
+ public async Task ConsumeAsync_FlushesWhenFlushIntervalElapsed()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ AzureDevOpsTestResultsPublisher publisher = CreatePublisher(directory.Path, options: new(10, TimeSpan.FromSeconds(5), 4, TimeSpan.FromMilliseconds(1)), out FakeAzureDevOpsTestResultsClient client, out FakeClock clock, out _);
+ client.CreateTestRunAsyncFunc = (_, _) => Task.FromResult(101);
+
+ List> publishedBatches = [];
+ client.PublishTestResultsAsyncFunc = (_, _, results, _) =>
+ {
+ publishedBatches.Add(results.ToArray());
+ return Task.CompletedTask;
+ };
+
+ await StartPublisherAsync(publisher);
+ await publisher.ConsumeAsync(Mock.Of(), CreateMessage(CreateNode("test-1", new PassedTestNodeStateProperty(), clock.UtcNow)), CancellationToken.None);
+
+ clock.UtcNow += TimeSpan.FromSeconds(6);
+ await publisher.ConsumeAsync(Mock.Of(), CreateMessage(CreateNode("test-2", new PassedTestNodeStateProperty(), clock.UtcNow)), CancellationToken.None);
+
+ Assert.HasCount(1, publishedBatches);
+ Assert.HasCount(2, publishedBatches[0]);
+ }
+
+ [TestMethod]
+ public async Task OnTestSessionFinishingAsync_FlushesRemainingResults()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ AzureDevOpsTestResultsPublisher publisher = CreatePublisher(directory.Path, options: new(10, TimeSpan.FromMinutes(1), 4, TimeSpan.FromMilliseconds(1)), out FakeAzureDevOpsTestResultsClient client, out FakeClock clock, out _);
+ client.CreateTestRunAsyncFunc = (_, _) => Task.FromResult(102);
+
+ List> publishedBatches = [];
+ client.PublishTestResultsAsyncFunc = (_, _, results, _) =>
+ {
+ publishedBatches.Add(results.ToArray());
+ return Task.CompletedTask;
+ };
+
+ await StartPublisherAsync(publisher);
+ await publisher.ConsumeAsync(Mock.Of(), CreateMessage(CreateNode("test-1", new PassedTestNodeStateProperty(), clock.UtcNow)), CancellationToken.None);
+ await publisher.OnTestSessionFinishingAsync(new Microsoft.Testing.Platform.Services.TestSessionContext(CancellationToken.None));
+
+ Assert.HasCount(1, publishedBatches);
+ Assert.HasCount(1, publishedBatches[0]);
+ Assert.HasCount(1, client.UpdateTestRunStateCalls);
+ Assert.AreEqual(AzureDevOpsLivePublishingConstants.CompletedTestRunState, client.UpdateTestRunStateCalls[0].State);
+ }
+
+ [TestMethod]
+ public async Task OnTestSessionFinishingAsync_FailingTestsExitCode_FinalizesAsCompleted()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ Mock processExitCode = new();
+ processExitCode.Setup(x => x.GetProcessExitCode()).Returns(2); // ExitCode.AtLeastOneTestFailed
+ processExitCode.SetupGet(x => x.HasTestAdapterTestSessionFailure).Returns(false);
+ AzureDevOpsTestResultsPublisher publisher = CreatePublisher(directory.Path, options: new(10, TimeSpan.FromMinutes(1), 4, TimeSpan.FromMilliseconds(1)), out FakeAzureDevOpsTestResultsClient client, out _, out _, processExitCode: processExitCode);
+ client.CreateTestRunAsyncFunc = (_, _) => Task.FromResult(110);
+
+ await StartPublisherAsync(publisher);
+ await publisher.OnTestSessionFinishingAsync(new Microsoft.Testing.Platform.Services.TestSessionContext(CancellationToken.None));
+
+ Assert.HasCount(1, client.UpdateTestRunStateCalls);
+ Assert.AreEqual(AzureDevOpsLivePublishingConstants.CompletedTestRunState, client.UpdateTestRunStateCalls[0].State);
+ }
+
+ [TestMethod]
+ public async Task OnTestSessionFinishingAsync_SessionAbortedExitCode_FinalizesAsAborted()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ Mock processExitCode = new();
+ processExitCode.Setup(x => x.GetProcessExitCode()).Returns(3); // ExitCode.TestSessionAborted
+ processExitCode.SetupGet(x => x.HasTestAdapterTestSessionFailure).Returns(false);
+ AzureDevOpsTestResultsPublisher publisher = CreatePublisher(directory.Path, options: new(10, TimeSpan.FromMinutes(1), 4, TimeSpan.FromMilliseconds(1)), out FakeAzureDevOpsTestResultsClient client, out _, out _, processExitCode: processExitCode);
+ client.CreateTestRunAsyncFunc = (_, _) => Task.FromResult(111);
+
+ await StartPublisherAsync(publisher);
+ await publisher.OnTestSessionFinishingAsync(new Microsoft.Testing.Platform.Services.TestSessionContext(CancellationToken.None));
+
+ Assert.HasCount(1, client.UpdateTestRunStateCalls);
+ Assert.AreEqual(AzureDevOpsLivePublishingConstants.AbortedTestRunState, client.UpdateTestRunStateCalls[0].State);
+ }
+
+ [TestMethod]
+ public async Task OnTestSessionFinishingAsync_SessionCanceled_FinalizesAsAborted()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ AzureDevOpsTestResultsPublisher publisher = CreatePublisher(directory.Path, options: new(10, TimeSpan.FromMinutes(1), 4, TimeSpan.FromMilliseconds(1)), out FakeAzureDevOpsTestResultsClient client, out _, out _);
+ client.CreateTestRunAsyncFunc = (_, _) => Task.FromResult(112);
+
+ await StartPublisherAsync(publisher);
+ await publisher.OnTestSessionFinishingAsync(new Microsoft.Testing.Platform.Services.TestSessionContext(new CancellationToken(canceled: true)));
+
+ Assert.HasCount(1, client.UpdateTestRunStateCalls);
+ Assert.AreEqual(AzureDevOpsLivePublishingConstants.AbortedTestRunState, client.UpdateTestRunStateCalls[0].State);
+ }
+
+ [TestMethod]
+ public async Task OnTestSessionFinishingAsync_TestAdapterFailure_FinalizesAsAborted()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ Mock processExitCode = new();
+ processExitCode.Setup(x => x.GetProcessExitCode()).Returns(10); // ExitCode.TestAdapterTestSessionFailure
+ processExitCode.SetupGet(x => x.HasTestAdapterTestSessionFailure).Returns(true);
+ AzureDevOpsTestResultsPublisher publisher = CreatePublisher(directory.Path, options: new(10, TimeSpan.FromMinutes(1), 4, TimeSpan.FromMilliseconds(1)), out FakeAzureDevOpsTestResultsClient client, out _, out _, processExitCode: processExitCode);
+ client.CreateTestRunAsyncFunc = (_, _) => Task.FromResult(113);
+
+ await StartPublisherAsync(publisher);
+ await publisher.OnTestSessionFinishingAsync(new Microsoft.Testing.Platform.Services.TestSessionContext(CancellationToken.None));
+
+ Assert.HasCount(1, client.UpdateTestRunStateCalls);
+ Assert.AreEqual(AzureDevOpsLivePublishingConstants.AbortedTestRunState, client.UpdateTestRunStateCalls[0].State);
+ }
+
+ [TestMethod]
+ public void CreateTestCaseResult_MapsMtpStatesToAzdoResults()
+ {
+ DateTimeOffset startTime = new(2025, 1, 1, 12, 0, 0, TimeSpan.Zero);
+ TimingProperty timing = new(new TimingInfo(startTime, startTime.AddSeconds(2), TimeSpan.FromSeconds(2)));
+ AzureDevOpsTestCaseResult? passed = AzureDevOpsTestResultsPublisher.CreateTestCaseResult(CreateNode("passed", new PassedTestNodeStateProperty(), startTime, timing), "tests.dll");
+ AzureDevOpsTestCaseResult? failed = AzureDevOpsTestResultsPublisher.CreateTestCaseResult(CreateNode("failed", new FailedTestNodeStateProperty(new InvalidOperationException("boom")), startTime, timing), "tests.dll");
+ AzureDevOpsTestCaseResult? skipped = AzureDevOpsTestResultsPublisher.CreateTestCaseResult(CreateNode("skipped", new SkippedTestNodeStateProperty("skip"), startTime, timing), "tests.dll");
+ AzureDevOpsTestCaseResult? timeout = AzureDevOpsTestResultsPublisher.CreateTestCaseResult(CreateNode("timeout", new TimeoutTestNodeStateProperty("too slow"), startTime, timing), "tests.dll");
+#pragma warning disable CS0618, MTP0001 // Type or member is obsolete
+ AzureDevOpsTestCaseResult? cancelled = AzureDevOpsTestResultsPublisher.CreateTestCaseResult(CreateNode("cancelled", new CancelledTestNodeStateProperty("stopped"), startTime, timing), "tests.dll");
+#pragma warning restore CS0618, MTP0001 // Type or member is obsolete
+
+ Assert.AreEqual(AzureDevOpsLivePublishingConstants.PassedTestOutcome, passed?.Outcome);
+ Assert.AreEqual(2000L, passed?.DurationInMs);
+ Assert.AreEqual(startTime, passed?.StartedDate);
+ Assert.AreEqual(AzureDevOpsLivePublishingConstants.FailedTestOutcome, failed?.Outcome);
+ Assert.AreEqual("boom", failed?.ErrorMessage);
+ Assert.AreEqual(AzureDevOpsLivePublishingConstants.NotExecutedTestOutcome, skipped?.Outcome);
+ Assert.AreEqual("skip", skipped?.ErrorMessage);
+ Assert.AreEqual(AzureDevOpsLivePublishingConstants.FailedTestOutcome, timeout?.Outcome);
+ Assert.AreEqual("Timeout: too slow", timeout?.ErrorMessage);
+ Assert.AreEqual(AzureDevOpsLivePublishingConstants.AbortedTestOutcome, cancelled?.Outcome);
+ Assert.AreEqual("stopped", cancelled?.ErrorMessage);
+ }
+
+ [TestMethod]
+ public async Task CreatePublisher_UsesSanitizedRunNameAndStorageWithoutExtension()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ Mock environment = CreateEnvironmentMock(processId: GetAliveProcessId(), stageName: new string('s', 240) + "/stage", jobName: "job\r\nline\u0001");
+ AzureDevOpsPublishConfiguration? capturedConfiguration = null;
+ AzureDevOpsTestResultsPublisher publisher = CreatePublisher(directory.Path, options: new(2, TimeSpan.FromMinutes(1), 4, TimeSpan.FromMilliseconds(1)), out FakeAzureDevOpsTestResultsClient client, out _, out _, environment: environment);
+ client.CreateTestRunAsyncFunc = (configuration, _) =>
+ {
+ capturedConfiguration = configuration;
+ return Task.FromResult(55);
+ };
+
+ await StartPublisherAsync(publisher);
+
+ Assert.IsNotNull(capturedConfiguration);
+ AzureDevOpsPublishConfiguration configuration = capturedConfiguration!;
+ Assert.AreEqual("MyTests", configuration.AutomatedTestStorage);
+ Assert.DoesNotContain("/", configuration.RunName);
+ Assert.DoesNotContain("\r", configuration.RunName);
+ Assert.DoesNotContain("\n", configuration.RunName);
+ Assert.IsLessThanOrEqualTo(AzureDevOpsLivePublishingConstants.MaxRunNameLength, configuration.RunName.Length);
+ }
+
+ [TestMethod]
+ public async Task AzureDevOpsTestResultsClient_HonorsRetryAfterOn429()
+ {
+ var events = new List();
+ FakeTask task = new(delayCallback: timeSpan => events.Add($"delay:{timeSpan.TotalSeconds}"));
+ FakeClock clock = new() { UtcNow = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero) };
+ QueueHttpMessageHandler handler = new(
+ (request, cancellationToken) =>
+ {
+ events.Add("send:1");
+ HttpResponseMessage response = new((HttpStatusCode)429)
+ {
+ Content = new StringContent("{}"),
+ };
+ response.Headers.RetryAfter = new System.Net.Http.Headers.RetryConditionHeaderValue(TimeSpan.FromSeconds(3));
+ return Task.FromResult(response);
+ },
+ (request, cancellationToken) =>
+ {
+ events.Add("send:2");
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("{\"id\":7}"),
+ });
+ });
+ using HttpClient httpClient = new(handler)
+ {
+ Timeout = Timeout.InfiniteTimeSpan,
+ };
+ AzureDevOpsTestResultsClient client = new(httpClient, task, clock);
+
+ int runId = await client.CreateTestRunAsync(new AzureDevOpsPublishConfiguration("https://dev.azure.com/org/", "project", "token", 1, "run", "tests.dll", "results"), CancellationToken.None);
+
+ Assert.AreEqual(7, runId);
+ Assert.HasCount(1, task.DelayCalls);
+ Assert.AreEqual(TimeSpan.FromSeconds(3), task.DelayCalls[0]);
+ CollectionAssert.AreEqual(new[] { "send:1", "delay:3", "send:2" }, events);
+ }
+
+ [TestMethod]
+ public async Task AzureDevOpsTestResultsClient_RetriesTaskCanceledException()
+ {
+ FakeTask task = new();
+ FakeClock clock = new() { UtcNow = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero) };
+ QueueHttpMessageHandler handler = new(
+ (request, cancellationToken) => Task.FromException(new TaskCanceledException("timeout")),
+ (request, cancellationToken) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("{\"id\":8}"),
+ }));
+ using HttpClient httpClient = new(handler)
+ {
+ Timeout = Timeout.InfiniteTimeSpan,
+ };
+ AzureDevOpsTestResultsClient client = new(httpClient, task, clock);
+
+ int runId = await client.CreateTestRunAsync(new AzureDevOpsPublishConfiguration("https://dev.azure.com/org/", "project", "token", 1, "run", "tests.dll", "results"), CancellationToken.None);
+
+ Assert.AreEqual(8, runId);
+ Assert.HasCount(1, task.DelayCalls);
+ Assert.AreEqual(TimeSpan.FromMilliseconds(500), task.DelayCalls[0]);
+ }
+
+ [TestMethod]
+ public async Task ConsumeAsync_PublishFailureLogsWarningAndDoesNotThrow()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ AzureDevOpsTestResultsPublisher publisher = CreatePublisher(directory.Path, options: new(1, TimeSpan.FromMinutes(1), 4, TimeSpan.FromMilliseconds(1)), out FakeAzureDevOpsTestResultsClient client, out FakeClock clock, out CollectingLogger logger);
+ client.CreateTestRunAsyncFunc = (_, _) => Task.FromResult(103);
+ client.PublishTestResultsAsyncFunc = (_, _, _, _) => Task.FromException(new JsonException("publish failed"));
+
+ await StartPublisherAsync(publisher);
+ await publisher.ConsumeAsync(Mock.Of(), CreateMessage(CreateNode("test-1", new PassedTestNodeStateProperty(), clock.UtcNow)), CancellationToken.None);
+ await publisher.OnTestSessionFinishingAsync(new Microsoft.Testing.Platform.Services.TestSessionContext(CancellationToken.None));
+
+ Assert.Contains(AzureDevOpsResources.AzureDevOpsLivePublishingPublishResultsFailed, string.Join(Environment.NewLine, logger.Logs));
+ }
+
+ [TestMethod]
+ public async Task RunIdCoordinator_CreateAndReadFlowSharesRunIdAcrossProcesses()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ FakeClock clock = new() { UtcNow = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero) };
+ CollectingLogger logger = new();
+ AzureDevOpsTestResultsPublisherOptions options = new(10, TimeSpan.FromSeconds(5), 10, TimeSpan.FromMilliseconds(1));
+ Mock ownerEnvironment = CreateEnvironmentMock(processId: GetAliveProcessId());
+ Mock joinerEnvironment = CreateEnvironmentMock(processId: int.MaxValue);
+ SystemFileSystem fileSystem = new();
+ AzureDevOpsRunIdCoordinator ownerCoordinator = new(fileSystem, new SystemTask(), clock, ownerEnvironment.Object, logger, options);
+ AzureDevOpsRunIdCoordinator joinerCoordinator = new(fileSystem, new SystemTask(), clock, joinerEnvironment.Object, logger, options);
+ AzureDevOpsPublishConfiguration configuration = new("https://dev.azure.com/org/", "project", "token", 123, "run", "tests.dll", directory.Path);
+
+ AzureDevOpsCoordinatedRun ownerRun = await ownerCoordinator.AcquireRunAsync(configuration, _ => Task.FromResult(88), CancellationToken.None);
+ AzureDevOpsCoordinatedRun joinerRun = await joinerCoordinator.AcquireRunAsync(configuration, _ => Task.FromResult(99), CancellationToken.None);
+
+ Assert.AreEqual(88, ownerRun.RunId);
+ Assert.IsTrue(ownerRun.IsOwner);
+ Assert.AreEqual(88, joinerRun.RunId);
+ Assert.IsFalse(joinerRun.IsOwner);
+ Assert.IsTrue(File.Exists(Path.Combine(directory.Path, "azdo-runid.123.json")));
+ }
+
+ [TestMethod]
+ public async Task RunIdCoordinator_AcquireRunAsync_ReplacesExpiredOwnerAndRunIdFiles()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ FakeClock clock = new() { UtcNow = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero) };
+ CollectingLogger logger = new();
+ AzureDevOpsTestResultsPublisherOptions options = new(10, TimeSpan.FromSeconds(5), 2, TimeSpan.FromMilliseconds(1));
+ Mock environment = CreateEnvironmentMock(processId: GetAliveProcessId());
+ SystemFileSystem fileSystem = new();
+ AzureDevOpsRunIdCoordinator coordinator = new(fileSystem, new FakeTask(), clock, environment.Object, logger, options);
+ AzureDevOpsPublishConfiguration configuration = new("https://dev.azure.com/org/", "project", "token", 123, "run", "tests.dll", directory.Path);
+
+ File.WriteAllText(Path.Combine(directory.Path, "azdo-runid.123.owner"), JsonSerializer.Serialize(new AzureDevOpsLeaseFile(int.MaxValue, 123, clock.UtcNow.AddMinutes(-1))));
+ File.WriteAllText(Path.Combine(directory.Path, "azdo-runid.123.json"), JsonSerializer.Serialize(new AzureDevOpsRunIdFile(7, 123, configuration.CollectionUri, configuration.Project, clock.UtcNow.AddMinutes(-1))));
+
+ AzureDevOpsCoordinatedRun coordinatedRun = await coordinator.AcquireRunAsync(configuration, _ => Task.FromResult(88), CancellationToken.None);
+
+ Assert.AreEqual(88, coordinatedRun.RunId);
+ Assert.IsTrue(coordinatedRun.IsOwner);
+ Assert.IsTrue(File.Exists(Path.Combine(directory.Path, "azdo-runid.123.json")));
+ }
+
+ [TestMethod]
+ public async Task RunIdCoordinator_AcquireRunAsync_OverwritesExistingParticipantFile()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ FakeClock clock = new() { UtcNow = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero) };
+ CollectingLogger logger = new();
+ AzureDevOpsTestResultsPublisherOptions options = new(10, TimeSpan.FromSeconds(5), 2, TimeSpan.FromMilliseconds(1));
+ int joinerProcessId = GetAliveProcessId();
+ Mock joinerEnvironment = CreateEnvironmentMock(processId: joinerProcessId);
+ SystemFileSystem fileSystem = new();
+ AzureDevOpsRunIdCoordinator joinerCoordinator = new(fileSystem, new FakeTask(), clock, joinerEnvironment.Object, logger, options);
+ AzureDevOpsPublishConfiguration configuration = new("https://dev.azure.com/org/", "project", "token", 123, "run", "tests.dll", directory.Path);
+
+ File.WriteAllText(Path.Combine(directory.Path, "azdo-runid.123.owner"), JsonSerializer.Serialize(new AzureDevOpsLeaseFile(int.MaxValue, 123, clock.UtcNow.AddHours(1))));
+ File.WriteAllText(Path.Combine(directory.Path, "azdo-runid.123.json"), JsonSerializer.Serialize(new AzureDevOpsRunIdFile(91, 123, configuration.CollectionUri, configuration.Project, clock.UtcNow.AddHours(1))));
+ File.WriteAllText(Path.Combine(directory.Path, $"azdo-runid.123.participant.{joinerProcessId}.json"), "stale");
+
+ AzureDevOpsCoordinatedRun coordinatedRun = await joinerCoordinator.AcquireRunAsync(configuration, _ => Task.FromResult(0), CancellationToken.None);
+ AzureDevOpsLeaseFile? participantLease = JsonSerializer.Deserialize(File.ReadAllText(coordinatedRun.ParticipantFilePath));
+
+ Assert.AreEqual(91, coordinatedRun.RunId);
+ Assert.IsFalse(coordinatedRun.IsOwner);
+ Assert.IsNotNull(participantLease);
+ AzureDevOpsLeaseFile lease = participantLease!;
+ Assert.AreEqual(123, lease.BuildId);
+ Assert.IsGreaterThan(clock.UtcNow, lease.ExpiresAt);
+ }
+
+ [TestMethod]
+ public async Task RunIdCoordinator_FinalizeRunAsync_TimesOutAndLogsWarning()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ FakeClock clock = new() { UtcNow = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero) };
+ CollectingLogger logger = new();
+ FakeTask task = new(timeSpan => clock.UtcNow += timeSpan);
+ AzureDevOpsTestResultsPublisherOptions options = new(10, TimeSpan.FromSeconds(5), 5, TimeSpan.FromMilliseconds(10), TimeSpan.FromMilliseconds(30), TimeSpan.FromHours(4));
+ int aliveProcessId = GetAliveProcessId();
+ Mock environment = CreateEnvironmentMock(processId: aliveProcessId);
+ AzureDevOpsRunIdCoordinator coordinator = new(new SystemFileSystem(), task, clock, environment.Object, logger, options);
+ string ownerFilePath = Path.Combine(directory.Path, "azdo-runid.123.owner");
+ string runIdFilePath = Path.Combine(directory.Path, "azdo-runid.123.json");
+ string participantFilePath = Path.Combine(directory.Path, $"azdo-runid.123.participant.{int.MaxValue}.json");
+
+ File.WriteAllText(ownerFilePath, JsonSerializer.Serialize(new AzureDevOpsLeaseFile(aliveProcessId, 123, clock.UtcNow.AddHours(1))));
+ File.WriteAllText(runIdFilePath, JsonSerializer.Serialize(new AzureDevOpsRunIdFile(5, 123, "https://dev.azure.com/org/", "project", clock.UtcNow.AddHours(1))));
+ File.WriteAllText(Path.Combine(directory.Path, $"azdo-runid.123.participant.{aliveProcessId}.json"), JsonSerializer.Serialize(new AzureDevOpsLeaseFile(aliveProcessId, 123, clock.UtcNow.AddHours(1))));
+
+ int finalizeCalls = 0;
+ await coordinator.FinalizeRunAsync(new AzureDevOpsCoordinatedRun(5, true, 123, directory.Path, runIdFilePath, ownerFilePath, participantFilePath), _ =>
+ {
+ finalizeCalls++;
+ return Task.CompletedTask;
+ }, CancellationToken.None);
+
+ Assert.AreEqual(1, finalizeCalls);
+ Assert.Contains(log => log.IndexOf("timed out", StringComparison.OrdinalIgnoreCase) >= 0, logger.Logs);
+ Assert.IsFalse(File.Exists(ownerFilePath));
+ Assert.IsFalse(File.Exists(runIdFilePath));
+ }
+
+ [TestMethod]
+ public async Task RunIdCoordinator_AcquireRunAsync_TransientUnreadableOwnerPreservesItForRetry()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ FakeClock clock = new() { UtcNow = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero) };
+ CollectingLogger logger = new();
+ // Tiny joiner max-wait so the test gives up quickly when the owner file looks transient.
+ AzureDevOpsTestResultsPublisherOptions options = new(10, TimeSpan.FromSeconds(5), 2, TimeSpan.FromMilliseconds(1), TimeSpan.FromSeconds(30), TimeSpan.FromHours(4), TimeSpan.FromMilliseconds(20));
+ FakeTask task = new(timeSpan => clock.UtcNow += timeSpan);
+ Mock environment = CreateEnvironmentMock(processId: GetAliveProcessId());
+ SystemFileSystem fileSystem = new();
+ AzureDevOpsRunIdCoordinator coordinator = new(fileSystem, task, clock, environment.Object, logger, options);
+ AzureDevOpsPublishConfiguration configuration = new("https://dev.azure.com/org/", "project", "token", 123, "run", "tests.dll", directory.Path);
+
+ // Write garbage that mimics a partial owner-write (file exists, but content is unparseable JSON).
+ string ownerFilePath = Path.Combine(directory.Path, "azdo-runid.123.owner");
+ File.WriteAllText(ownerFilePath, "{partial");
+
+ // Acquiring should fail because the owner file looks transient and we refuse to clobber it.
+ InvalidOperationException exception = await Assert.ThrowsExactlyAsync(
+ () => coordinator.AcquireRunAsync(configuration, _ => Task.FromResult(99), CancellationToken.None));
+
+ Assert.AreEqual(AzureDevOpsResources.AzureDevOpsLivePublishingMissingRunIdFile, exception.Message);
+ // The owner file must still be present so the real owner can complete its write.
+ Assert.IsTrue(File.Exists(ownerFilePath));
+ }
+
+ [TestMethod]
+ public async Task RunIdCoordinator_AcquireRunAsync_JoinerKeepsWaitingWhileOwnerLeaseValid()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ FakeClock clock = new() { UtcNow = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero) };
+ CollectingLogger logger = new();
+ // CoordinationReadRetryCount=2 — the initial retry budget is small (4 ms total). Without
+ // owner-lease-aware waiting the joiner would give up immediately, but the joiner max-wait
+ // (200 ms) plus an active owner lease (1 h) lets it keep polling.
+ AzureDevOpsTestResultsPublisherOptions options = new(10, TimeSpan.FromSeconds(5), 2, TimeSpan.FromMilliseconds(1), TimeSpan.FromSeconds(30), TimeSpan.FromHours(4), TimeSpan.FromMilliseconds(200));
+ FakeTask task = new(timeSpan => clock.UtcNow += timeSpan);
+ int joinerProcessId = int.MaxValue;
+ Mock joinerEnvironment = CreateEnvironmentMock(processId: joinerProcessId);
+ SystemFileSystem fileSystem = new();
+ AzureDevOpsRunIdCoordinator joinerCoordinator = new(fileSystem, task, clock, joinerEnvironment.Object, logger, options);
+ AzureDevOpsPublishConfiguration configuration = new("https://dev.azure.com/org/", "project", "token", 123, "run", "tests.dll", directory.Path);
+
+ string ownerFilePath = Path.Combine(directory.Path, "azdo-runid.123.owner");
+ string runIdFilePath = Path.Combine(directory.Path, "azdo-runid.123.json");
+
+ // Simulate an owner that has acquired the lease but is still inside a long CreateTestRunAsync.
+ File.WriteAllText(ownerFilePath, JsonSerializer.Serialize(new AzureDevOpsLeaseFile(GetAliveProcessId(), 123, clock.UtcNow.AddHours(1))));
+
+ // Drop the run-id file after the joiner has already exhausted the base retry budget so we
+ // exercise the owner-lease-aware extension of the wait loop.
+ int delayCalls = 0;
+ task = new(timeSpan =>
+ {
+ clock.UtcNow += timeSpan;
+ delayCalls++;
+ if (delayCalls == 3)
+ {
+ File.WriteAllText(runIdFilePath, JsonSerializer.Serialize(new AzureDevOpsRunIdFile(77, 123, configuration.CollectionUri, configuration.Project, clock.UtcNow.AddHours(1))));
+ }
+ });
+ joinerCoordinator = new(fileSystem, task, clock, joinerEnvironment.Object, logger, options);
+
+ AzureDevOpsCoordinatedRun coordinatedRun = await joinerCoordinator.AcquireRunAsync(configuration, _ => Task.FromResult(0), CancellationToken.None);
+
+ Assert.AreEqual(77, coordinatedRun.RunId);
+ Assert.IsFalse(coordinatedRun.IsOwner);
+ Assert.IsGreaterThan(2, delayCalls);
+ }
+
+ [TestMethod]
+ public async Task RunIdCoordinator_AcquireRunAsync_JoinerGivesUpWhenOwnerLeaseExpires()
+ {
+ using TestDirectory directory = CreateTestDirectory();
+ FakeClock clock = new() { UtcNow = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero) };
+ CollectingLogger logger = new();
+ AzureDevOpsTestResultsPublisherOptions options = new(10, TimeSpan.FromSeconds(5), 2, TimeSpan.FromMilliseconds(1), TimeSpan.FromSeconds(30), TimeSpan.FromHours(4), TimeSpan.FromMinutes(5));
+ FakeTask task = new(timeSpan => clock.UtcNow += timeSpan);
+ Mock joinerEnvironment = CreateEnvironmentMock(processId: int.MaxValue);
+ SystemFileSystem fileSystem = new();
+ AzureDevOpsRunIdCoordinator joinerCoordinator = new(fileSystem, task, clock, joinerEnvironment.Object, logger, options);
+ AzureDevOpsPublishConfiguration configuration = new("https://dev.azure.com/org/", "project", "token", 123, "run", "tests.dll", directory.Path);
+
+ // Owner lease is already expired and the owner PID is a long-dead one, so the joiner should
+ // take over and create the run itself rather than waiting indefinitely.
+ File.WriteAllText(Path.Combine(directory.Path, "azdo-runid.123.owner"), JsonSerializer.Serialize(new AzureDevOpsLeaseFile(int.MaxValue, 123, clock.UtcNow.AddMinutes(-5))));
+
+ AzureDevOpsCoordinatedRun coordinatedRun = await joinerCoordinator.AcquireRunAsync(configuration, _ => Task.FromResult(123), CancellationToken.None);
+
+ Assert.AreEqual(123, coordinatedRun.RunId);
+ Assert.IsTrue(coordinatedRun.IsOwner);
+ }
+
+ private static async Task StartPublisherAsync(AzureDevOpsTestResultsPublisher publisher)
+ {
+ Assert.IsTrue(await publisher.IsEnabledAsync());
+ await publisher.OnTestSessionStartingAsync(new Microsoft.Testing.Platform.Services.TestSessionContext(CancellationToken.None));
+ }
+
+ private TestDirectory CreateTestDirectory() => new(_directoriesToDelete);
+
+ private static int GetAliveProcessId()
+#if NET
+ => Environment.ProcessId;
+#else
+ => Process.GetCurrentProcess().Id;
+#endif
+
+ private static void TryDeleteDirectory(string path)
+ {
+ try
+ {
+ if (Directory.Exists(path))
+ {
+ Directory.Delete(path, recursive: true);
+ }
+ }
+ catch (IOException)
+ {
+ }
+ catch (UnauthorizedAccessException)
+ {
+ }
+ }
+
+ private static Mock CreateEnvironmentMock(int processId, string? stageName = "stage", string? jobName = "job")
+ {
+ Mock environment = new();
+ environment.SetupGet(x => x.ProcessId).Returns(processId);
+ environment.SetupGet(x => x.MachineName).Returns("agent-name");
+ environment.Setup(x => x.GetEnvironmentVariable("TF_BUILD")).Returns("true");
+ environment.Setup(x => x.GetEnvironmentVariable("SYSTEM_COLLECTIONURI")).Returns("https://dev.azure.com/org/");
+ environment.Setup(x => x.GetEnvironmentVariable("SYSTEM_TEAMPROJECT")).Returns("project");
+ environment.Setup(x => x.GetEnvironmentVariable("SYSTEM_ACCESSTOKEN")).Returns("token");
+ environment.Setup(x => x.GetEnvironmentVariable("BUILD_BUILDID")).Returns("123");
+ environment.Setup(x => x.GetEnvironmentVariable("AGENT_NAME")).Returns("agent-name");
+ environment.Setup(x => x.GetEnvironmentVariable("SYSTEM_STAGENAME")).Returns(stageName);
+ environment.Setup(x => x.GetEnvironmentVariable("SYSTEM_JOBNAME")).Returns(jobName);
+ return environment;
+ }
+
+ private static AzureDevOpsTestResultsPublisher CreatePublisher(
+ string resultsDirectory,
+ AzureDevOpsTestResultsPublisherOptions options,
+ out FakeAzureDevOpsTestResultsClient client,
+ out FakeClock clock,
+ out CollectingLogger logger,
+ Mock? environment = null,
+ Mock? testApplicationModuleInfo = null,
+ ITask? task = null,
+ Mock? processExitCode = null)
+ {
+ Mock commandLineOptions = new();
+ commandLineOptions.Setup(x => x.IsOptionSet(AzureDevOpsCommandLineOptions.PublishAzureDevOpsTestResultsOptionName)).Returns(true);
+ string[]? runNameArguments = null;
+ commandLineOptions.Setup(x => x.TryGetOptionArgumentList(AzureDevOpsCommandLineOptions.PublishAzureDevOpsRunNameOptionName, out runNameArguments)).Returns(false);
+
+ Mock configuration = new();
+ configuration.Setup(x => x[PlatformConfigurationConstants.PlatformResultDirectory]).Returns(resultsDirectory);
+
+ environment ??= CreateEnvironmentMock(processId: GetAliveProcessId());
+
+ testApplicationModuleInfo ??= new Mock();
+ testApplicationModuleInfo.Setup(x => x.TryGetAssemblyName()).Returns("MyTests");
+ testApplicationModuleInfo.Setup(x => x.GetCurrentTestApplicationFullPath()).Returns(Path.Combine("testfx-worktrees", "azdo-live", "artifacts", "MyTests.dll"));
+
+ if (processExitCode is null)
+ {
+ processExitCode = new Mock();
+ processExitCode.Setup(x => x.GetProcessExitCode()).Returns(0);
+ processExitCode.SetupGet(x => x.HasTestAdapterTestSessionFailure).Returns(false);
+ }
+
+ client = new FakeAzureDevOpsTestResultsClient();
+ clock = new FakeClock { UtcNow = new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero) };
+ logger = new CollectingLogger();
+
+ return new AzureDevOpsTestResultsPublisher(
+ commandLineOptions.Object,
+ configuration.Object,
+ environment.Object,
+ new SystemFileSystem(),
+ testApplicationModuleInfo.Object,
+ processExitCode.Object,
+ client,
+ task ?? new FakeTask(),
+ clock,
+ logger,
+ options);
+ }
+
+ private static TestNodeUpdateMessage CreateMessage(TestNode node)
+ => new(new SessionUid(Guid.NewGuid().ToString()), node);
+
+ private static TestNode CreateNode(string uid, IProperty state, DateTimeOffset startTime, TimingProperty? timing = null)
+ {
+ PropertyBag properties = timing is null ? new PropertyBag(state) : new PropertyBag(state, timing);
+ return new TestNode
+ {
+ Uid = new TestNodeUid(uid),
+ DisplayName = uid,
+ Properties = properties,
+ };
+ }
+
+ private sealed class FakeClock : IClock
+ {
+ public DateTimeOffset UtcNow { get; set; }
+ }
+
+ private sealed class FakeAzureDevOpsTestResultsClient : IAzureDevOpsTestResultsClient
+ {
+ public Func> CreateTestRunAsyncFunc { get; set; } = (_, _) => Task.FromResult(0);
+
+ public Func, CancellationToken, Task> PublishTestResultsAsyncFunc { get; set; } = (_, _, _, _) => Task.CompletedTask;
+
+ public List<(AzureDevOpsPublishConfiguration Configuration, int RunId, string State)> UpdateTestRunStateCalls { get; } = [];
+
+ public Task CreateTestRunAsync(AzureDevOpsPublishConfiguration configuration, CancellationToken cancellationToken)
+ => CreateTestRunAsyncFunc(configuration, cancellationToken);
+
+ public Task PublishTestResultsAsync(AzureDevOpsPublishConfiguration configuration, int runId, IReadOnlyList results, CancellationToken cancellationToken)
+ => PublishTestResultsAsyncFunc(configuration, runId, results, cancellationToken);
+
+ public Task UpdateTestRunStateAsync(AzureDevOpsPublishConfiguration configuration, int runId, string state, CancellationToken cancellationToken)
+ {
+ UpdateTestRunStateCalls.Add((configuration, runId, state));
+ return Task.CompletedTask;
+ }
+ }
+
+ private sealed class FakeTask(Action? delayCallback = null) : ITask
+ {
+ public List DelayCalls { get; } = [];
+
+ public Task Delay(int millisecondDelay)
+ {
+ DelayCalls.Add(TimeSpan.FromMilliseconds(millisecondDelay));
+ return Task.CompletedTask;
+ }
+
+ public Task Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
+ {
+ DelayCalls.Add(timeSpan);
+ delayCallback?.Invoke(timeSpan);
+ return Task.CompletedTask;
+ }
+
+ public Task Run(Action action)
+ {
+ action();
+ return Task.CompletedTask;
+ }
+
+ public Task Run(Func function, CancellationToken cancellationToken)
+ => function();
+
+ public Task Run(Func?> function, CancellationToken cancellationToken)
+ => function()!;
+
+ public Task RunLongRunning(Func action, string name, CancellationToken cancellationToken)
+ => action();
+
+ public Task WhenAll(params Task[] tasks)
+ => Task.WhenAll(tasks);
+ }
+
+ private sealed class CollectingLogger : ILogger
+ {
+ public List Logs { get; } = [];
+
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ public void Log(LogLevel logLevel, TState state, Exception? exception, Func formatter)
+ => Logs.Add($"{logLevel}: {formatter(state, exception)}");
+
+ public Task LogAsync(LogLevel logLevel, TState state, Exception? exception, Func formatter)
+ {
+ Logs.Add($"{logLevel}: {formatter(state, exception)}");
+ return Task.CompletedTask;
+ }
+ }
+
+ private sealed class QueueHttpMessageHandler(params Func>[] responses) : HttpMessageHandler
+ {
+ private readonly Queue>> _responses = new(responses);
+
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ => _responses.Dequeue().Invoke(request, cancellationToken);
+ }
+
+ private sealed class TestDirectory : IDisposable
+ {
+ public TestDirectory(ICollection trackedDirectories)
+ {
+ Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), nameof(AzureDevOpsLivePublishingTests), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(Path);
+ trackedDirectories.Add(Path);
+ }
+
+ public string Path { get; }
+
+ public void Dispose()
+ => TryDeleteDirectory(Path);
+ }
+}