Skip to content
This repository has been archived by the owner on Nov 1, 2023. It is now read-only.

Add a field to the job summary to indicate if at least one bug was created. #3441

Closed
wants to merge 9 commits into from
11 changes: 4 additions & 7 deletions src/ApiService/ApiService/Functions/Jobs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,10 @@ private async Task<HttpResponseData> Get(HttpRequestData req) {
static JobTaskInfo TaskToJobTaskInfo(Task t) => new(t.TaskId, t.Config.Task.Type, t.State);

var tasks = _context.TaskOperations.SearchStates(jobId);
if (search.WithTasks ?? false) {
var ts = await tasks.ToListAsync();
return await RequestHandling.Ok(req, JobResponse.ForJob(job, ts));
} else {
var taskInfo = await tasks.Select(TaskToJobTaskInfo).ToListAsync();
return await RequestHandling.Ok(req, JobResponse.ForJob(job, taskInfo));
}

IAsyncEnumerable<IJobTaskInfo> taskInfo = search.WithTasks ?? false ? tasks : tasks.Select(TaskToJobTaskInfo);
var hasBugs = await _context.AdoNotificationEntryOperations.WasNotfied(jobId);
return await RequestHandling.Ok(req, JobResponse.ForJob(job, taskInfo.ToEnumerable(), hasBugs));
}

var jobs = await _context.JobOperations.SearchState(states: search.State ?? Enumerable.Empty<JobState>()).ToListAsync();
Expand Down
8 changes: 8 additions & 0 deletions src/ApiService/ApiService/OneFuzzTypes/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1178,3 +1178,11 @@ string ReproCmd
public interface ITruncatable<T> {
public T Truncate(int maxLength);
}

public record AdoNotificationEntry(
[PartitionKey] Guid JobId,
[RowKey] int Id,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A work item ID needs an associated ADO org. We should store the ADO org here as well, otherwise it'll be very painful to figure out which org it was created in. Could we also name this WorkItemId so we know what the ID is for?

string Title
) : EntityBase {

}
1 change: 1 addition & 0 deletions src/ApiService/ApiService/OneFuzzTypes/Requests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ public record JobSearch(
List<JobState>? State = null,
List<TaskState>? TaskState = null,
bool? WithTasks = null

) : BaseRequest;

