Fixed a bug where a misplaced = character could bypass heredoc-style processing. (#2627)

* Fixed a bug where a misplaced `=` character could bypass heredoc-style processing.

Fixes https://github.com/github/c2c-actions/issues/6910

GitHub Docs for context:  https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings

* Consolidate near-identical FileCommand-related unit test classes. (#2672)
This commit is contained in:
John Wesley Walker III
2023-06-29 12:52:05 +02:00
committed by GitHub
parent c05e6748c3
commit 4ffd081aea
7 changed files with 659 additions and 1008 deletions

View File

@@ -322,21 +322,9 @@ namespace GitHub.Runner.Worker
var equalsIndex = line.IndexOf("=", StringComparison.Ordinal);
var heredocIndex = line.IndexOf("<<", StringComparison.Ordinal);
// Normal style NAME=VALUE
if (equalsIndex >= 0 && (heredocIndex < 0 || equalsIndex < heredocIndex))
{
var split = line.Split(new[] { '=' }, 2, StringSplitOptions.None);
if (string.IsNullOrEmpty(line))
{
throw new Exception($"Invalid format '{line}'. Name must not be empty");
}
key = split[0];
output = split[1];
}
// Heredoc style NAME<<EOF
else if (heredocIndex >= 0 && (equalsIndex < 0 || heredocIndex < equalsIndex))
// Heredoc style NAME<<EOF (where EOF is typically randomly-generated Base64 and may include an '=' character)
// see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
if (heredocIndex >= 0 && (equalsIndex < 0 || heredocIndex < equalsIndex))
{
var split = line.Split(new[] { "<<" }, 2, StringSplitOptions.None);
if (string.IsNullOrEmpty(split[0]) || string.IsNullOrEmpty(split[1]))
@@ -364,6 +352,18 @@ namespace GitHub.Runner.Worker
output = endIndex > startIndex ? text.Substring(startIndex, endIndex - startIndex) : string.Empty;
}
// Normal style NAME=VALUE
else if (equalsIndex >= 0 && heredocIndex < 0)
{
var split = line.Split(new[] { '=' }, 2, StringSplitOptions.None);
if (string.IsNullOrEmpty(line))
{
throw new Exception($"Invalid format '{line}'. Name must not be empty");
}
key = split[0];
output = split[1];
}
else
{
throw new Exception($"Invalid format '{line}'");

View File

@@ -1,10 +1,21 @@
using System.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using Xunit;
using GitHub.Runner.Sdk;
using System.Runtime.CompilerServices;
using System.Linq;
namespace GitHub.Runner.Common.Tests
{
public enum LineEndingType
{
Native,
Linux = 0x__0A,
Windows = 0x0D0A
}
public static class TestUtil
{
private const string Src = "src";
@@ -41,5 +52,24 @@ namespace GitHub.Runner.Common.Tests
Assert.True(Directory.Exists(testDataDir));
return testDataDir;
}
public static void WriteContent(string path, string content, LineEndingType lineEnding = LineEndingType.Native)
{
WriteContent(path, Enumerable.Repeat(content, 1), lineEnding);
}
public static void WriteContent(string path, IEnumerable<string> content, LineEndingType lineEnding = LineEndingType.Native)
{
string newline = lineEnding switch
{
LineEndingType.Linux => "\n",
LineEndingType.Windows => "\r\n",
_ => Environment.NewLine,
};
var encoding = new UTF8Encoding(true); // Emit BOM
var contentStr = string.Join(newline, content);
File.WriteAllText(path, contentStr, encoding);
}
}
}

View File

@@ -124,7 +124,7 @@ namespace GitHub.Runner.Common.Tests.Worker
"",
"## This is more markdown content",
};
WriteContent(stepSummaryFile, content);
TestUtil.WriteContent(stepSummaryFile, content);
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
_jobExecutionContext.Complete();
@@ -153,7 +153,7 @@ namespace GitHub.Runner.Common.Tests.Worker
"",
"# GITHUB_TOKEN ghs_verysecuretoken",
};
WriteContent(stepSummaryFile, content);
TestUtil.WriteContent(stepSummaryFile, content);
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
@@ -167,21 +167,6 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
private void WriteContent(
string path,
List<string> content,
string newline = null)
{
if (string.IsNullOrEmpty(newline))
{
newline = Environment.NewLine;
}
var encoding = new UTF8Encoding(true); // Emit BOM
var contentStr = string.Join(newline, content);
File.WriteAllText(path, contentStr, encoding);
}
private TestHostContext Setup([CallerMemberName] string name = "")
{
var hostContext = new TestHostContext(this, name);

View File

@@ -0,0 +1,420 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using Moq;
using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
namespace GitHub.Runner.Common.Tests.Worker
{
public abstract class FileCommandTestBase<T>
where T : IFileCommandExtension, new()
{
protected void TestDirectoryNotFound()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "directory-not-found", "env");
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _store.Count);
}
}
protected void TestNotFound()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "file-not-found");
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _store.Count);
}
}
protected void TestEmptyFile()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "empty-file");
var content = new List<string>();
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _store.Count);
}
}
protected void TestSimple()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_KEY=MY VALUE",
};
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _store.Count);
Assert.Equal("MY VALUE", _store["MY_KEY"]);
}
}
protected void TestSimple_SkipEmptyLines()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
string.Empty,
"MY_KEY=my value",
string.Empty,
"MY_KEY_2=my second value",
string.Empty,
};
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(2, _store.Count);
Assert.Equal("my value", _store["MY_KEY"]);
Assert.Equal("my second value", _store["MY_KEY_2"]);
}
}
protected void TestSimple_EmptyValue()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple-empty-value");
var content = new List<string>
{
"MY_KEY=",
};
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _store.Count);
Assert.Equal(string.Empty, _store["MY_KEY"]);
}
}
protected void TestSimple_MultipleValues()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_KEY=my value",
"MY_KEY_2=",
"MY_KEY_3=my third value",
};
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(3, _store.Count);
Assert.Equal("my value", _store["MY_KEY"]);
Assert.Equal(string.Empty, _store["MY_KEY_2"]);
Assert.Equal("my third value", _store["MY_KEY_3"]);
}
}
protected void TestSimple_SpecialCharacters()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_KEY==abc",
"MY_KEY_2=def=ghi",
"MY_KEY_3=jkl=",
};
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(3, _store.Count);
Assert.Equal("=abc", _store["MY_KEY"]);
Assert.Equal("def=ghi", _store["MY_KEY_2"]);
Assert.Equal("jkl=", _store["MY_KEY_3"]);
}
}
protected void TestHeredoc()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_KEY<<EOF",
"line one",
"line two",
"line three",
"EOF",
};
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _store.Count);
Assert.Equal($"line one{BREAK}line two{BREAK}line three", _store["MY_KEY"]);
}
}
protected void TestHeredoc_EmptyValue()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_KEY<<EOF",
"EOF",
};
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _store.Count);
Assert.Equal(string.Empty, _store["MY_KEY"]);
}
}
protected void TestHeredoc_SkipEmptyLines()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
string.Empty,
"MY_KEY<<EOF",
"hello",
"world",
"EOF",
string.Empty,
"MY_KEY_2<<EOF",
"HELLO",
"AGAIN",
"EOF",
string.Empty,
};
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(2, _store.Count);
Assert.Equal($"hello{BREAK}world", _store["MY_KEY"]);
Assert.Equal($"HELLO{BREAK}AGAIN", _store["MY_KEY_2"]);
}
}
protected void TestHeredoc_EdgeCases()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_KEY_1<<EOF",
"hello",
string.Empty,
"three",
string.Empty,
"EOF",
"MY_KEY_2<<EOF",
"hello=two",
"EOF",
"MY_KEY_3<<EOF",
" EOF",
"EOF",
"MY_KEY_4<<EOF",
"EOF EOF",
"EOF",
};
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(4, _store.Count);
Assert.Equal($"hello{BREAK}{BREAK}three{BREAK}", _store["MY_KEY_1"]);
Assert.Equal($"hello=two", _store["MY_KEY_2"]);
Assert.Equal($" EOF", _store["MY_KEY_3"]);
Assert.Equal($"EOF EOF", _store["MY_KEY_4"]);
}
}
protected void TestHeredoc_EndMarkerVariations(string validEndMarker)
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
string eof = validEndMarker;
var content = new List<string>
{
$"MY_KEY_1<<{eof}",
$"hello",
$"one",
$"{eof}",
$"MY_KEY_2<<{eof}",
$"hello=two",
$"{eof}",
$"MY_KEY_3<<{eof}",
$" {eof}",
$"{eof}",
$"MY_KEY_4<<{eof}",
$"{eof} {eof}",
$"{eof}",
};
TestUtil.WriteContent(stateFile, content);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(4, _store.Count);
Assert.Equal($"hello{BREAK}one", _store["MY_KEY_1"]);
Assert.Equal($"hello=two", _store["MY_KEY_2"]);
Assert.Equal($" {eof}", _store["MY_KEY_3"]);
Assert.Equal($"{eof} {eof}", _store["MY_KEY_4"]);
}
}
protected void TestHeredoc_EqualBeforeMultilineIndicator()
{
using var hostContext = Setup();
var stateFile = Path.Combine(_rootDirectory, "heredoc");
// Define a hypothetical injectable payload that just happens to contain the '=' character.
string contrivedGitHubIssueTitle = "Issue 999: Better handling for the `=` character";
// The docs recommend using randomly-generated EOF markers.
// Here's a randomly-generated base64 EOF marker that just happens to contain an '=' character. ('=' is a padding character in base64.)
// see https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
string randomizedEOF = "khkIhPxsVA==";
var content = new List<string>
{
// In a real world scenario, "%INJECT%" might instead be something like "${{ github.event.issue.title }}"
$"PREFIX_%INJECT%<<{randomizedEOF}".Replace("%INJECT%", contrivedGitHubIssueTitle),
"RandomDataThatJustHappensToContainAnEquals=Character",
randomizedEOF,
};
TestUtil.WriteContent(stateFile, content);
var ex = Assert.Throws<Exception>(() => _fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null));
Assert.StartsWith("Invalid format", ex.Message);
}
protected void TestHeredoc_MissingNewLine()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
string content = "MY_KEY<<EOF line one line two line three EOF";
TestUtil.WriteContent(stateFile, content);
var ex = Assert.Throws<Exception>(() => _fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null));
Assert.Contains("Matching delimiter not found", ex.Message);
}
}
protected void TestHeredoc_MissingNewLineMultipleLines()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
string multilineFragment = @"line one
line two
line three";
// Note that the final EOF does not appear on it's own line.
string content = $"MY_KEY<<EOF {multilineFragment} EOF";
TestUtil.WriteContent(stateFile, content);
var ex = Assert.Throws<Exception>(() => _fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null));
Assert.Contains("EOF marker missing new line", ex.Message);
}
}
protected void TestHeredoc_PreservesNewline()
{
using (var hostContext = Setup())
{
var newline = "\n";
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_KEY<<EOF",
"hello",
"world",
"EOF",
};
TestUtil.WriteContent(stateFile, content, LineEndingType.Linux);
_fileCmdExtension.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _store.Count);
Assert.Equal($"hello{newline}world", _store["MY_KEY"]);
}
}
protected TestHostContext Setup([CallerMemberName] string name = "")
{
_issues = new List<Tuple<DTWebApi.Issue, string>>();
var hostContext = new TestHostContext(this, name);
// Trace
_trace = hostContext.GetTrace();
// Directory for test data
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
Directory.CreateDirectory(workDirectory);
_rootDirectory = Path.Combine(workDirectory, nameof(T));
Directory.CreateDirectory(_rootDirectory);
// Execution context
_executionContext = new Mock<IExecutionContext>();
_executionContext.Setup(x => x.Global)
.Returns(new GlobalContext
{
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
{
var resolvedMessage = issue.Message;
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
resolvedMessage = logOptions.LogMessageOverride;
}
_issues.Add(new(issue, resolvedMessage));
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>
{
_trace.Info($"{tag}{message}");
});
_store = PostSetup();
_fileCmdExtension = new T();
_fileCmdExtension.Initialize(hostContext);
return hostContext;
}
protected abstract IDictionary<string, string> PostSetup();
protected static readonly string BREAK = Environment.NewLine;
protected IFileCommandExtension _fileCmdExtension { get; private set; }
protected Mock<IExecutionContext> _executionContext { get; private set; }
protected List<Tuple<DTWebApi.Issue, string>> _issues { get; private set; }
protected IDictionary<string, string> _store { get; private set; }
protected string _rootDirectory { get; private set; }
protected ITraceWriter _trace { get; private set; }
}
}

