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 equalsIndex = line.IndexOf("=", StringComparison.Ordinal);
var heredocIndex = line.IndexOf("<<", StringComparison.Ordinal); var heredocIndex = line.IndexOf("<<", StringComparison.Ordinal);
// Normal style NAME=VALUE // Heredoc style NAME<<EOF (where EOF is typically randomly-generated Base64 and may include an '=' character)
if (equalsIndex >= 0 && (heredocIndex < 0 || equalsIndex < heredocIndex)) // 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(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))
{ {
var split = line.Split(new[] { "<<" }, 2, StringSplitOptions.None); var split = line.Split(new[] { "<<" }, 2, StringSplitOptions.None);
if (string.IsNullOrEmpty(split[0]) || string.IsNullOrEmpty(split[1])) 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; 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 else
{ {
throw new Exception($"Invalid format '{line}'"); 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 Xunit;
using GitHub.Runner.Sdk; using GitHub.Runner.Sdk;
using System.Runtime.CompilerServices; using System.Linq;
namespace GitHub.Runner.Common.Tests namespace GitHub.Runner.Common.Tests
{ {
public enum LineEndingType
{
Native,
Linux = 0x__0A,
Windows = 0x0D0A
}
public static class TestUtil public static class TestUtil
{ {
private const string Src = "src"; private const string Src = "src";
@@ -41,5 +52,24 @@ namespace GitHub.Runner.Common.Tests
Assert.True(Directory.Exists(testDataDir)); Assert.True(Directory.Exists(testDataDir));
return 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", "## This is more markdown content",
}; };
WriteContent(stepSummaryFile, content); TestUtil.WriteContent(stepSummaryFile, content);
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null); _createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
_jobExecutionContext.Complete(); _jobExecutionContext.Complete();
@@ -153,7 +153,7 @@ namespace GitHub.Runner.Common.Tests.Worker
"", "",
"# GITHUB_TOKEN ghs_verysecuretoken", "# GITHUB_TOKEN ghs_verysecuretoken",
}; };
WriteContent(stepSummaryFile, content); TestUtil.WriteContent(stepSummaryFile, content);
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null); _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 = "") private TestHostContext Setup([CallerMemberName] string name = "")
{ {
var hostContext = new TestHostContext(this, 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq; 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;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit; using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
namespace GitHub.Runner.Common.Tests.Worker 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; protected override IDictionary<string, string> PostSetup()
private string _rootDirectory; {
private SaveStateFileCommand _saveStateFileCommand; var intraActionState = new Dictionary<string, string>();
private Dictionary<string, string> _intraActionState; _executionContext.Setup(x => x.IntraActionState).Returns(intraActionState);
private ITraceWriter _trace; return intraActionState;
}
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_DirectoryNotFound() public void SaveStateFileCommand_DirectoryNotFound()
{ {
using (var hostContext = Setup()) base.TestDirectoryNotFound();
{
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);
}
} }
[Fact] [Fact]
@@ -46,13 +29,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_NotFound() public void SaveStateFileCommand_NotFound()
{ {
using (var hostContext = Setup()) base.TestNotFound();
{
var stateFile = Path.Combine(_rootDirectory, "file-not-found");
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _intraActionState.Count);
}
} }
[Fact] [Fact]
@@ -60,15 +37,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_EmptyFile() public void SaveStateFileCommand_EmptyFile()
{ {
using (var hostContext = Setup()) base.TestEmptyFile();
{
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);
}
} }
[Fact] [Fact]
@@ -76,19 +45,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple() public void SaveStateFileCommand_Simple()
{ {
using (var hostContext = Setup()) base.TestSimple();
{
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"]);
}
} }
[Fact] [Fact]
@@ -96,24 +53,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_SkipEmptyLines() public void SaveStateFileCommand_Simple_SkipEmptyLines()
{ {
using (var hostContext = Setup()) base.TestSimple_SkipEmptyLines();
{
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"]);
}
} }
[Fact] [Fact]
@@ -121,19 +61,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_EmptyValue() public void SaveStateFileCommand_Simple_EmptyValue()
{ {
using (var hostContext = Setup()) base.TestSimple_EmptyValue();
{
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"]);
}
} }
[Fact] [Fact]
@@ -141,23 +69,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_MultipleValues() public void SaveStateFileCommand_Simple_MultipleValues()
{ {
using (var hostContext = Setup()) base.TestSimple_MultipleValues();
{
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"]);
}
} }
[Fact] [Fact]
@@ -165,23 +77,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Simple_SpecialCharacters() public void SaveStateFileCommand_Simple_SpecialCharacters()
{ {
using (var hostContext = Setup()) base.TestSimple_SpecialCharacters();
{
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"]);
}
} }
[Fact] [Fact]
@@ -189,23 +85,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc() public void SaveStateFileCommand_Heredoc()
{ {
using (var hostContext = Setup()) base.TestHeredoc();
{
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"]);
}
} }
[Fact] [Fact]
@@ -213,20 +93,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_EmptyValue() public void SaveStateFileCommand_Heredoc_EmptyValue()
{ {
using (var hostContext = Setup()) base.TestHeredoc_EmptyValue();
{
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"]);
}
} }
[Fact] [Fact]
@@ -234,73 +101,52 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_SkipEmptyLines() public void SaveStateFileCommand_Heredoc_SkipEmptyLines()
{ {
using (var hostContext = Setup()) base.TestHeredoc_SkipEmptyLines();
{
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"]);
}
} }
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_SpecialCharacters() public void SaveStateFileCommand_Heredoc_EdgeCases()
{ {
using (var hostContext = Setup()) base.TestHeredoc_EdgeCases();
{ }
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string> [Theory]
{ [Trait("Level", "L0")]
"MY_STATE<<=EOF", [Trait("Category", "Worker")]
"hello", // All of the following are not only valid, but quite plausible end markers.
"one", // Most are derived straight from the example at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
"=EOF", #pragma warning disable format
"MY_STATE_2<<<EOF", [InlineData("=EOF")][InlineData("==EOF")][InlineData("EO=F")][InlineData("EO==F")][InlineData("EOF=")][InlineData("EOF==")]
"hello", [InlineData("<EOF")][InlineData("<<EOF")][InlineData("EO<F")][InlineData("EO<<F")][InlineData("EOF<")][InlineData("EOF<<")]
"two", [InlineData("+EOF")][InlineData("++EOF")][InlineData("EO+F")][InlineData("EO++F")][InlineData("EOF+")][InlineData("EOF++")]
"<EOF", [InlineData("/EOF")][InlineData("//EOF")][InlineData("EO/F")][InlineData("EO//F")][InlineData("EOF/")][InlineData("EOF//")]
"MY_STATE_3<<EOF", #pragma warning restore format
"hello", [InlineData("<<//++==")]
string.Empty, [InlineData("contrivedBase64==")]
"three", [InlineData("khkIhPxsVA==")]
string.Empty, [InlineData("D+Y8zE/EOw==")]
"EOF", [InlineData("wuOWG4S6FQ==")]
"MY_STATE_4<<EOF", [InlineData("7wigCJ//iw==")]
"hello=four", [InlineData("uifTuYTs8K4=")]
"EOF", [InlineData("M7N2ITg/04c=")]
"MY_STATE_5<<EOF", [InlineData("Xhh+qp+Y6iM=")]
" EOF", [InlineData("5tdblQajc/b+EGBZXo0w")]
"EOF", [InlineData("jk/UMjIx/N0eVcQYOUfw")]
}; [InlineData("/n5lsw73Cwl35Hfuscdz")]
WriteContent(stateFile, content); [InlineData("ZvnAEW+9O0tXp3Fmb3Oh")]
_saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); public void SaveStateFileCommand_Heredoc_EndMarkerVariations(string validEndMarker)
Assert.Equal(0, _issues.Count); {
Assert.Equal(5, _intraActionState.Count); base.TestHeredoc_EndMarkerVariations(validEndMarker);
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"]); [Fact]
Assert.Equal($"hello=four", _intraActionState["MY_STATE_4"]); [Trait("Level", "L0")]
Assert.Equal($" EOF", _intraActionState["MY_STATE_5"]); [Trait("Category", "Worker")]
} public void SaveStateFileCommand_Heredoc_EqualBeforeMultilineIndicator()
{
base.TestHeredoc_EqualBeforeMultilineIndicator();
} }
[Fact] [Fact]
@@ -308,21 +154,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_MissingNewLine() public void SaveStateFileCommand_Heredoc_MissingNewLine()
{ {
using (var hostContext = Setup()) base.TestHeredoc_MissingNewLine();
{
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);
}
} }
[Fact] [Fact]
@@ -330,21 +162,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_MissingNewLineMultipleLines() public void SaveStateFileCommand_Heredoc_MissingNewLineMultipleLines()
{ {
using (var hostContext = Setup()) base.TestHeredoc_MissingNewLineMultipleLines();
{
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);
}
} }
#if OS_WINDOWS #if OS_WINDOWS
@@ -353,90 +171,9 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SaveStateFileCommand_Heredoc_PreservesNewline() public void SaveStateFileCommand_Heredoc_PreservesNewline()
{ {
using (var hostContext = Setup()) base.TestHeredoc_PreservesNewline();
{
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"]);
}
} }
#endif #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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq; 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;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit; using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
namespace GitHub.Runner.Common.Tests.Worker 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; protected override IDictionary<string, string> PostSetup()
private string _rootDirectory; {
private SetEnvFileCommand _setEnvFileCommand; return _executionContext.Object.Global.EnvironmentVariables;
private ITraceWriter _trace; }
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_DirectoryNotFound() public void SetEnvFileCommand_DirectoryNotFound()
{ {
using (var hostContext = Setup()) base.TestDirectoryNotFound();
{
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);
}
} }
[Fact] [Fact]
@@ -45,13 +27,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_NotFound() public void SetEnvFileCommand_NotFound()
{ {
using (var hostContext = Setup()) base.TestNotFound();
{
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);
}
} }
[Fact] [Fact]
@@ -59,15 +35,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_EmptyFile() public void SetEnvFileCommand_EmptyFile()
{ {
using (var hostContext = Setup()) base.TestEmptyFile();
{
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);
}
} }
[Fact] [Fact]
@@ -75,19 +43,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple() public void SetEnvFileCommand_Simple()
{ {
using (var hostContext = Setup()) base.TestSimple();
{
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"]);
}
} }
[Fact] [Fact]
@@ -95,24 +51,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_SkipEmptyLines() public void SetEnvFileCommand_Simple_SkipEmptyLines()
{ {
using (var hostContext = Setup()) base.TestSimple_SkipEmptyLines();
{
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"]);
}
} }
[Fact] [Fact]
@@ -120,19 +59,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_EmptyValue() public void SetEnvFileCommand_Simple_EmptyValue()
{ {
using (var hostContext = Setup()) base.TestSimple_EmptyValue();
{
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"]);
}
} }
[Fact] [Fact]
@@ -140,23 +67,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_MultipleValues() public void SetEnvFileCommand_Simple_MultipleValues()
{ {
using (var hostContext = Setup()) base.TestSimple_MultipleValues();
{
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"]);
}
} }
[Fact] [Fact]
@@ -164,23 +75,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Simple_SpecialCharacters() public void SetEnvFileCommand_Simple_SpecialCharacters()
{ {
using (var hostContext = Setup()) base.TestSimple_SpecialCharacters();
{
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"]);
}
} }
[Fact] [Fact]
@@ -188,23 +83,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc() public void SetEnvFileCommand_Heredoc()
{ {
using (var hostContext = Setup()) base.TestHeredoc();
{
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"]);
}
} }
[Fact] [Fact]
@@ -212,20 +91,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_EmptyValue() public void SetEnvFileCommand_Heredoc_EmptyValue()
{ {
using (var hostContext = Setup()) base.TestHeredoc_EmptyValue();
{
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"]);
}
} }
[Fact] [Fact]
@@ -233,73 +99,52 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_SkipEmptyLines() public void SetEnvFileCommand_Heredoc_SkipEmptyLines()
{ {
using (var hostContext = Setup()) base.TestHeredoc_SkipEmptyLines();
{
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"]);
}
} }
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_SpecialCharacters() public void SetEnvFileCommand_Heredoc_EdgeCases()
{ {
using (var hostContext = Setup()) base.TestHeredoc_EdgeCases();
{ }
var envFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string> [Theory]
{ [Trait("Level", "L0")]
"MY_ENV<<=EOF", [Trait("Category", "Worker")]
"hello", // All of the following are not only valid, but quite plausible end markers.
"one", // Most are derived straight from the example at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
"=EOF", #pragma warning disable format
"MY_ENV_2<<<EOF", [InlineData("=EOF")][InlineData("==EOF")][InlineData("EO=F")][InlineData("EO==F")][InlineData("EOF=")][InlineData("EOF==")]
"hello", [InlineData("<EOF")][InlineData("<<EOF")][InlineData("EO<F")][InlineData("EO<<F")][InlineData("EOF<")][InlineData("EOF<<")]
"two", [InlineData("+EOF")][InlineData("++EOF")][InlineData("EO+F")][InlineData("EO++F")][InlineData("EOF+")][InlineData("EOF++")]
"<EOF", [InlineData("/EOF")][InlineData("//EOF")][InlineData("EO/F")][InlineData("EO//F")][InlineData("EOF/")][InlineData("EOF//")]
"MY_ENV_3<<EOF", #pragma warning restore format
"hello", [InlineData("<<//++==")]
string.Empty, [InlineData("contrivedBase64==")]
"three", [InlineData("khkIhPxsVA==")]
string.Empty, [InlineData("D+Y8zE/EOw==")]
"EOF", [InlineData("wuOWG4S6FQ==")]
"MY_ENV_4<<EOF", [InlineData("7wigCJ//iw==")]
"hello=four", [InlineData("uifTuYTs8K4=")]
"EOF", [InlineData("M7N2ITg/04c=")]
"MY_ENV_5<<EOF", [InlineData("Xhh+qp+Y6iM=")]
" EOF", [InlineData("5tdblQajc/b+EGBZXo0w")]
"EOF", [InlineData("jk/UMjIx/N0eVcQYOUfw")]
}; [InlineData("/n5lsw73Cwl35Hfuscdz")]
WriteContent(envFile, content); [InlineData("ZvnAEW+9O0tXp3Fmb3Oh")]
_setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null); public void SetEnvFileCommand_Heredoc_EndMarkerVariations(string validEndMarker)
Assert.Equal(0, _issues.Count); {
Assert.Equal(5, _executionContext.Object.Global.EnvironmentVariables.Count); base.TestHeredoc_EndMarkerVariations(validEndMarker);
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"]); [Fact]
Assert.Equal($"hello=four", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_4"]); [Trait("Level", "L0")]
Assert.Equal($" EOF", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_5"]); [Trait("Category", "Worker")]
} public void SetEnvFileCommand_Heredoc_EqualBeforeMultilineIndicator()
{
base.TestHeredoc_EqualBeforeMultilineIndicator();
} }
[Fact] [Fact]
@@ -307,43 +152,15 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_MissingNewLine() public void SetEnvFileCommand_Heredoc_MissingNewLine()
{ {
using (var hostContext = Setup()) base.TestHeredoc_MissingNewLine();
{
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);
}
} }
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_MissingNewLineMultipleLinesEnv() public void SetEnvFileCommand_Heredoc_MissingNewLineMultipleLines()
{ {
using (var hostContext = Setup()) base.TestHeredoc_MissingNewLineMultipleLines();
{
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);
}
} }
#if OS_WINDOWS #if OS_WINDOWS
@@ -352,87 +169,9 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetEnvFileCommand_Heredoc_PreservesNewline() public void SetEnvFileCommand_Heredoc_PreservesNewline()
{ {
using (var hostContext = Setup()) base.TestHeredoc_PreservesNewline();
{
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"]);
}
} }
#endif #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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq; 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;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using Moq; using Moq;
using Xunit; using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
namespace GitHub.Runner.Common.Tests.Worker 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; protected override IDictionary<string, string> PostSetup()
private Dictionary<string, string> _outputs; {
private string _rootDirectory; var outputs = new Dictionary<string, string>();
private SetOutputFileCommand _setOutputFileCommand; var reference = string.Empty;
private ITraceWriter _trace; _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] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_DirectoryNotFound() public void SetOutputFileCommand_DirectoryNotFound()
{ {
using (var hostContext = Setup()) base.TestDirectoryNotFound();
{
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);
}
} }
[Fact] [Fact]
@@ -46,13 +38,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_NotFound() public void SetOutputFileCommand_NotFound()
{ {
using (var hostContext = Setup()) base.TestNotFound();
{
var stateFile = Path.Combine(_rootDirectory, "file-not-found");
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _outputs.Count);
}
} }
[Fact] [Fact]
@@ -60,15 +46,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_EmptyFile() public void SetOutputFileCommand_EmptyFile()
{ {
using (var hostContext = Setup()) base.TestEmptyFile();
{
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);
}
} }
[Fact] [Fact]
@@ -76,19 +54,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple() public void SetOutputFileCommand_Simple()
{ {
using (var hostContext = Setup()) base.TestSimple();
{
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"]);
}
} }
[Fact] [Fact]
@@ -96,24 +62,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_SkipEmptyLines() public void SetOutputFileCommand_Simple_SkipEmptyLines()
{ {
using (var hostContext = Setup()) base.TestSimple_SkipEmptyLines();
{
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"]);
}
} }
[Fact] [Fact]
@@ -121,19 +70,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_EmptyValue() public void SetOutputFileCommand_Simple_EmptyValue()
{ {
using (var hostContext = Setup()) base.TestSimple_EmptyValue();
{
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"]);
}
} }
[Fact] [Fact]
@@ -141,23 +78,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_MultipleValues() public void SetOutputFileCommand_Simple_MultipleValues()
{ {
using (var hostContext = Setup()) base.TestSimple_MultipleValues();
{
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"]);
}
} }
[Fact] [Fact]
@@ -165,23 +86,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_SpecialCharacters() public void SetOutputFileCommand_Simple_SpecialCharacters()
{ {
using (var hostContext = Setup()) base.TestSimple_SpecialCharacters();
{
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"]);
}
} }
[Fact] [Fact]
@@ -189,23 +94,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc() public void SetOutputFileCommand_Heredoc()
{ {
using (var hostContext = Setup()) base.TestHeredoc();
{
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"]);
}
} }
[Fact] [Fact]
@@ -213,20 +102,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_EmptyValue() public void SetOutputFileCommand_Heredoc_EmptyValue()
{ {
using (var hostContext = Setup()) base.TestHeredoc_EmptyValue();
{
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"]);
}
} }
[Fact] [Fact]
@@ -234,73 +110,52 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_SkipEmptyLines() public void SetOutputFileCommand_Heredoc_SkipEmptyLines()
{ {
using (var hostContext = Setup()) base.TestHeredoc_SkipEmptyLines();
{
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"]);
}
} }
[Fact] [Fact]
[Trait("Level", "L0")] [Trait("Level", "L0")]
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_SpecialCharacters() public void SetOutputFileCommand_Heredoc_EdgeCases()
{ {
using (var hostContext = Setup()) base.TestHeredoc_EdgeCases();
{ }
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string> [Theory]
{ [Trait("Level", "L0")]
"MY_OUTPUT<<=EOF", [Trait("Category", "Worker")]
"hello", // All of the following are not only valid, but quite plausible end markers.
"one", // Most are derived straight from the example at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
"=EOF", #pragma warning disable format
"MY_OUTPUT_2<<<EOF", [InlineData("=EOF")][InlineData("==EOF")][InlineData("EO=F")][InlineData("EO==F")][InlineData("EOF=")][InlineData("EOF==")]
"hello", [InlineData("<EOF")][InlineData("<<EOF")][InlineData("EO<F")][InlineData("EO<<F")][InlineData("EOF<")][InlineData("EOF<<")]
"two", [InlineData("+EOF")][InlineData("++EOF")][InlineData("EO+F")][InlineData("EO++F")][InlineData("EOF+")][InlineData("EOF++")]
"<EOF", [InlineData("/EOF")][InlineData("//EOF")][InlineData("EO/F")][InlineData("EO//F")][InlineData("EOF/")][InlineData("EOF//")]
"MY_OUTPUT_3<<EOF", #pragma warning restore format
"hello", [InlineData("<<//++==")]
string.Empty, [InlineData("contrivedBase64==")]
"three", [InlineData("khkIhPxsVA==")]
string.Empty, [InlineData("D+Y8zE/EOw==")]
"EOF", [InlineData("wuOWG4S6FQ==")]
"MY_OUTPUT_4<<EOF", [InlineData("7wigCJ//iw==")]
"hello=four", [InlineData("uifTuYTs8K4=")]
"EOF", [InlineData("M7N2ITg/04c=")]
"MY_OUTPUT_5<<EOF", [InlineData("Xhh+qp+Y6iM=")]
" EOF", [InlineData("5tdblQajc/b+EGBZXo0w")]
"EOF", [InlineData("jk/UMjIx/N0eVcQYOUfw")]
}; [InlineData("/n5lsw73Cwl35Hfuscdz")]
WriteContent(stateFile, content); [InlineData("ZvnAEW+9O0tXp3Fmb3Oh")]
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); public void SetOutputFileCommand_Heredoc_EndMarkerVariations(string validEndMarker)
Assert.Equal(0, _issues.Count); {
Assert.Equal(5, _outputs.Count); base.TestHeredoc_EndMarkerVariations(validEndMarker);
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"]); [Fact]
Assert.Equal($"hello=four", _outputs["MY_OUTPUT_4"]); [Trait("Level", "L0")]
Assert.Equal($" EOF", _outputs["MY_OUTPUT_5"]); [Trait("Category", "Worker")]
} public void SetOutputFileCommand_Heredoc_EqualBeforeMultilineIndicator()
{
base.TestHeredoc_EqualBeforeMultilineIndicator();
} }
[Fact] [Fact]
@@ -308,21 +163,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_MissingNewLine() public void SetOutputFileCommand_Heredoc_MissingNewLine()
{ {
using (var hostContext = Setup()) base.TestHeredoc_MissingNewLine();
{
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);
}
} }
[Fact] [Fact]
@@ -330,21 +171,7 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_MissingNewLineMultipleLines() public void SetOutputFileCommand_Heredoc_MissingNewLineMultipleLines()
{ {
using (var hostContext = Setup()) base.TestHeredoc_MissingNewLineMultipleLines();
{
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);
}
} }
#if OS_WINDOWS #if OS_WINDOWS
@@ -353,96 +180,9 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")] [Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_PreservesNewline() public void SetOutputFileCommand_Heredoc_PreservesNewline()
{ {
using (var hostContext = Setup()) base.TestHeredoc_PreservesNewline();
{
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"]);
}
} }
#endif #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;
}
} }
} }