public record NodeAddSshKeyPost(
Expand Down
9 changes: 6 additions & 3 deletions src/ApiService/ApiService/OneFuzzTypes/Responses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public record ContainerInfo(
Uri SasUrl
) : BaseResponse();


public record JobResponse(
Guid JobId,
JobState State,
Expand All @@ -101,10 +102,11 @@ public record JobResponse(
IEnumerable<IJobTaskInfo>? TaskInfo,
StoredUserInfo? UserInfo,
[property: JsonPropertyName("Timestamp")] // must retain capital T for backcompat
DateTimeOffset? Timestamp
DateTimeOffset? Timestamp,
bool HasBugs
// not including UserInfo from Job model
) : BaseResponse() {
public static JobResponse ForJob(Job j, IEnumerable<IJobTaskInfo>? taskInfo)
public static JobResponse ForJob(Job j, IEnumerable<IJobTaskInfo>? taskInfo, bool hasBugs = false)
=> new(
JobId: j.JobId,
State: j.State,
Expand All @@ -113,7 +115,8 @@ public static JobResponse ForJob(Job j, IEnumerable<IJobTaskInfo>? taskInfo)
EndTime: j.EndTime,
TaskInfo: taskInfo,
UserInfo: j.UserInfo,
Timestamp: j.Timestamp
Timestamp: j.Timestamp,
HasBugs: hasBugs
);
public DateTimeOffset? StartTime => EndTime is DateTimeOffset endTime ? endTime.Subtract(TimeSpan.FromHours(Config.Duration)) : null;
}
Expand Down
5 changes: 3 additions & 2 deletions src/ApiService/ApiService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ namespace Microsoft.OneFuzz.Service;
public class Program {

/// <summary>
///
///
/// </summary>
public class LoggingMiddleware : IFunctionsWorkerMiddleware {
/// <summary>
///
///
/// </summary>
/// <param name="context"></param>
/// <param name="next"></param>
Expand Down Expand Up @@ -198,6 +198,7 @@ public static async Async.Task Main() {
.AddScoped<INodeMessageOperations, NodeMessageOperations>()
.AddScoped<ISubnet, Subnet>()
.AddScoped<IAutoScaleOperations, AutoScaleOperations>()
.AddScoped<IAdoNotificationEntryOperations, AdoNotificationEntryOperations>()
.AddSingleton<GraphServiceClient>(new GraphServiceClient(new DefaultAzureCredential()))
.AddSingleton<DependencyTrackingTelemetryModule>()
.AddSingleton<ICreds, Creds>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using ApiService.OneFuzzLib.Orm;
using Microsoft.Extensions.Logging;
namespace Microsoft.OneFuzz.Service;

public interface IAdoNotificationEntryOperations : IOrm<AdoNotificationEntry> {

public IAsyncEnumerable<AdoNotificationEntry> GetByJobId(Guid jobId);

public Async.Task<bool> WasNotfied(Guid jobId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: typo WasNotified


}
public class AdoNotificationEntryOperations : Orm<AdoNotificationEntry>, IAdoNotificationEntryOperations {

public AdoNotificationEntryOperations(ILogger<AdoNotificationEntryOperations> log, IOnefuzzContext context)
: base(log, context) {

}

public IAsyncEnumerable<AdoNotificationEntry> GetByJobId(Guid jobId) {
return QueryAsync(filter: Query.PartitionKey(jobId.ToString()));
}

public async Async.Task<bool> WasNotfied(Guid jobId) {
return await QueryAsync(filter: Query.PartitionKey(jobId.ToString()), maxPerPage: 1).AnyAsync();
}
}
3 changes: 3 additions & 0 deletions src/ApiService/ApiService/onefuzzlib/OnefuzzContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public interface IOnefuzzContext {
ITeams Teams { get; }
IGithubIssues GithubIssues { get; }
IAdo Ado { get; }
IAdoNotificationEntryOperations AdoNotificationEntryOperations { get; }

IFeatureManagerSnapshot FeatureManagerSnapshot { get; }
IConfigurationRefresher ConfigurationRefresher { get; }
Expand Down Expand Up @@ -105,4 +106,6 @@ public OnefuzzContext(IServiceProvider serviceProvider) {
public IFeatureManagerSnapshot FeatureManagerSnapshot => _serviceProvider.GetRequiredService<IFeatureManagerSnapshot>();

public IConfigurationRefresher ConfigurationRefresher => _serviceProvider.GetRequiredService<IConfigurationRefresherProvider>().Refreshers.First();

public IAdoNotificationEntryOperations AdoNotificationEntryOperations => _serviceProvider.GetRequiredService<IAdoNotificationEntryOperations>();
}
34 changes: 26 additions & 8 deletions src/ApiService/ApiService/onefuzzlib/notifications/Ado.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ public class Ado : NotificationsBase, IAdo {
private const string TITLE_FIELD = "System.Title";
private static List<string> DEFAULT_REGRESSION_IGNORE_STATES = new() { "New", "Commited", "Active" };

public enum AdoNotificationPublishignState {
Added,
Updated,
Skipped,
}

public record ProcessResult(AdoNotificationPublishignState State, WorkItem WorkItem);

public Ado(ILogger<Ado> logTracer, IOnefuzzContext context) : base(logTracer, context) {
}

Expand Down Expand Up @@ -319,7 +327,14 @@ private static async Async.Task ProcessNotification(IOnefuzzContext context, Con

var renderedConfig = RenderAdoTemplate(logTracer, renderer, config, instanceUrl);
var ado = new AdoConnector(renderedConfig, project!, client, instanceUrl, logTracer, await GetValidFields(client, project));
await ado.Process(notificationInfo, isRegression);
await foreach (var processState in ado.Process(notificationInfo, isRegression)) {
if (processState.State == AdoNotificationPublishignState.Added) {
if (processState.WorkItem.Id == null) {
continue;
}
_ = await context.AdoNotificationEntryOperations.Update(new AdoNotificationEntry(report.JobId, (int)processState.WorkItem.Id, (string)processState.WorkItem.Fields[TITLE_FIELD]));
}
}
}

public static RenderedAdoTemplate RenderAdoTemplate(ILogger logTracer, Renderer renderer, AdoTemplate original, Uri instanceUrl) {
Expand Down Expand Up @@ -529,28 +544,25 @@ public async Async.Task<bool> UpdateExisting(WorkItem item, IList<(string, strin
// the below was causing on_duplicate not to work
// var systemState = JsonSerializer.Serialize(item.Fields["System.State"]);
var systemState = (string)item.Fields["System.State"];
var stateUpdated = false;
if (_config.OnDuplicate.SetState.TryGetValue(systemState, out var v)) {
document.Add(new JsonPatchOperation() {
Operation = VisualStudio.Services.WebApi.Patch.Operation.Replace,
Path = "/fields/System.State",
Value = v
});

stateUpdated = true;
}

if (document.Any()) {
_ = await _client.UpdateWorkItemAsync(document, _project, (int)item.Id!);
var adoEventType = "AdoUpdate";
_logTracer.LogEvent(adoEventType);
return true;

} else {
var adoEventType = "AdoNoUpdate";
_logTracer.LogEvent(adoEventType);
return false;
}

return stateUpdated;
}

private bool MatchesUnlessCase(WorkItem workItem) =>
Expand Down Expand Up @@ -607,7 +619,8 @@ private async Async.Task<WorkItem> CreateNew() {
return (taskType, document);
}

public async Async.Task Process(IList<(string, string)> notificationInfo, bool isRegression) {

public async IAsyncEnumerable<ProcessResult> Process(IList<(string, string)> notificationInfo, bool isRegression) {
var updated = false;
WorkItem? oldestWorkItem = null;
await foreach (var workItem in ExistingWorkItems(notificationInfo)) {
Expand All @@ -617,7 +630,9 @@ public async Async.Task Process(IList<(string, string)> notificationInfo, bool i
_logTracer.AddTags(new List<(string, string)> { ("MatchingWorkItemIds", $"{workItem.Id}") });
_logTracer.LogInformation("Found matching work item");
}

if (IsADODuplicateWorkItem(workItem, _config.AdoDuplicateFields)) {
yield return new ProcessResult(AdoNotificationPublishignState.Skipped, workItem);
continue;
}

Expand All @@ -634,11 +649,12 @@ public async Async.Task Process(IList<(string, string)> notificationInfo, bool i
}

_ = await UpdateExisting(workItem, notificationInfo);
yield return new ProcessResult(AdoNotificationPublishignState.Updated, workItem);
updated = true;
}

if (updated || isRegression) {
return;
yield break;
}

if (oldestWorkItem != null) {
Expand All @@ -656,13 +672,15 @@ public async Async.Task Process(IList<(string, string)> notificationInfo, bool i
_project,
(int)oldestWorkItem.Id!);
}
yield return new ProcessResult(AdoNotificationPublishignState.Updated, oldestWorkItem);
} else {
// We never saw a work item like this before, it must be new
var entry = await CreateNew();
var adoEventType = "AdoNewItem";
_logTracer.AddTags(notificationInfo);
_logTracer.AddTag("WorkItemId", entry.Id.HasValue ? entry.Id.Value.ToString() : "");
_logTracer.LogEvent(adoEventType);
yield return new ProcessResult(AdoNotificationPublishignState.Added, entry);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/ApiService/FunctionalTests/1f-api/Jobs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public async Task<Result<IEnumerable<Job>, Error>> Get(Guid? jobId = null, List<
.AddIfNotNullV("task_state", taskState)
.AddIfNotNullV("with_tasks", withTasks);


var r = await Get(n);
return IEnumerableResult<Job>(r);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.Extensions.Logging;
using Microsoft.OneFuzz.Service;
namespace IntegrationTests.Fakes;

public sealed class TestAdoNotificationEntryOperations : AdoNotificationEntryOperations {
public TestAdoNotificationEntryOperations(ILogger<AdoNotificationEntryOperations> log, IOnefuzzContext context)
: base(log, context) { }
}
25 changes: 25 additions & 0 deletions src/ApiService/IntegrationTests/Fakes/TestContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Microsoft.Extensions.Caching.Memory;
Expand Down Expand Up @@ -42,6 +43,7 @@ public TestContext(IHttpClientFactory httpClientFactory, OneFuzzLoggerProvider p
ReproOperations = new ReproOperations(provider.CreateLogger<ReproOperations>(), this);
Reports = new Reports(provider.CreateLogger<Reports>(), Containers);
NotificationOperations = new NotificationOperations(provider.CreateLogger<NotificationOperations>(), this);
AdoNotificationEntryOperations = new TestAdoNotificationEntryOperations(provider.CreateLogger<AdoNotificationEntryOperations>(), this);

FeatureManagerSnapshot = new TestFeatureManagerSnapshot();
WebhookOperations = new TestWebhookOperations(httpClientFactory, provider.CreateLogger<WebhookOperations>(), this);
Expand All @@ -65,9 +67,28 @@ public Async.Task InsertAll(params EntityBase[] objs)
InstanceConfig ic => ConfigOperations.Insert(ic),
Notification n => NotificationOperations.Insert(n),
Webhook w => WebhookOperations.Insert(w),
AdoNotificationEntry ado => AdoNotificationEntryOperations.Insert(ado),
_ => throw new NotSupportedException($"You will need to add an TestContext.InsertAll case for {x.GetType()} entities"),
}));

public Async.Task InsertAll(IEnumerable<EntityBase> objs)
=> Async.Task.WhenAll(
objs.Select(x => x switch {
Task t => TaskOperations.Insert(t),
Node n => NodeOperations.Insert(n),
Pool p => PoolOperations.Insert(p),
Job j => JobOperations.Insert(j),
JobResult jr => JobResultOperations.Insert(jr),
Repro r => ReproOperations.Insert(r),
Scaleset ss => ScalesetOperations.Insert(ss),
NodeTasks nt => NodeTasksOperations.Insert(nt),
InstanceConfig ic => ConfigOperations.Insert(ic),
Notification n => NotificationOperations.Insert(n),
Webhook w => WebhookOperations.Insert(w),
AdoNotificationEntry ado => AdoNotificationEntryOperations.Insert(ado),
_ => throw new NotSupportedException($"You will need to add an TestContext.InsertAll case for {x.GetType()} entities"),
}));

// Implementations:

public IMemoryCache Cache { get; }
Expand Down Expand Up @@ -109,6 +130,8 @@ public Async.Task InsertAll(params EntityBase[] objs)

public IWebhookMessageLogOperations WebhookMessageLogOperations { get; }

public IAdoNotificationEntryOperations AdoNotificationEntryOperations { get; }

// -- Remainder not implemented --

public IConfig Config => throw new System.NotImplementedException();
Expand Down Expand Up @@ -143,4 +166,6 @@ public Async.Task InsertAll(params EntityBase[] objs)
public IAdo Ado => throw new NotImplementedException();

public IConfigurationRefresher ConfigurationRefresher => throw new NotImplementedException();


}
24 changes: 24 additions & 0 deletions src/ApiService/IntegrationTests/JobsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,28 @@ await Context.InsertAll(
Assert.Equal(task.Config.Task.Type, returnedTasks[0].Type);

}

[Fact]
public async Async.Task Get_CanFindSpecificJobWithBugs() {
var taskConfig = new TaskConfig(_jobId, new List<Guid>(), new TaskDetails(TaskType.Coverage, 60));

var random = new Random();
var bugs = Enumerable.Range(1, 100).Select(i => random.Next(0, 100)).Distinct().Select(i => new AdoNotificationEntry(_jobId, i, $"test_i")).ToList();
await Context.InsertAll(bugs);
await Context.InsertAll(
new Job(_jobId, JobState.Stopped, _config, null),
new Task(_jobId, Guid.NewGuid(), TaskState.Running, Os.Windows, taskConfig)
);

var func = new Jobs(Context, LoggerProvider.CreateLogger<Jobs>());

var ctx = new TestFunctionContext();
var result = await func.Run(TestHttpRequestData.FromJson("GET", new JobSearch(JobId: _jobId)), ctx);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);

var response = BodyAs<JobResponse>(result);
Assert.Equal(_jobId, response.JobId);
Assert.NotNull(response.TaskInfo);
Assert.True(response.HasBugs);
}
}
1 change: 1 addition & 0 deletions src/pytypes/onefuzztypes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ class Job(BaseModel):
task_info: Optional[List[Union[Task, JobTaskInfo]]]
user_info: Optional[UserInfo]
start_time: Optional[datetime] = None
has_bugs: Optional[bool] = None


class NetworkConfig(BaseModel):
Expand Down
Loading