View File

@@ -1,44 +1,27 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class SaveStateFileCommandL0
public sealed class SaveStateFileCommandL0 : FileCommandTestBase<SaveStateFileCommand>
{
private Mock<IExecutionContext> _executionContext;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private string _rootDirectory;
private SaveStateFileCommand _saveStateFileCommand;
private Dictionary<string, string> _intraActionState;
private ITraceWriter _trace;
protected override IDictionary<string, string> PostSetup()
{
var intraActionState = new Dictionary<string, string>();
_executionContext.Setup(x => x.IntraActionState).Returns(intraActionState);
return intraActionState;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SaveStateFileCommand_DirectoryNotFound()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "directory-not-found", "env");
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _intraActionState.Count);
}
base.TestDirectoryNotFound();
}
[Fact]
@@ -46,13 +29,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_NotFound()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "file-not-found");
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _intraActionState.Count);
}
base.TestNotFound();
}
[Fact]
@@ -60,15 +37,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_EmptyFile()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "empty-file");
var content = new List<string>();
WriteContent(stateFile, content);
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _intraActionState.Count);
}
base.TestEmptyFile();
}
[Fact]
@@ -76,19 +45,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_STATE=MY VALUE",
};
WriteContent(stateFile, content);
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _intraActionState.Count);
Assert.Equal("MY VALUE", _intraActionState["MY_STATE"]);
}
base.TestSimple();
}
[Fact]
@@ -96,24 +53,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_SkipEmptyLines()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
string.Empty,
"MY_STATE=my value",
string.Empty,
"MY_STATE_2=my second value",
string.Empty,
};
WriteContent(stateFile, content);
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(2, _intraActionState.Count);
Assert.Equal("my value", _intraActionState["MY_STATE"]);
Assert.Equal("my second value", _intraActionState["MY_STATE_2"]);
}
base.TestSimple_SkipEmptyLines();
}
[Fact]
@@ -121,19 +61,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_EmptyValue()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple-empty-value");
var content = new List<string>
{
"MY_STATE=",
};
WriteContent(stateFile, content);
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _intraActionState.Count);
Assert.Equal(string.Empty, _intraActionState["MY_STATE"]);
}
base.TestSimple_EmptyValue();
}
[Fact]
@@ -141,23 +69,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_MultipleValues()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_STATE=my value",
"MY_STATE_2=",
"MY_STATE_3=my third value",
};
WriteContent(stateFile, content);
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(3, _intraActionState.Count);
Assert.Equal("my value", _intraActionState["MY_STATE"]);
Assert.Equal(string.Empty, _intraActionState["MY_STATE_2"]);
Assert.Equal("my third value", _intraActionState["MY_STATE_3"]);
}
base.TestSimple_MultipleValues();
}
[Fact]
@@ -165,23 +77,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_SpecialCharacters()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_STATE==abc",
"MY_STATE_2=def=ghi",
"MY_STATE_3=jkl=",
};
WriteContent(stateFile, content);
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(3, _intraActionState.Count);
Assert.Equal("=abc", _intraActionState["MY_STATE"]);
Assert.Equal("def=ghi", _intraActionState["MY_STATE_2"]);
Assert.Equal("jkl=", _intraActionState["MY_STATE_3"]);
}
base.TestSimple_SpecialCharacters();
}
[Fact]
@@ -189,23 +85,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_STATE<<EOF",
"line one",
"line two",
"line three",
"EOF",
};
WriteContent(stateFile, content);
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _intraActionState.Count);
Assert.Equal($"line one{Environment.NewLine}line two{Environment.NewLine}line three", _intraActionState["MY_STATE"]);
}
base.TestHeredoc();
}
[Fact]
@@ -213,20 +93,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_EmptyValue()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_STATE<<EOF",
"EOF",
};
WriteContent(stateFile, content);
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _intraActionState.Count);
Assert.Equal(string.Empty, _intraActionState["MY_STATE"]);
}
base.TestHeredoc_EmptyValue();
}
[Fact]
@@ -234,73 +101,52 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_SkipEmptyLines()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
string.Empty,
"MY_STATE<<EOF",
"hello",
"world",
"EOF",
string.Empty,
"MY_STATE_2<<EOF",
"HELLO",
"AGAIN",
"EOF",
string.Empty,
};
WriteContent(stateFile, content);
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(2, _intraActionState.Count);
Assert.Equal($"hello{Environment.NewLine}world", _intraActionState["MY_STATE"]);
Assert.Equal($"HELLO{Environment.NewLine}AGAIN", _intraActionState["MY_STATE_2"]);
}
base.TestHeredoc_SkipEmptyLines();
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_SpecialCharacters()
public void SaveStateFileCommand_Heredoc_EdgeCases()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_STATE<<=EOF",
"hello",
"one",
"=EOF",
"MY_STATE_2<<<EOF",
"hello",
"two",
"<EOF",
"MY_STATE_3<<EOF",
"hello",
string.Empty,
"three",
string.Empty,
"EOF",
"MY_STATE_4<<EOF",
"hello=four",
"EOF",
"MY_STATE_5<<EOF",
" EOF",
"EOF",
};
WriteContent(stateFile, content);
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(5, _intraActionState.Count);
Assert.Equal($"hello{Environment.NewLine}one", _intraActionState["MY_STATE"]);
Assert.Equal($"hello{Environment.NewLine}two", _intraActionState["MY_STATE_2"]);
Assert.Equal($"hello{Environment.NewLine}{Environment.NewLine}three{Environment.NewLine}", _intraActionState["MY_STATE_3"]);
Assert.Equal($"hello=four", _intraActionState["MY_STATE_4"]);
Assert.Equal($" EOF", _intraActionState["MY_STATE_5"]);
}
base.TestHeredoc_EdgeCases();
}
[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
// All of the following are not only valid, but quite plausible end markers.
// Most are derived straight from the example at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
#pragma warning disable format
[InlineData("=EOF")][InlineData("==EOF")][InlineData("EO=F")][InlineData("EO==F")][InlineData("EOF=")][InlineData("EOF==")]
[InlineData("<EOF")][InlineData("<<EOF")][InlineData("EO<F")][InlineData("EO<<F")][InlineData("EOF<")][InlineData("EOF<<")]
[InlineData("+EOF")][InlineData("++EOF")][InlineData("EO+F")][InlineData("EO++F")][InlineData("EOF+")][InlineData("EOF++")]
[InlineData("/EOF")][InlineData("//EOF")][InlineData("EO/F")][InlineData("EO//F")][InlineData("EOF/")][InlineData("EOF//")]
#pragma warning restore format
[InlineData("<<//++==")]
[InlineData("contrivedBase64==")]
[InlineData("khkIhPxsVA==")]
[InlineData("D+Y8zE/EOw==")]
[InlineData("wuOWG4S6FQ==")]
[InlineData("7wigCJ//iw==")]
[InlineData("uifTuYTs8K4=")]
[InlineData("M7N2ITg/04c=")]
[InlineData("Xhh+qp+Y6iM=")]
[InlineData("5tdblQajc/b+EGBZXo0w")]
[InlineData("jk/UMjIx/N0eVcQYOUfw")]
[InlineData("/n5lsw73Cwl35Hfuscdz")]
[InlineData("ZvnAEW+9O0tXp3Fmb3Oh")]
public void SaveStateFileCommand_Heredoc_EndMarkerVariations(string validEndMarker)
{
base.TestHeredoc_EndMarkerVariations(validEndMarker);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_EqualBeforeMultilineIndicator()
{
base.TestHeredoc_EqualBeforeMultilineIndicator();
}
[Fact]
@@ -308,21 +154,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_MissingNewLine()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_STATE<<EOF",
"line one",
"line two",
"line three",
"EOF",
};
WriteContent(stateFile, content, " ");
var ex = Assert.Throws<Exception>(() => _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null));
Assert.Contains("Matching delimiter not found", ex.Message);
}
base.TestHeredoc_MissingNewLine();
}
[Fact]
@@ -330,21 +162,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_MissingNewLineMultipleLines()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_STATE<<EOF",
@"line one
line two
line three",
"EOF",
};
WriteContent(stateFile, content, " ");
var ex = Assert.Throws<Exception>(() => _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null));
Assert.Contains("EOF marker missing new line", ex.Message);
}
base.TestHeredoc_MissingNewLineMultipleLines();
}
#if OS_WINDOWS
@@ -353,90 +171,9 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_PreservesNewline()
{
using (var hostContext = Setup())
{
var newline = "\n";
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_STATE<<EOF",
"hello",
"world",
"EOF",
};
WriteContent(stateFile, content, newline: newline);
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _intraActionState.Count);
Assert.Equal($"hello{newline}world", _intraActionState["MY_STATE"]);
}
base.TestHeredoc_PreservesNewline();
}
#endif
private void WriteContent(
string path,
List<string> content,
string newline = null)
{
if (string.IsNullOrEmpty(newline))
{
newline = Environment.NewLine;
}
var encoding = new UTF8Encoding(true); // Emit BOM
var contentStr = string.Join(newline, content);
File.WriteAllText(path, contentStr, encoding);
}
private TestHostContext Setup([CallerMemberName] string name = "")
{
_issues = new List<Tuple<DTWebApi.Issue, string>>();
_intraActionState = new Dictionary<string, string>();
var hostContext = new TestHostContext(this, name);
// Trace
_trace = hostContext.GetTrace();
// Directory for test data
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
Directory.CreateDirectory(workDirectory);
_rootDirectory = Path.Combine(workDirectory, nameof(SaveStateFileCommandL0));
Directory.CreateDirectory(_rootDirectory);
// Execution context
_executionContext = new Mock<IExecutionContext>();
_executionContext.Setup(x => x.Global)
.Returns(new GlobalContext
{
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
{
var resolvedMessage = issue.Message;
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
resolvedMessage = logOptions.LogMessageOverride;
}
_issues.Add(new(issue, resolvedMessage));
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>
{
_trace.Info($"{tag}{message}");
});
_executionContext.Setup(x => x.IntraActionState)
.Returns(_intraActionState);
// SaveStateFileCommand
_saveStateFileCommand = new SaveStateFileCommand();
_saveStateFileCommand.Initialize(hostContext);
return hostContext;
}
}
}

