mirror of
https://github.com/actions/runner.git
synced 2026-03-15 09:20:44 -04:00
459 lines
19 KiB
C#
459 lines
19 KiB
C#
using GitHub.Actions.WorkflowParser;
|
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
|
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
|
using GitHub.DistributedTask.WebApi;
|
|
using GitHub.Runner.Common;
|
|
using GitHub.Runner.Sdk;
|
|
using GitHub.Runner.Worker;
|
|
using LegacyContextData = GitHub.DistributedTask.Pipelines.ContextData;
|
|
using LegacyExpressions = GitHub.DistributedTask.Expressions2;
|
|
using Moq;
|
|
using Newtonsoft.Json;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using Xunit;
|
|
|
|
namespace GitHub.Runner.Common.Tests.Worker
|
|
{
|
|
/// <summary>
|
|
/// Tests for parser comparison wrapper classes.
|
|
/// </summary>
|
|
public sealed class ActionManifestParserComparisonL0
|
|
{
|
|
private CancellationTokenSource _ecTokenSource;
|
|
private Mock<IExecutionContext> _ec;
|
|
private TestHostContext _hc;
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void ConvertToLegacySteps_ProducesCorrectSteps_WithExplicitPropertyMapping()
|
|
{
|
|
try
|
|
{
|
|
// Arrange - Test that ActionManifestManagerWrapper properly converts new steps to legacy format
|
|
Setup();
|
|
|
|
// Enable comparison feature
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
// Register required services
|
|
var legacyManager = new ActionManifestManagerLegacy();
|
|
legacyManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
|
|
|
var newManager = new ActionManifestManager();
|
|
newManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManager>(newManager);
|
|
|
|
var wrapper = new ActionManifestManagerWrapper();
|
|
wrapper.Initialize(_hc);
|
|
|
|
var manifestPath = Path.Combine(TestUtil.GetTestDataPath(), "conditional_composite_action.yml");
|
|
|
|
// Act - Load through the wrapper (which internally converts)
|
|
var result = wrapper.Load(_ec.Object, manifestPath);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal(ActionExecutionType.Composite, result.Execution.ExecutionType);
|
|
|
|
var compositeExecution = result.Execution as CompositeActionExecutionData;
|
|
Assert.NotNull(compositeExecution);
|
|
Assert.NotNull(compositeExecution.Steps);
|
|
Assert.Equal(6, compositeExecution.Steps.Count);
|
|
|
|
// Verify steps are NOT null (this was the bug - JSON round-trip produced nulls)
|
|
foreach (var step in compositeExecution.Steps)
|
|
{
|
|
Assert.NotNull(step);
|
|
Assert.NotNull(step.Reference);
|
|
Assert.IsType<GitHub.DistributedTask.Pipelines.ScriptReference>(step.Reference);
|
|
}
|
|
|
|
// Verify step with condition
|
|
var successStep = compositeExecution.Steps[2];
|
|
Assert.Equal("success-conditional", successStep.ContextName);
|
|
Assert.Equal("success()", successStep.Condition);
|
|
|
|
// Verify step with complex condition
|
|
var lastStep = compositeExecution.Steps[5];
|
|
Assert.Contains("inputs.exit-code == 1", lastStep.Condition);
|
|
Assert.Contains("failure()", lastStep.Condition);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobContainer_EmptyImage_BothParsersReturnNull()
|
|
{
|
|
try
|
|
{
|
|
// Arrange - Test that both parsers return null for empty container image at runtime
|
|
Setup();
|
|
|
|
var fileTable = new List<string>();
|
|
|
|
// Create legacy evaluator
|
|
var legacyTraceWriter = new GitHub.DistributedTask.ObjectTemplating.EmptyTraceWriter();
|
|
var schema = PipelineTemplateSchemaFactory.GetSchema();
|
|
var legacyEvaluator = new PipelineTemplateEvaluator(legacyTraceWriter, schema, fileTable);
|
|
|
|
// Create new evaluator
|
|
var newTraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter();
|
|
var newEvaluator = new WorkflowTemplateEvaluator(newTraceWriter, fileTable, features: null);
|
|
|
|
// Create a token representing an empty container image (simulates expression evaluated to empty string)
|
|
var emptyImageToken = new StringToken(null, null, null, "");
|
|
|
|
var contextData = new DictionaryContextData();
|
|
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
// Act - Call both evaluators
|
|
var legacyResult = legacyEvaluator.EvaluateJobContainer(emptyImageToken, contextData, expressionFunctions);
|
|
|
|
// Convert token for new evaluator
|
|
var newToken = new GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.StringToken(null, null, null, "");
|
|
var newContextData = new GitHub.Actions.Expressions.Data.DictionaryExpressionData();
|
|
var newExpressionFunctions = new List<GitHub.Actions.Expressions.IFunctionInfo>();
|
|
|
|
var newResult = newEvaluator.EvaluateJobContainer(newToken, newContextData, newExpressionFunctions);
|
|
|
|
// Assert - Both should return null for empty image (no container)
|
|
Assert.Null(legacyResult);
|
|
Assert.Null(newResult);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void FromJsonEmptyString_BothParsersFail_WithDifferentMessages()
|
|
{
|
|
// This test verifies that both parsers fail with different error messages when parsing fromJSON('')
|
|
// The comparison layer should treat these as semantically equivalent (both are JSON parse errors)
|
|
try
|
|
{
|
|
Setup();
|
|
|
|
var fileTable = new List<string>();
|
|
|
|
// Create legacy evaluator
|
|
var legacyTraceWriter = new GitHub.DistributedTask.ObjectTemplating.EmptyTraceWriter();
|
|
var schema = PipelineTemplateSchemaFactory.GetSchema();
|
|
var legacyEvaluator = new PipelineTemplateEvaluator(legacyTraceWriter, schema, fileTable);
|
|
|
|
// Create new evaluator
|
|
var newTraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter();
|
|
var newEvaluator = new WorkflowTemplateEvaluator(newTraceWriter, fileTable, features: null);
|
|
|
|
// Create expression token for fromJSON('')
|
|
var legacyToken = new BasicExpressionToken(null, null, null, "fromJson('')");
|
|
var newToken = new GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.BasicExpressionToken(null, null, null, "fromJson('')");
|
|
|
|
var contextData = new DictionaryContextData();
|
|
var newContextData = new GitHub.Actions.Expressions.Data.DictionaryExpressionData();
|
|
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
|
var newExpressionFunctions = new List<GitHub.Actions.Expressions.IFunctionInfo>();
|
|
|
|
// Act - Both should throw
|
|
Exception legacyException = null;
|
|
Exception newException = null;
|
|
|
|
try
|
|
{
|
|
legacyEvaluator.EvaluateStepDisplayName(legacyToken, contextData, expressionFunctions);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
legacyException = ex;
|
|
}
|
|
|
|
try
|
|
{
|
|
newEvaluator.EvaluateStepName(newToken, newContextData, newExpressionFunctions);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
newException = ex;
|
|
}
|
|
|
|
// Assert - Both threw exceptions
|
|
Assert.NotNull(legacyException);
|
|
Assert.NotNull(newException);
|
|
|
|
// Verify the error messages are different (which is why we need semantic comparison)
|
|
Assert.NotEqual(legacyException.Message, newException.Message);
|
|
|
|
// Verify both are JSON parse errors (contain JSON-related error indicators)
|
|
var legacyFullMsg = GetFullExceptionMessage(legacyException);
|
|
var newFullMsg = GetFullExceptionMessage(newException);
|
|
|
|
// At least one should contain indicators of JSON parsing failure
|
|
var legacyIsJsonError = legacyFullMsg.Contains("JToken") ||
|
|
legacyFullMsg.Contains("JsonReader") ||
|
|
legacyFullMsg.Contains("fromJson");
|
|
var newIsJsonError = newFullMsg.Contains("JToken") ||
|
|
newFullMsg.Contains("JsonReader") ||
|
|
newFullMsg.Contains("fromJson");
|
|
|
|
Assert.True(legacyIsJsonError, $"Legacy exception should be JSON error: {legacyFullMsg}");
|
|
Assert.True(newIsJsonError, $"New exception should be JSON error: {newFullMsg}");
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateDefaultInput_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var legacyManager = new ActionManifestManagerLegacy();
|
|
legacyManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
|
|
|
var newManager = new ActionManifestManager();
|
|
newManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManager>(newManager);
|
|
|
|
var wrapper = new ActionManifestManagerWrapper();
|
|
wrapper.Initialize(_hc);
|
|
|
|
_ec.Object.ExpressionValues["github"] = new LegacyContextData.DictionaryContextData();
|
|
_ec.Object.ExpressionValues["strategy"] = new LegacyContextData.DictionaryContextData();
|
|
_ec.Object.ExpressionValues["matrix"] = new LegacyContextData.DictionaryContextData();
|
|
_ec.Object.ExpressionValues["steps"] = new LegacyContextData.DictionaryContextData();
|
|
_ec.Object.ExpressionValues["job"] = new LegacyContextData.DictionaryContextData();
|
|
_ec.Object.ExpressionValues["runner"] = new LegacyContextData.DictionaryContextData();
|
|
_ec.Object.ExpressionValues["env"] = new LegacyContextData.DictionaryContextData();
|
|
_ec.Object.ExpressionFunctions.Add(new LegacyExpressions.FunctionInfo<GitHub.Runner.Worker.Expressions.HashFilesFunction>("hashFiles", 1, 255));
|
|
|
|
var result = wrapper.EvaluateDefaultInput(_ec.Object, "testInput", new StringToken(null, null, null, "defaultValue"));
|
|
|
|
Assert.Equal("defaultValue", result);
|
|
Assert.False(_ec.Object.Global.HasActionManifestMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateContainerArguments_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var legacyManager = new ActionManifestManagerLegacy();
|
|
legacyManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
|
|
|
var newManager = new ActionManifestManager();
|
|
newManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManager>(newManager);
|
|
|
|
var wrapper = new ActionManifestManagerWrapper();
|
|
wrapper.Initialize(_hc);
|
|
|
|
var arguments = new SequenceToken(null, null, null);
|
|
arguments.Add(new StringToken(null, null, null, "arg1"));
|
|
arguments.Add(new StringToken(null, null, null, "arg2"));
|
|
|
|
var evaluateContext = new Dictionary<string, LegacyContextData.PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var result = wrapper.EvaluateContainerArguments(_ec.Object, arguments, evaluateContext);
|
|
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Equal("arg1", result[0]);
|
|
Assert.Equal("arg2", result[1]);
|
|
Assert.False(_ec.Object.Global.HasActionManifestMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateContainerEnvironment_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var legacyManager = new ActionManifestManagerLegacy();
|
|
legacyManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
|
|
|
var newManager = new ActionManifestManager();
|
|
newManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManager>(newManager);
|
|
|
|
var wrapper = new ActionManifestManagerWrapper();
|
|
wrapper.Initialize(_hc);
|
|
|
|
var environment = new MappingToken(null, null, null);
|
|
environment.Add(new StringToken(null, null, null, "hello"), new StringToken(null, null, null, "world"));
|
|
|
|
var evaluateContext = new Dictionary<string, LegacyContextData.PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var result = wrapper.EvaluateContainerEnvironment(_ec.Object, environment, evaluateContext);
|
|
|
|
Assert.Equal(1, result.Count);
|
|
Assert.Equal("world", result["hello"]);
|
|
Assert.False(_ec.Object.Global.HasActionManifestMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateCompositeOutputs_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var legacyManager = new ActionManifestManagerLegacy();
|
|
legacyManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
|
|
|
var newManager = new ActionManifestManager();
|
|
newManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManager>(newManager);
|
|
|
|
var wrapper = new ActionManifestManagerWrapper();
|
|
wrapper.Initialize(_hc);
|
|
|
|
var outputDef = new MappingToken(null, null, null);
|
|
outputDef.Add(new StringToken(null, null, null, "description"), new StringToken(null, null, null, "test output"));
|
|
outputDef.Add(new StringToken(null, null, null, "value"), new StringToken(null, null, null, "value1"));
|
|
|
|
var token = new MappingToken(null, null, null);
|
|
token.Add(new StringToken(null, null, null, "output1"), outputDef);
|
|
|
|
var evaluateContext = new Dictionary<string, LegacyContextData.PipelineContextData>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var result = wrapper.EvaluateCompositeOutputs(_ec.Object, token, evaluateContext);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.False(_ec.Object.Global.HasActionManifestMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void Load_BothParsersRejectInvalidExpressionContext()
|
|
{
|
|
try
|
|
{
|
|
// Arrange — regression test: both parsers must reject github context
|
|
// in container-runs-env (only inputs is allowed per schema)
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var legacyManager = new ActionManifestManagerLegacy();
|
|
legacyManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManagerLegacy>(legacyManager);
|
|
|
|
var newManager = new ActionManifestManager();
|
|
newManager.Initialize(_hc);
|
|
_hc.SetSingleton<IActionManifestManager>(newManager);
|
|
|
|
var wrapper = new ActionManifestManagerWrapper();
|
|
wrapper.Initialize(_hc);
|
|
|
|
var manifestPath = Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_env_invalid_context.yml");
|
|
|
|
// Act & Assert — both parsers should reject, wrapper should throw
|
|
Assert.Throws<ArgumentException>(() => wrapper.Load(_ec.Object, manifestPath));
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
private string GetFullExceptionMessage(Exception ex)
|
|
{
|
|
var messages = new List<string>();
|
|
var current = ex;
|
|
while (current != null)
|
|
{
|
|
messages.Add(current.Message);
|
|
current = current.InnerException;
|
|
}
|
|
return string.Join(" -> ", messages);
|
|
}
|
|
|
|
private void Setup([CallerMemberName] string name = "")
|
|
{
|
|
_ecTokenSource?.Dispose();
|
|
_ecTokenSource = new CancellationTokenSource();
|
|
|
|
_hc = new TestHostContext(this, name);
|
|
|
|
var expressionValues = new LegacyContextData.DictionaryContextData();
|
|
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
_ec = new Mock<IExecutionContext>();
|
|
_ec.Setup(x => x.Global)
|
|
.Returns(new GlobalContext
|
|
{
|
|
FileTable = new List<String>(),
|
|
Variables = new Variables(_hc, new Dictionary<string, VariableValue>()),
|
|
WriteDebug = true,
|
|
});
|
|
_ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token);
|
|
_ec.Setup(x => x.ExpressionValues).Returns(expressionValues);
|
|
_ec.Setup(x => x.ExpressionFunctions).Returns(expressionFunctions);
|
|
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"{tag}{message}"); });
|
|
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<ExecutionContextLogOptions>())).Callback((Issue issue, ExecutionContextLogOptions logOptions) => { _hc.GetTrace().Info($"[{issue.Type}]{logOptions.LogMessageOverride ?? issue.Message}"); });
|
|
}
|
|
|
|
private void Teardown()
|
|
{
|
|
_hc?.Dispose();
|
|
}
|
|
}
|
|
}
|