[1742] Ensure multiple composite annoations are correctly written. (#2311)

* [1742] Ensure multiple composite annoations are correctly written.

This implementation uses a collector pattern to allow embedded ExecutionContexts to stash Issue objects for later processing by a non-embedded ancestor ExecutionContext.

Also:
 - Provide explicit constructor implementations for ExecutionContext
 - Leverage explicit constructors to solidify immutability of several ExecutionContext class members.
 - Fixed erroneous call to ExecutionContext.Complete in CompositeActionHandler.cs
 - Use a consistent timestamp for FinishTime in ExecutionContext::Complete

* Ensure collected issues are processed only by a non-embedded ExecutionContext.
This was already implicit.  Now, just making it explicit.

* Provide a clear mechanism that allows callers to opt-in/opt-out of ExecutionContext::AddIssue's logging behavior.
* Addressed deserialization inconsistencies in TimelineRecord.cs
* Added TimelineRecord unit tests.
* Refined unit tests related to TimelineRecord::Variables case-insensitivity
* Add a unit test that verifies ExecutionContextLogOptions::LogMessageOverride has the desired effect.
* Responded to PR feedback.
* Don't allow embedded ExecutionContexts to add Issues to a TimelineRecord
This commit is contained in:
John Wesley Walker III
2023-05-10 16:24:02 +02:00
committed by GitHub
parent 1bc14f0607
commit f8a28c3c4e
21 changed files with 794 additions and 270 deletions

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -52,42 +52,43 @@ namespace GitHub.Runner.Common.Tests.Worker
// Act.
ec.InitializeJob(jobRequest, CancellationToken.None);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
// Flood the ExecutionContext with errors and warnings (past its max capacity of 10).
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.Complete();
// Assert.
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.ErrorCount == 15)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.WarningCount == 14)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.ErrorCount > 0)), Times.AtLeast(10));
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.WarningCount > 0)), Times.AtLeast(10));
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Type == IssueType.Error).Count() == 10)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Type == IssueType.Warning).Count() == 10)), Times.AtLeastOnce);
}
@@ -190,9 +191,9 @@ namespace GitHub.Runner.Common.Tests.Worker
bigMessage += "a";
}
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = bigMessage });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = bigMessage });
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = bigMessage });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = bigMessage }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = bigMessage }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = bigMessage }, ExecutionContextLogOptions.Default);
ec.Complete();
@@ -203,6 +204,61 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void AddIssue_OverrideLogMessage()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange: Create a job request message.
TaskOrchestrationPlanReference plan = new();
TimelineReference timeline = new();
Guid jobId = Guid.NewGuid();
string jobName = "some job name";
var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), null, null, null, null);
jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource()
{
Alias = Pipelines.PipelineConstants.SelfAlias,
Id = "github",
Version = "sha1"
});
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
// Arrange: Setup the paging logger.
var pagingLogger = new Mock<IPagingLogger>();
var jobServerQueue = new Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger.Object);
hc.SetSingleton(jobServerQueue.Object);
var ec = new Runner.Worker.ExecutionContext();
ec.Initialize(hc);
// Act.
ec.InitializeJob(jobRequest, CancellationToken.None);
var issueMessage = "Message embedded in issue.";
var overrideMessage = "Message override.";
var options = new ExecutionContextLogOptions(true, overrideMessage);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = issueMessage }, options);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = issueMessage }, options);
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = issueMessage }, options);
// Finally, add a variation that DOESN'T override the message.
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = issueMessage }, ExecutionContextLogOptions.Default);
ec.Complete();
// Assert.
jobServerQueue.Verify(x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<long?>()), Times.Exactly(4));
jobServerQueue.Verify(x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.Is<string>(text => text.EndsWith(overrideMessage)), It.IsAny<long?>()), Times.Exactly(3));
jobServerQueue.Verify(x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.Is<string>(text => text.EndsWith(issueMessage)), It.IsAny<long?>()), Times.Exactly(1));
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
@@ -242,13 +298,15 @@ namespace GitHub.Runner.Common.Tests.Worker
var embeddedStep = ec.CreateChild(Guid.NewGuid(), "action_1_pre", "action_1_pre", null, null, ActionRunStage.Main, isEmbedded: true);
embeddedStep.Start();
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error annotation that should have step and line number information" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning annotation that should have step and line number information" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice annotation that should have step and line number information" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error annotation that should have step and line number information" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning annotation that should have step and line number information" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice annotation that should have step and line number information" }, ExecutionContextLogOptions.Default);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Error).Count() == 1)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Warning).Count() == 1)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Notice).Count() == 1)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()), Times.AtLeastOnce);
// Verify that Error/Warning/Notice issues added to embedded steps don't get sent up to the server.
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Error).Count() == 1)), Times.Never);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Warning).Count() == 1)), Times.Never);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Notice).Count() == 1)), Times.Never);
}
}
@@ -626,12 +684,12 @@ namespace GitHub.Runner.Common.Tests.Worker
ec.StepTelemetry.StepId = Guid.NewGuid();
ec.StepTelemetry.Stage = "main";
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" }, ExecutionContextLogOptions.Default);
ec.Complete();
@@ -692,9 +750,9 @@ namespace GitHub.Runner.Common.Tests.Worker
embeddedStep.StepTelemetry.Action = "actions/checkout";
embeddedStep.StepTelemetry.Ref = "v2";
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" }, ExecutionContextLogOptions.Default);
embeddedStep.PublishStepTelemetry();
@@ -756,9 +814,9 @@ namespace GitHub.Runner.Common.Tests.Worker
embeddedStep.StepTelemetry.Action = "actions/checkout";
embeddedStep.StepTelemetry.Ref = "v2";
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" }, ExecutionContextLogOptions.Default);
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" }, ExecutionContextLogOptions.Default);
ec.Complete();
@@ -927,7 +985,7 @@ namespace GitHub.Runner.Common.Tests.Worker
inputVarsContext["VARIABLE_2"] = new StringContextData("value2");
jobRequest.ContextData["vars"] = inputVarsContext;
// Arrange: Setup the paging logger.
// Arrange: Setup the paging logger.
var pagingLogger1 = new Mock<IPagingLogger>();
var jobServerQueue = new Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger1.Object);
@@ -941,7 +999,7 @@ namespace GitHub.Runner.Common.Tests.Worker
var expected = new DictionaryContextData();
expected["VARIABLE_1"] = new StringContextData("value1");
expected["VARIABLE_2"] = new StringContextData("value1");
Assert.True(ExpressionValuesAssertEqual(expected, jobContext.ExpressionValues["vars"] as DictionaryContextData));
}
}
@@ -972,7 +1030,7 @@ namespace GitHub.Runner.Common.Tests.Worker
inputVarsContext[Constants.Variables.Actions.RunnerDebug] = new StringContextData("true");
jobRequest.ContextData["vars"] = inputVarsContext;
// Arrange: Setup the paging logger.
// Arrange: Setup the paging logger.
var pagingLogger1 = new Mock<IPagingLogger>();
var jobServerQueue = new Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger1.Object);
@@ -983,7 +1041,7 @@ namespace GitHub.Runner.Common.Tests.Worker
jobContext.InitializeJob(jobRequest, CancellationToken.None);
Assert.Equal("true", jobContext.Global.Variables.Get(Constants.Variables.Actions.StepDebug));
Assert.Equal("true", jobContext.Global.Variables.Get(Constants.Variables.Actions.RunnerDebug));
}
@@ -1018,7 +1076,7 @@ namespace GitHub.Runner.Common.Tests.Worker
jobRequest.Variables[Constants.Variables.Actions.StepDebug] = "false";
jobRequest.Variables[Constants.Variables.Actions.RunnerDebug] = "false";
// Arrange: Setup the paging logger.
// Arrange: Setup the paging logger.
var pagingLogger1 = new Mock<IPagingLogger>();
var jobServerQueue = new Mock<IJobServerQueue>();
hc.EnqueueInstance(pagingLogger1.Object);
@@ -1029,7 +1087,7 @@ namespace GitHub.Runner.Common.Tests.Worker
jobContext.InitializeJob(jobRequest, CancellationToken.None);
Assert.Equal("false", jobContext.Global.Variables.Get(Constants.Variables.Actions.StepDebug));
Assert.Equal("false", jobContext.Global.Variables.Get(Constants.Variables.Actions.RunnerDebug));
}
@@ -1061,4 +1119,4 @@ namespace GitHub.Runner.Common.Tests.Worker
return true;
}
}
}
}