View File

@@ -1,43 +1,25 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class SetEnvFileCommandL0
public sealed class SetEnvFileCommandL0 : FileCommandTestBase<SetEnvFileCommand>
{
private Mock<IExecutionContext> _executionContext;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private string _rootDirectory;
private SetEnvFileCommand _setEnvFileCommand;
private ITraceWriter _trace;
protected override IDictionary<string, string> PostSetup()
{
return _executionContext.Object.Global.EnvironmentVariables;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetEnvFileCommand_DirectoryNotFound()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "directory-not-found", "env");
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _executionContext.Object.Global.EnvironmentVariables.Count);
}
base.TestDirectoryNotFound();
}
[Fact]
@@ -45,13 +27,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_NotFound()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "file-not-found");
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _executionContext.Object.Global.EnvironmentVariables.Count);
}
base.TestNotFound();
}
[Fact]
@@ -59,15 +35,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_EmptyFile()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "empty-file");
var content = new List<string>();
WriteContent(envFile, content);
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _executionContext.Object.Global.EnvironmentVariables.Count);
}
base.TestEmptyFile();
}
[Fact]
@@ -75,19 +43,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_ENV=MY VALUE",
};
WriteContent(envFile, content);
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count);
Assert.Equal("MY VALUE", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
}
base.TestSimple();
}
[Fact]
@@ -95,24 +51,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_SkipEmptyLines()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
string.Empty,
"MY_ENV=my value",
string.Empty,
"MY_ENV_2=my second value",
string.Empty,
};
WriteContent(envFile, content);
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(2, _executionContext.Object.Global.EnvironmentVariables.Count);
Assert.Equal("my value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
Assert.Equal("my second value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]);
}
base.TestSimple_SkipEmptyLines();
}
[Fact]
@@ -120,19 +59,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_EmptyValue()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "simple-empty-value");
var content = new List<string>
{
"MY_ENV=",
};
WriteContent(envFile, content);
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count);
Assert.Equal(string.Empty, _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
}
base.TestSimple_EmptyValue();
}
[Fact]
@@ -140,23 +67,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_MultipleValues()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_ENV=my value",
"MY_ENV_2=",
"MY_ENV_3=my third value",
};
WriteContent(envFile, content);
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(3, _executionContext.Object.Global.EnvironmentVariables.Count);
Assert.Equal("my value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
Assert.Equal(string.Empty, _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]);
Assert.Equal("my third value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_3"]);
}
base.TestSimple_MultipleValues();
}
[Fact]
@@ -164,23 +75,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_SpecialCharacters()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_ENV==abc",
"MY_ENV_2=def=ghi",
"MY_ENV_3=jkl=",
};
WriteContent(envFile, content);
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(3, _executionContext.Object.Global.EnvironmentVariables.Count);
Assert.Equal("=abc", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
Assert.Equal("def=ghi", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]);
Assert.Equal("jkl=", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_3"]);
}
base.TestSimple_SpecialCharacters();
}
[Fact]
@@ -188,23 +83,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_ENV<<EOF",
"line one",
"line two",
"line three",
"EOF",
};
WriteContent(envFile, content);
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count);
Assert.Equal($"line one{Environment.NewLine}line two{Environment.NewLine}line three", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
}
base.TestHeredoc();
}
[Fact]
@@ -212,20 +91,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_EmptyValue()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_ENV<<EOF",
"EOF",
};
WriteContent(envFile, content);
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count);
Assert.Equal(string.Empty, _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
}
base.TestHeredoc_EmptyValue();
}
[Fact]
@@ -233,73 +99,52 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_SkipEmptyLines()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
string.Empty,
"MY_ENV<<EOF",
"hello",
"world",
"EOF",
string.Empty,
"MY_ENV_2<<EOF",
"HELLO",
"AGAIN",
"EOF",
string.Empty,
};
WriteContent(envFile, content);
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(2, _executionContext.Object.Global.EnvironmentVariables.Count);
Assert.Equal($"hello{Environment.NewLine}world", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
Assert.Equal($"HELLO{Environment.NewLine}AGAIN", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]);
}
base.TestHeredoc_SkipEmptyLines();
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_SpecialCharacters()
public void SetEnvFileCommand_Heredoc_EdgeCases()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_ENV<<=EOF",
"hello",
"one",
"=EOF",
"MY_ENV_2<<<EOF",
"hello",
"two",
"<EOF",
"MY_ENV_3<<EOF",
"hello",
string.Empty,
"three",
string.Empty,
"EOF",
"MY_ENV_4<<EOF",
"hello=four",
"EOF",
"MY_ENV_5<<EOF",
" EOF",
"EOF",
};
WriteContent(envFile, content);
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(5, _executionContext.Object.Global.EnvironmentVariables.Count);
Assert.Equal($"hello{Environment.NewLine}one", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
Assert.Equal($"hello{Environment.NewLine}two", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]);
Assert.Equal($"hello{Environment.NewLine}{Environment.NewLine}three{Environment.NewLine}", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_3"]);
Assert.Equal($"hello=four", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_4"]);
Assert.Equal($" EOF", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_5"]);
}
base.TestHeredoc_EdgeCases();
}
[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
// All of the following are not only valid, but quite plausible end markers.
// Most are derived straight from the example at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
#pragma warning disable format
[InlineData("=EOF")][InlineData("==EOF")][InlineData("EO=F")][InlineData("EO==F")][InlineData("EOF=")][InlineData("EOF==")]
[InlineData("<EOF")][InlineData("<<EOF")][InlineData("EO<F")][InlineData("EO<<F")][InlineData("EOF<")][InlineData("EOF<<")]
[InlineData("+EOF")][InlineData("++EOF")][InlineData("EO+F")][InlineData("EO++F")][InlineData("EOF+")][InlineData("EOF++")]
[InlineData("/EOF")][InlineData("//EOF")][InlineData("EO/F")][InlineData("EO//F")][InlineData("EOF/")][InlineData("EOF//")]
#pragma warning restore format
[InlineData("<<//++==")]
[InlineData("contrivedBase64==")]
[InlineData("khkIhPxsVA==")]
[InlineData("D+Y8zE/EOw==")]
[InlineData("wuOWG4S6FQ==")]
[InlineData("7wigCJ//iw==")]
[InlineData("uifTuYTs8K4=")]
[InlineData("M7N2ITg/04c=")]
[InlineData("Xhh+qp+Y6iM=")]
[InlineData("5tdblQajc/b+EGBZXo0w")]
[InlineData("jk/UMjIx/N0eVcQYOUfw")]
[InlineData("/n5lsw73Cwl35Hfuscdz")]
[InlineData("ZvnAEW+9O0tXp3Fmb3Oh")]
public void SetEnvFileCommand_Heredoc_EndMarkerVariations(string validEndMarker)
{
base.TestHeredoc_EndMarkerVariations(validEndMarker);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_EqualBeforeMultilineIndicator()
{
base.TestHeredoc_EqualBeforeMultilineIndicator();
}
[Fact]
@@ -307,43 +152,15 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_MissingNewLine()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_ENV<<EOF",
"line one",
"line two",
"line three",
"EOF",
};
WriteContent(envFile, content, " ");
var ex = Assert.Throws<Exception>(() => _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null));
Assert.Contains("Matching delimiter not found", ex.Message);
}
base.TestHeredoc_MissingNewLine();
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_MissingNewLineMultipleLinesEnv()
public void SetEnvFileCommand_Heredoc_MissingNewLineMultipleLines()
{
using (var hostContext = Setup())
{
var envFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_ENV<<EOF",
@"line one
line two
line three",
"EOF",
};
WriteContent(envFile, content, " ");
var ex = Assert.Throws<Exception>(() => _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null));
Assert.Contains("EOF marker missing new line", ex.Message);
}
base.TestHeredoc_MissingNewLineMultipleLines();
}
#if OS_WINDOWS
@@ -352,87 +169,9 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_PreservesNewline()
{
using (var hostContext = Setup())
{
var newline = "\n";
var envFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_ENV<<EOF",
"hello",
"world",
"EOF",
};
WriteContent(envFile, content, newline: newline);
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count);
Assert.Equal($"hello{newline}world", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]);
}
base.TestHeredoc_PreservesNewline();
}
#endif
private void WriteContent(
string path,
List<string> content,
string newline = null)
{
if (string.IsNullOrEmpty(newline))
{
newline = Environment.NewLine;
}
var encoding = new UTF8Encoding(true); // Emit BOM
var contentStr = string.Join(newline, content);
File.WriteAllText(path, contentStr, encoding);
}
private TestHostContext Setup([CallerMemberName] string name = "")
{
_issues = new List<Tuple<DTWebApi.Issue, string>>();
var hostContext = new TestHostContext(this, name);
// Trace
_trace = hostContext.GetTrace();
// Directory for test data
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
Directory.CreateDirectory(workDirectory);
_rootDirectory = Path.Combine(workDirectory, nameof(SetEnvFileCommandL0));
Directory.CreateDirectory(_rootDirectory);
// Execution context
_executionContext = new Mock<IExecutionContext>();
_executionContext.Setup(x => x.Global)
.Returns(new GlobalContext
{
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
{
var resolvedMessage = issue.Message;
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
resolvedMessage = logOptions.LogMessageOverride;
}
_issues.Add(new(issue, resolvedMessage));
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>
{
_trace.Info($"{tag}{message}");
});
// SetEnvFileCommand
_setEnvFileCommand = new SetEnvFileCommand();
_setEnvFileCommand.Initialize(hostContext);
return hostContext;
}
}
}

