937 lines
38 KiB
C#
937 lines
38 KiB
C#
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 System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using Xunit;
|
|
|
|
namespace GitHub.Runner.Common.Tests.Worker
|
|
{
|
|
public sealed class PipelineTemplateEvaluatorWrapperL0
|
|
{
|
|
private CancellationTokenSource _ecTokenSource;
|
|
private CancellationTokenSource _rootTokenSource;
|
|
private Mock<IExecutionContext> _ec;
|
|
private TestHostContext _hc;
|
|
|
|
// -------------------------------------------------------------------
|
|
// EvaluateAndCompare core behavior
|
|
// -------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateAndCompare_DoesNotRecordMismatch_WhenResultsMatch()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
|
|
var token = new StringToken(null, null, null, "test-value");
|
|
var contextData = new DictionaryContextData();
|
|
var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);
|
|
|
|
Assert.Equal("test-value", result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateAndCompare_SkipsMismatchRecording_WhenCancellationOccursDuringEvaluation()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
|
|
// Call EvaluateAndCompare directly: the new evaluator cancels the root token
|
|
// and returns a different value, forcing hasMismatch = true.
|
|
// Because cancellation flipped during the evaluation window, the
|
|
// mismatch should be skipped.
|
|
var result = wrapper.EvaluateAndCompare<string, string>(
|
|
"TestCancellationSkip",
|
|
() => "legacy-value",
|
|
() =>
|
|
{
|
|
_rootTokenSource.Cancel();
|
|
return "different-value";
|
|
},
|
|
(legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));
|
|
|
|
Assert.Equal("legacy-value", result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateAndCompare_SkipsMismatchRecording_WhenRootCancellationOccursBetweenEvaluators()
|
|
{
|
|
// Simulates job-level cancellation firing between legacy and new evaluator runs.
|
|
// Root is mocked with a separate CancellationTokenSource to exercise the
|
|
// _context.Root?.CancellationToken path (the job-level token).
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
|
|
// Legacy evaluator cancels the root token (simulating job cancel) and returns a value.
|
|
// The new evaluator returns a different value. The mismatch should be skipped.
|
|
var result = wrapper.EvaluateAndCompare<string, string>(
|
|
"TestRootCancellationSkip",
|
|
() =>
|
|
{
|
|
var legacyValue = "legacy-value";
|
|
_rootTokenSource.Cancel();
|
|
return legacyValue;
|
|
},
|
|
() => "different-value",
|
|
(legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));
|
|
|
|
Assert.Equal("legacy-value", result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateAndCompare_RecordsMismatch_WhenResultsDifferWithoutCancellation()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
|
|
// Different results without cancellation — mismatch SHOULD be recorded.
|
|
var result = wrapper.EvaluateAndCompare<string, string>(
|
|
"TestMismatchRecorded",
|
|
() => "legacy-value",
|
|
() => "different-value",
|
|
(legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));
|
|
|
|
Assert.Equal("legacy-value", result);
|
|
Assert.True(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Smoke tests — both parsers agree, no mismatch recorded
|
|
// -------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateStepContinueOnError_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new BooleanToken(null, null, null, true);
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateStepContinueOnError(token, contextData, functions);
|
|
|
|
Assert.True(result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateStepEnvironment_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new MappingToken(null, null, null);
|
|
token.Add(new StringToken(null, null, null, "FOO"), new StringToken(null, null, null, "bar"));
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateStepEnvironment(token, contextData, functions, StringComparer.OrdinalIgnoreCase);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("bar", result["FOO"]);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateStepIf_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new BasicExpressionToken(null, null, null, "true");
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
var expressionState = new List<KeyValuePair<string, object>>();
|
|
|
|
var result = wrapper.EvaluateStepIf(token, contextData, functions, expressionState);
|
|
|
|
Assert.True(result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateStepInputs_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new MappingToken(null, null, null);
|
|
token.Add(new StringToken(null, null, null, "input1"), new StringToken(null, null, null, "val1"));
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateStepInputs(token, contextData, functions);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("val1", result["input1"]);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateStepTimeout_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new NumberToken(null, null, null, 10);
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateStepTimeout(token, contextData, functions);
|
|
|
|
Assert.Equal(10, result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobContainer_EmptyImage_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new StringToken(null, null, null, "");
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
|
|
|
|
Assert.Null(result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobContainer_DockerPrefixOnly_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new StringToken(null, null, null, "docker://");
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
|
|
|
|
Assert.Null(result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobContainer_DockerPrefixOnlyMapping_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new MappingToken(null, null, null);
|
|
token.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "docker://"));
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
|
|
|
|
Assert.Null(result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobContainer_EmptyImageMapping_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new MappingToken(null, null, null);
|
|
token.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, ""));
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
|
|
|
|
Assert.Null(result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobContainer_ValidImage_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new StringToken(null, null, null, "ubuntu:latest");
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("ubuntu:latest", result.Image);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobContainer_DockerPrefixWithImage_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new StringToken(null, null, null, "docker://ubuntu:latest");
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobContainer(token, contextData, functions);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("ubuntu:latest", result.Image);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobOutput_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new MappingToken(null, null, null);
|
|
token.Add(new StringToken(null, null, null, "out1"), new StringToken(null, null, null, "val1"));
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobOutput(token, contextData, functions);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("val1", result["out1"]);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateEnvironmentUrl_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new StringToken(null, null, null, "https://example.com");
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateEnvironmentUrl(token, contextData, functions);
|
|
|
|
Assert.NotNull(result);
|
|
var stringResult = result as StringToken;
|
|
Assert.NotNull(stringResult);
|
|
Assert.Equal("https://example.com", stringResult.Value);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobDefaultsRun_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var token = new MappingToken(null, null, null);
|
|
token.Add(new StringToken(null, null, null, "shell"), new StringToken(null, null, null, "bash"));
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobDefaultsRun(token, contextData, functions);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("bash", result["shell"]);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobServiceContainers_Null_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobServiceContainers(null, contextData, functions);
|
|
|
|
Assert.Null(result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobServiceContainers_EmptyImage_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
// Build a services mapping token with one service whose image is empty string
|
|
// Similar to: services: { db: { image: '' } }
|
|
var servicesMapping = new MappingToken(null, null, null);
|
|
var serviceMapping = new MappingToken(null, null, null);
|
|
serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, ""));
|
|
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);
|
|
|
|
// Should get a list with one entry where the container is null (empty image = no container)
|
|
Assert.NotNull(result);
|
|
Assert.Single(result);
|
|
Assert.Equal("db", result[0].Key);
|
|
Assert.Null(result[0].Value);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobServiceContainers_DockerPrefixOnlyImage_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var servicesMapping = new MappingToken(null, null, null);
|
|
var serviceMapping = new MappingToken(null, null, null);
|
|
serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "docker://"));
|
|
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Single(result);
|
|
Assert.Equal("db", result[0].Key);
|
|
Assert.Null(result[0].Value);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobServiceContainers_ExpressionEvalsToEmpty_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
// Simulates: services: { db: { image: ${{ condition && 'img' || '' }} } }
|
|
// where the expression evaluates to '' at runtime
|
|
var servicesMapping = new MappingToken(null, null, null);
|
|
var serviceMapping = new MappingToken(null, null, null);
|
|
serviceMapping.Add(new StringToken(null, null, null, "image"), new BasicExpressionToken(null, null, null, "''"));
|
|
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Single(result);
|
|
Assert.Equal("db", result[0].Key);
|
|
Assert.Null(result[0].Value);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobServiceContainers_ValidImage_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var servicesMapping = new MappingToken(null, null, null);
|
|
var serviceMapping = new MappingToken(null, null, null);
|
|
serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "postgres:latest"));
|
|
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Single(result);
|
|
Assert.Equal("db", result[0].Key);
|
|
Assert.NotNull(result[0].Value);
|
|
Assert.Equal("postgres:latest", result[0].Value.Image);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobServiceContainers_EntrypointAndCommand_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var servicesMapping = new MappingToken(null, null, null);
|
|
var serviceMapping = new MappingToken(null, null, null);
|
|
serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "postgres:latest"));
|
|
serviceMapping.Add(new StringToken(null, null, null, "entrypoint"), new StringToken(null, null, null, "/bin/bash"));
|
|
serviceMapping.Add(new StringToken(null, null, null, "command"), new StringToken(null, null, null, "-lc echo hi"));
|
|
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: true);
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Single(result);
|
|
Assert.Equal("db", result[0].Key);
|
|
Assert.NotNull(result[0].Value);
|
|
Assert.Equal("postgres:latest", result[0].Value.Image);
|
|
Assert.Equal("/bin/bash", result[0].Value.Entrypoint);
|
|
Assert.Equal("-lc echo hi", result[0].Value.Command);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobServiceContainers_EntrypointAndCommand_FlagOff_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var servicesMapping = new MappingToken(null, null, null);
|
|
var serviceMapping = new MappingToken(null, null, null);
|
|
serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "postgres:latest"));
|
|
serviceMapping.Add(new StringToken(null, null, null, "entrypoint"), new StringToken(null, null, null, "/bin/bash"));
|
|
serviceMapping.Add(new StringToken(null, null, null, "command"), new StringToken(null, null, null, "-lc echo hi"));
|
|
servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
Assert.Throws<GitHub.DistributedTask.ObjectTemplating.TemplateValidationException>(() =>
|
|
wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions));
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateJobSnapshotRequest_Null_BothParsersAgree()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
var contextData = new DictionaryContextData();
|
|
var functions = new List<LegacyExpressions.IFunctionInfo>();
|
|
|
|
var result = wrapper.EvaluateJobSnapshotRequest(null, contextData, functions);
|
|
|
|
Assert.Null(result);
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// JSON parse error equivalence via EvaluateAndCompare
|
|
// -------------------------------------------------------------------
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateAndCompare_JsonReaderExceptions_TreatedAsEquivalent()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
|
|
// Both throw JsonReaderException with different messages — should be treated as equivalent
|
|
var legacyEx = new Newtonsoft.Json.JsonReaderException("Error reading JToken from JsonReader. Path '', line 0, position 0.");
|
|
var newEx = new Newtonsoft.Json.JsonReaderException("Error parsing fromJson", new Newtonsoft.Json.JsonReaderException("Unexpected end"));
|
|
|
|
Assert.Throws<Newtonsoft.Json.JsonReaderException>(() =>
|
|
wrapper.EvaluateAndCompare<string, string>(
|
|
"TestJsonEquivalence",
|
|
() => throw legacyEx,
|
|
() => throw newEx,
|
|
(a, b) => string.Equals(a, b, StringComparison.Ordinal)));
|
|
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateAndCompare_MixedJsonExceptionTypes_TreatedAsEquivalent()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
|
|
// Legacy throws Newtonsoft JsonReaderException, new throws System.Text.Json.JsonException
|
|
var legacyEx = new Newtonsoft.Json.JsonReaderException("Error reading JToken");
|
|
var newEx = new System.Text.Json.JsonException("Error parsing fromJson");
|
|
|
|
Assert.Throws<Newtonsoft.Json.JsonReaderException>(() =>
|
|
wrapper.EvaluateAndCompare<string, string>(
|
|
"TestMixedJsonTypes",
|
|
() => throw legacyEx,
|
|
() => throw newEx,
|
|
(a, b) => string.Equals(a, b, StringComparison.Ordinal)));
|
|
|
|
Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
[Trait("Level", "L0")]
|
|
[Trait("Category", "Worker")]
|
|
public void EvaluateAndCompare_NonJsonExceptions_RecordsMismatch()
|
|
{
|
|
try
|
|
{
|
|
Setup();
|
|
_ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");
|
|
|
|
var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
|
|
|
|
// Both throw non-JSON exceptions with different messages — should record mismatch
|
|
var legacyEx = new InvalidOperationException("some error");
|
|
var newEx = new InvalidOperationException("different error");
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
wrapper.EvaluateAndCompare<string, string>(
|
|
"TestNonJsonMismatch",
|
|
() => throw legacyEx,
|
|
() => throw newEx,
|
|
(a, b) => string.Equals(a, b, StringComparison.Ordinal)));
|
|
|
|
Assert.True(_ec.Object.Global.HasTemplateEvaluatorMismatch);
|
|
}
|
|
finally
|
|
{
|
|
Teardown();
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// Helpers
|
|
// -------------------------------------------------------------------
|
|
|
|
private void Setup([CallerMemberName] string name = "")
|
|
{
|
|
_ecTokenSource?.Dispose();
|
|
_ecTokenSource = new CancellationTokenSource();
|
|
_rootTokenSource?.Dispose();
|
|
_rootTokenSource = 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);
|
|
var rootEc = new Mock<IExecutionContext>();
|
|
rootEc.Setup(x => x.CancellationToken).Returns(_rootTokenSource.Token);
|
|
_ec.Setup(x => x.Root).Returns(rootEc.Object);
|
|
_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()
|
|
{
|
|
_ecTokenSource?.Dispose();
|
|
_rootTokenSource?.Dispose();
|
|
_hc?.Dispose();
|
|
}
|
|
}
|
|
}
|