Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions BuildMonitor.Test/AzureDevOpsStatusTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.BuildMonitor.Test;

using ktsu.Semantics.Strings;

using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Covers the provider status after an Azure DevOps request. The request wrapper cleared the status
/// after every action that returned normally, so an error the action reported itself, such as an
/// owner that matches no project, was wiped straight away and never reached the status bar
/// (ktsu-dev/BuildMonitor#304).
/// </summary>
[TestClass]
public sealed class AzureDevOpsStatusTests
{
private static readonly OwnerName MissingProject = "Missing".As<OwnerName>();

[TestMethod]
public async Task AProjectNotFoundErrorSurvivesTheRequestThatReportedIt()
{
// Arrange
AzureDevOps provider = new();

// Act
await provider.MakeAzureDevOpsRequestAsync("test/missing", () =>
{
provider.ReportProjectNotFound(MissingProject);
return Task.CompletedTask;
}).ConfigureAwait(false);

// Assert
Assert.AreEqual(ProviderStatus.Error, provider.Status, "The not-found error should still be showing once the request returns");
Assert.Contains("'Missing' not found", provider.StatusMessage);
}

[TestMethod]
public async Task ALaterSuccessfulRequestClearsTheError()
{
// Arrange
AzureDevOps provider = new();
await provider.MakeAzureDevOpsRequestAsync("test/missing", () =>
{
provider.ReportProjectNotFound(MissingProject);
return Task.CompletedTask;
}).ConfigureAwait(false);

// Act
await provider.MakeAzureDevOpsRequestAsync("test/ok", () => Task.CompletedTask).ConfigureAwait(false);

// Assert
Assert.AreEqual(ProviderStatus.OK, provider.Status);
Assert.AreEqual(string.Empty, provider.StatusMessage);
}
}
10 changes: 10 additions & 0 deletions BuildMonitor/BuildProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ protected BuildProviderToken Token
[JsonIgnore]
internal string StatusMessage { get; private set; } = string.Empty;

/// <summary>
/// Counts calls to <see cref="SetStatus"/>, so a request wrapper can tell whether the work it ran
/// reported a status of its own that a blanket <see cref="ClearStatus"/> would overwrite.
/// </summary>
[JsonIgnore]
internal int StatusSetCount => statusSetCount;

private int statusSetCount;

/// <summary>
/// When rate limited, the time when the rate limit resets.
/// </summary>
Expand Down Expand Up @@ -482,6 +491,7 @@ protected void SetStatus(ProviderStatus status, string message)
Status = status;
StatusMessage = message;
StatusTimestamp = DateTimeOffset.UtcNow;
_ = Interlocked.Increment(ref statusSetCount);
}

/// <summary>
Expand Down
13 changes: 11 additions & 2 deletions BuildMonitor/Providers/AzureDevOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
Owner owner = Owners[ownerName];
RepositoryId repositoryId = project.Id.ToString().As<RepositoryId>();
RepositoryName repositoryName = project.Name.As<RepositoryName>();
_ = owner.Repositories.GetOrAdd(repositoryId, _ => owner.CreateRepository(repositoryName, repositoryId));

Check warning on line 183 in BuildMonitor/Providers/AzureDevOps.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Use the lambda parameter instead of capturing the argument 'repositoryId'

Check warning on line 183 in BuildMonitor/Providers/AzureDevOps.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Use the lambda parameter instead of capturing the argument 'repositoryId'

Check warning on line 183 in BuildMonitor/Providers/AzureDevOps.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Use the lambda parameter instead of capturing the argument 'repositoryId'

Check warning on line 183 in BuildMonitor/Providers/AzureDevOps.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Use the lambda parameter instead of capturing the argument 'repositoryId'
}
Log.Info($"{Name}: DiscoverProjects found {projectCount} project(s)");
}).ConfigureAwait(false);
Expand Down Expand Up @@ -222,7 +222,7 @@

// Get existing repository or create new one
bool isNew = !owner.Repositories.ContainsKey(repositoryId);
_ = owner.Repositories.GetOrAdd(repositoryId, _ => owner.CreateRepository(repositoryName, repositoryId));

Check warning on line 225 in BuildMonitor/Providers/AzureDevOps.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Use the lambda parameter instead of capturing the argument 'repositoryId'

Check warning on line 225 in BuildMonitor/Providers/AzureDevOps.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Use the lambda parameter instead of capturing the argument 'repositoryId'

Check warning on line 225 in BuildMonitor/Providers/AzureDevOps.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Use the lambda parameter instead of capturing the argument 'repositoryId'

Check warning on line 225 in BuildMonitor/Providers/AzureDevOps.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Use the lambda parameter instead of capturing the argument 'repositoryId'

if (isNew)
{
Expand All @@ -238,7 +238,7 @@
if (!foundProject)
{
Log.Warning($"{Name}: Project '{owner.Name}' not found among {projectCount} projects in organization '{AccountId}'");
SetStatus(ProviderStatus.Error, $"Project '{owner.Name}' not found in organization '{AccountId}'");
ReportProjectNotFound(owner.Name);
}
}).ConfigureAwait(false);
}
Expand Down Expand Up @@ -270,7 +270,7 @@

// Get existing build or create new one
bool isNew = false;
Build build = repository.Builds.GetOrAdd(buildId, _ =>

Check warning on line 273 in BuildMonitor/Providers/AzureDevOps.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Use the lambda parameter instead of capturing the argument 'buildId'

Check warning on line 273 in BuildMonitor/Providers/AzureDevOps.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Use the lambda parameter instead of capturing the argument 'buildId'
{
isNew = true;
return repository.CreateBuild(buildName, buildId);
Expand Down Expand Up @@ -398,6 +398,9 @@
BuildMonitor.QueueSaveAppData();
}

internal void ReportProjectNotFound(OwnerName ownerName) =>
SetStatus(ProviderStatus.Error, $"Project '{ownerName}' not found in organization '{AccountId}'");

internal async Task MakeAzureDevOpsRequestAsync(string name, Func<Task> action)
{
await RequestSemaphore.WaitAsync().ConfigureAwait(false);
Expand All @@ -412,8 +415,14 @@

try
{
// Clear only when the action reported no status of its own. A project that matched no
// owner used to be reported and then cleared straight away (ktsu-dev/BuildMonitor#304).
int statusSetCountBefore = StatusSetCount;
await BuildMonitor.MakeRequestAsync(name, action).ConfigureAwait(false);
ClearStatus();
if (StatusSetCount == statusSetCountBefore)
{
ClearStatus();
}
}
catch (VssServiceResponseException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.Unauthorized)
{
Expand Down
Loading