View File

@@ -1,44 +1,36 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class SetOutputFileCommandL0
public sealed class SetOutputFileCommandL0 : FileCommandTestBase<SetOutputFileCommand>
{
private Mock<IExecutionContext> _executionContext;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private Dictionary<string, string> _outputs;
private string _rootDirectory;
private SetOutputFileCommand _setOutputFileCommand;
private ITraceWriter _trace;
protected override IDictionary<string, string> PostSetup()
{
var outputs = new Dictionary<string, string>();
var reference = string.Empty;
_executionContext.Setup(x => x.SetOutput(It.IsAny<string>(), It.IsAny<string>(), out reference))
.Callback((string name, string value, out string reference) =>
{
reference = value;
outputs[name] = value;
});
return outputs;
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetOutputFileCommand_DirectoryNotFound()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "directory-not-found", "env");
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _outputs.Count);
}
base.TestDirectoryNotFound();
}
[Fact]
@@ -46,13 +38,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_NotFound()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "file-not-found");
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _outputs.Count);
}
base.TestNotFound();
}
[Fact]
@@ -60,15 +46,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_EmptyFile()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "empty-file");
var content = new List<string>();
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _outputs.Count);
}
base.TestEmptyFile();
}
[Fact]
@@ -76,19 +54,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_OUTPUT=MY VALUE",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _outputs.Count);
Assert.Equal("MY VALUE", _outputs["MY_OUTPUT"]);
}
base.TestSimple();
}
[Fact]
@@ -96,24 +62,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_SkipEmptyLines()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
string.Empty,
"MY_OUTPUT=my value",
string.Empty,
"MY_OUTPUT_2=my second value",
string.Empty,
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(2, _outputs.Count);
Assert.Equal("my value", _outputs["MY_OUTPUT"]);
Assert.Equal("my second value", _outputs["MY_OUTPUT_2"]);
}
base.TestSimple_SkipEmptyLines();
}
[Fact]
@@ -121,19 +70,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_EmptyValue()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple-empty-value");
var content = new List<string>
{
"MY_OUTPUT=",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _outputs.Count);
Assert.Equal(string.Empty, _outputs["MY_OUTPUT"]);
}
base.TestSimple_EmptyValue();
}
[Fact]
@@ -141,23 +78,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_MultipleValues()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_OUTPUT=my value",
"MY_OUTPUT_2=",
"MY_OUTPUT_3=my third value",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(3, _outputs.Count);
Assert.Equal("my value", _outputs["MY_OUTPUT"]);
Assert.Equal(string.Empty, _outputs["MY_OUTPUT_2"]);
Assert.Equal("my third value", _outputs["MY_OUTPUT_3"]);
}
base.TestSimple_MultipleValues();
}
[Fact]
@@ -165,23 +86,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_SpecialCharacters()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_OUTPUT==abc",
"MY_OUTPUT_2=def=ghi",
"MY_OUTPUT_3=jkl=",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(3, _outputs.Count);
Assert.Equal("=abc", _outputs["MY_OUTPUT"]);
Assert.Equal("def=ghi", _outputs["MY_OUTPUT_2"]);
Assert.Equal("jkl=", _outputs["MY_OUTPUT_3"]);
}
base.TestSimple_SpecialCharacters();
}
[Fact]
@@ -189,23 +94,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<EOF",
"line one",
"line two",
"line three",
"EOF",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _outputs.Count);
Assert.Equal($"line one{Environment.NewLine}line two{Environment.NewLine}line three", _outputs["MY_OUTPUT"]);
}
base.TestHeredoc();
}
[Fact]
@@ -213,20 +102,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_EmptyValue()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<EOF",
"EOF",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _outputs.Count);
Assert.Equal(string.Empty, _outputs["MY_OUTPUT"]);
}
base.TestHeredoc_EmptyValue();
}
[Fact]
@@ -234,73 +110,52 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_SkipEmptyLines()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
string.Empty,
"MY_OUTPUT<<EOF",
"hello",
"world",
"EOF",
string.Empty,
"MY_OUTPUT_2<<EOF",
"HELLO",
"AGAIN",
"EOF",
string.Empty,
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(2, _outputs.Count);
Assert.Equal($"hello{Environment.NewLine}world", _outputs["MY_OUTPUT"]);
Assert.Equal($"HELLO{Environment.NewLine}AGAIN", _outputs["MY_OUTPUT_2"]);
}
base.TestHeredoc_SkipEmptyLines();
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_SpecialCharacters()
public void SetOutputFileCommand_Heredoc_EdgeCases()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<=EOF",
"hello",
"one",
"=EOF",
"MY_OUTPUT_2<<<EOF",
"hello",
"two",
"<EOF",
"MY_OUTPUT_3<<EOF",
"hello",
string.Empty,
"three",
string.Empty,
"EOF",
"MY_OUTPUT_4<<EOF",
"hello=four",
"EOF",
"MY_OUTPUT_5<<EOF",
" EOF",
"EOF",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(5, _outputs.Count);
Assert.Equal($"hello{Environment.NewLine}one", _outputs["MY_OUTPUT"]);
Assert.Equal($"hello{Environment.NewLine}two", _outputs["MY_OUTPUT_2"]);
Assert.Equal($"hello{Environment.NewLine}{Environment.NewLine}three{Environment.NewLine}", _outputs["MY_OUTPUT_3"]);
Assert.Equal($"hello=four", _outputs["MY_OUTPUT_4"]);
Assert.Equal($" EOF", _outputs["MY_OUTPUT_5"]);
}
base.TestHeredoc_EdgeCases();
}
[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
// All of the following are not only valid, but quite plausible end markers.
// Most are derived straight from the example at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
#pragma warning disable format
[InlineData("=EOF")][InlineData("==EOF")][InlineData("EO=F")][InlineData("EO==F")][InlineData("EOF=")][InlineData("EOF==")]
[InlineData("<EOF")][InlineData("<<EOF")][InlineData("EO<F")][InlineData("EO<<F")][InlineData("EOF<")][InlineData("EOF<<")]
[InlineData("+EOF")][InlineData("++EOF")][InlineData("EO+F")][InlineData("EO++F")][InlineData("EOF+")][InlineData("EOF++")]
[InlineData("/EOF")][InlineData("//EOF")][InlineData("EO/F")][InlineData("EO//F")][InlineData("EOF/")][InlineData("EOF//")]
#pragma warning restore format
[InlineData("<<//++==")]
[InlineData("contrivedBase64==")]
[InlineData("khkIhPxsVA==")]
[InlineData("D+Y8zE/EOw==")]
[InlineData("wuOWG4S6FQ==")]
[InlineData("7wigCJ//iw==")]
[InlineData("uifTuYTs8K4=")]
[InlineData("M7N2ITg/04c=")]
[InlineData("Xhh+qp+Y6iM=")]
[InlineData("5tdblQajc/b+EGBZXo0w")]
[InlineData("jk/UMjIx/N0eVcQYOUfw")]
[InlineData("/n5lsw73Cwl35Hfuscdz")]
[InlineData("ZvnAEW+9O0tXp3Fmb3Oh")]
public void SetOutputFileCommand_Heredoc_EndMarkerVariations(string validEndMarker)
{
base.TestHeredoc_EndMarkerVariations(validEndMarker);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_EqualBeforeMultilineIndicator()
{
base.TestHeredoc_EqualBeforeMultilineIndicator();
}
[Fact]
@@ -308,21 +163,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_MissingNewLine()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<EOF",
"line one",
"line two",
"line three",
"EOF",
};
WriteContent(stateFile, content, " ");
var ex = Assert.Throws<Exception>(() => _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null));
Assert.Contains("Matching delimiter not found", ex.Message);
}
base.TestHeredoc_MissingNewLine();
}
[Fact]
@@ -330,21 +171,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_MissingNewLineMultipleLines()
{
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<EOF",
@"line one
line two
line three",
"EOF",
};
WriteContent(stateFile, content, " ");
var ex = Assert.Throws<Exception>(() => _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null));
Assert.Contains("EOF marker missing new line", ex.Message);
}
base.TestHeredoc_MissingNewLineMultipleLines();
}
#if OS_WINDOWS
@@ -353,96 +180,9 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_PreservesNewline()
{
using (var hostContext = Setup())
{
var newline = "\n";
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<EOF",
"hello",
"world",
"EOF",
};
WriteContent(stateFile, content, newline: newline);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _outputs.Count);
Assert.Equal($"hello{newline}world", _outputs["MY_OUTPUT"]);
}
base.TestHeredoc_PreservesNewline();
}
#endif
private void WriteContent(
string path,
List<string> content,
string newline = null)
{
if (string.IsNullOrEmpty(newline))
{
newline = Environment.NewLine;
}
var encoding = new UTF8Encoding(true); // Emit BOM
var contentStr = string.Join(newline, content);
File.WriteAllText(path, contentStr, encoding);
}
private TestHostContext Setup([CallerMemberName] string name = "")
{
_issues = new List<Tuple<DTWebApi.Issue, string>>();
_outputs = new Dictionary<string, string>();
var hostContext = new TestHostContext(this, name);
// Trace
_trace = hostContext.GetTrace();
// Directory for test data
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
Directory.CreateDirectory(workDirectory);
_rootDirectory = Path.Combine(workDirectory, nameof(SetOutputFileCommandL0));
Directory.CreateDirectory(_rootDirectory);
// Execution context
_executionContext = new Mock<IExecutionContext>();
_executionContext.Setup(x => x.Global)
.Returns(new GlobalContext
{
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
{
var resolvedMessage = issue.Message;
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
resolvedMessage = logOptions.LogMessageOverride;
}
_issues.Add(new(issue, resolvedMessage));
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>
{
_trace.Info($"{tag}{message}");
});
var reference = string.Empty;
_executionContext.Setup(x => x.SetOutput(It.IsAny<string>(), It.IsAny<string>(), out reference))
.Callback((string name, string value, out string reference) =>
{
reference = value;
_outputs[name] = value;
});
// SetOutputFileCommand
_setOutputFileCommand = new SetOutputFileCommand();
_setOutputFileCommand.Initialize(hostContext);
return hostContext;
}
}
}