mirror of
https://github.com/actions/runner.git
synced 2025-12-12 05:37:01 +00:00
Add file commands for save-state and set-output (#2118)
This commit is contained in:
@@ -60,6 +60,8 @@ namespace GitHub.Runner.Common
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.AddPathFileCommand, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.SetEnvFileCommand, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.CreateStepSummaryCommand, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.SaveStateFileCommand, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.SetOutputFileCommand, Runner.Worker");
|
||||
break;
|
||||
case "GitHub.Runner.Listener.Check.ICheckExtension":
|
||||
Add<T>(extensions, "GitHub.Runner.Listener.Check.InternetCheck, Runner.Listener");
|
||||
|
||||
@@ -138,74 +138,10 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string filePath, ContainerInfo container)
|
||||
{
|
||||
try
|
||||
var pairs = new EnvFileKeyValuePairs(context, filePath);
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
var text = File.ReadAllText(filePath) ?? string.Empty;
|
||||
var index = 0;
|
||||
var line = ReadLine(text, ref index);
|
||||
while (line != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
{
|
||||
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 environment variable format '{line}'. Environment variable name must not be empty");
|
||||
}
|
||||
SetEnvironmentVariable(context, split[0], split[1]);
|
||||
}
|
||||
// Heredoc style NAME<<EOF
|
||||
else if (heredocIndex >= 0 && (equalsIndex < 0 || heredocIndex < equalsIndex))
|
||||
{
|
||||
var split = line.Split(new[] { "<<" }, 2, StringSplitOptions.None);
|
||||
if (string.IsNullOrEmpty(split[0]) || string.IsNullOrEmpty(split[1]))
|
||||
{
|
||||
throw new Exception($"Invalid environment variable format '{line}'. Environment variable name must not be empty and delimiter must not be empty");
|
||||
}
|
||||
var name = split[0];
|
||||
var delimiter = split[1];
|
||||
var startIndex = index; // Start index of the value (inclusive)
|
||||
var endIndex = index; // End index of the value (exclusive)
|
||||
var tempLine = ReadLine(text, ref index, out var newline);
|
||||
while (!string.Equals(tempLine, delimiter, StringComparison.Ordinal))
|
||||
{
|
||||
if (tempLine == null)
|
||||
{
|
||||
throw new Exception($"Invalid environment variable value. Matching delimiter not found '{delimiter}'");
|
||||
}
|
||||
if (newline == null)
|
||||
{
|
||||
throw new Exception($"Invalid environment variable value. EOF marker missing new line.");
|
||||
}
|
||||
endIndex = index - newline.Length;
|
||||
tempLine = ReadLine(text, ref index, out newline);
|
||||
}
|
||||
|
||||
var value = endIndex > startIndex ? text.Substring(startIndex, endIndex - startIndex) : string.Empty;
|
||||
SetEnvironmentVariable(context, name, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Invalid environment variable format '{line}'");
|
||||
}
|
||||
}
|
||||
|
||||
line = ReadLine(text, ref index);
|
||||
}
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
context.Debug($"Environment variables file does not exist '{filePath}'");
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
context.Debug($"Environment variables file does not exist '{filePath}'");
|
||||
SetEnvironmentVariable(context, pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,48 +154,6 @@ namespace GitHub.Runner.Worker
|
||||
context.SetEnvContext(name, value);
|
||||
context.Debug($"{name}='{value}'");
|
||||
}
|
||||
|
||||
private static string ReadLine(
|
||||
string text,
|
||||
ref int index)
|
||||
{
|
||||
return ReadLine(text, ref index, out _);
|
||||
}
|
||||
|
||||
private static string ReadLine(
|
||||
string text,
|
||||
ref int index,
|
||||
out string newline)
|
||||
{
|
||||
if (index >= text.Length)
|
||||
{
|
||||
newline = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
var originalIndex = index;
|
||||
var lfIndex = text.IndexOf("\n", index, StringComparison.Ordinal);
|
||||
if (lfIndex < 0)
|
||||
{
|
||||
index = text.Length;
|
||||
newline = null;
|
||||
return text.Substring(originalIndex);
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
var crLFIndex = text.IndexOf("\r\n", index, StringComparison.Ordinal);
|
||||
if (crLFIndex >= 0 && crLFIndex < lfIndex)
|
||||
{
|
||||
index = crLFIndex + 2; // Skip over CRLF
|
||||
newline = "\r\n";
|
||||
return text.Substring(originalIndex, crLFIndex - originalIndex);
|
||||
}
|
||||
#endif
|
||||
|
||||
index = lfIndex + 1; // Skip over LF
|
||||
newline = "\n";
|
||||
return text.Substring(originalIndex, lfIndex - originalIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CreateStepSummaryCommand : RunnerService, IFileCommandExtension
|
||||
@@ -325,4 +219,200 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SaveStateFileCommand : RunnerService, IFileCommandExtension
|
||||
{
|
||||
public string ContextName => "state";
|
||||
public string FilePrefix => "save_state_";
|
||||
|
||||
public Type ExtensionType => typeof(IFileCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string filePath, ContainerInfo container)
|
||||
{
|
||||
var pairs = new EnvFileKeyValuePairs(context, filePath);
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
// Embedded steps (composite) keep track of the state at the root level
|
||||
if (context.IsEmbedded)
|
||||
{
|
||||
var id = context.EmbeddedId;
|
||||
if (!context.Root.EmbeddedIntraActionState.ContainsKey(id))
|
||||
{
|
||||
context.Root.EmbeddedIntraActionState[id] = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
context.Root.EmbeddedIntraActionState[id][pair.Key] = pair.Value;
|
||||
}
|
||||
// Otherwise modify the ExecutionContext
|
||||
else
|
||||
{
|
||||
context.IntraActionState[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
context.Debug($"Save intra-action state {pair.Key} = {pair.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SetOutputFileCommand : RunnerService, IFileCommandExtension
|
||||
{
|
||||
public string ContextName => "output";
|
||||
public string FilePrefix => "set_output_";
|
||||
|
||||
public Type ExtensionType => typeof(IFileCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string filePath, ContainerInfo container)
|
||||
{
|
||||
var pairs = new EnvFileKeyValuePairs(context, filePath);
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
context.SetOutput(pair.Key, pair.Value, out var reference);
|
||||
context.Debug($"Set output {pair.Key} = {pair.Value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EnvFileKeyValuePairs: IEnumerable<KeyValuePair<string, string>>
|
||||
{
|
||||
private IExecutionContext _context;
|
||||
private string _filePath;
|
||||
|
||||
public EnvFileKeyValuePairs(IExecutionContext context, string filePath)
|
||||
{
|
||||
_context = context;
|
||||
_filePath = filePath;
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
|
||||
{
|
||||
var text = string.Empty;
|
||||
try
|
||||
{
|
||||
text = File.ReadAllText(_filePath) ?? string.Empty;
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
_context.Debug($"File does not exist '{_filePath}'");
|
||||
yield break;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
_context.Debug($"File does not exist '{_filePath}'");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var index = 0;
|
||||
var line = ReadLine(text, ref index);
|
||||
while (line != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
{
|
||||
var key = string.Empty;
|
||||
var output = string.Empty;
|
||||
|
||||
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))
|
||||
{
|
||||
var split = line.Split(new[] { "<<" }, 2, StringSplitOptions.None);
|
||||
if (string.IsNullOrEmpty(split[0]) || string.IsNullOrEmpty(split[1]))
|
||||
{
|
||||
throw new Exception($"Invalid format '{line}'. Name must not be empty and delimiter must not be empty");
|
||||
}
|
||||
key = split[0];
|
||||
var delimiter = split[1];
|
||||
var startIndex = index; // Start index of the value (inclusive)
|
||||
var endIndex = index; // End index of the value (exclusive)
|
||||
var tempLine = ReadLine(text, ref index, out var newline);
|
||||
while (!string.Equals(tempLine, delimiter, StringComparison.Ordinal))
|
||||
{
|
||||
if (tempLine == null)
|
||||
{
|
||||
throw new Exception($"Invalid value. Matching delimiter not found '{delimiter}'");
|
||||
}
|
||||
if (newline == null)
|
||||
{
|
||||
throw new Exception($"Invalid value. EOF marker missing new line.");
|
||||
}
|
||||
endIndex = index - newline.Length;
|
||||
tempLine = ReadLine(text, ref index, out newline);
|
||||
}
|
||||
|
||||
output = endIndex > startIndex ? text.Substring(startIndex, endIndex - startIndex) : string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Invalid format '{line}'");
|
||||
}
|
||||
|
||||
yield return new KeyValuePair<string, string>(key, output);
|
||||
}
|
||||
|
||||
line = ReadLine(text, ref index);
|
||||
}
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator
|
||||
System.Collections.IEnumerable.GetEnumerator()
|
||||
{
|
||||
// Invoke IEnumerator<KeyValuePair<string, string>> GetEnumerator() above.
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
private static string ReadLine(
|
||||
string text,
|
||||
ref int index)
|
||||
{
|
||||
return ReadLine(text, ref index, out _);
|
||||
}
|
||||
|
||||
private static string ReadLine(
|
||||
string text,
|
||||
ref int index,
|
||||
out string newline)
|
||||
{
|
||||
if (index >= text.Length)
|
||||
{
|
||||
newline = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
var originalIndex = index;
|
||||
var lfIndex = text.IndexOf("\n", index, StringComparison.Ordinal);
|
||||
if (lfIndex < 0)
|
||||
{
|
||||
index = text.Length;
|
||||
newline = null;
|
||||
return text.Substring(originalIndex);
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
var crLFIndex = text.IndexOf("\r\n", index, StringComparison.Ordinal);
|
||||
if (crLFIndex >= 0 && crLFIndex < lfIndex)
|
||||
{
|
||||
index = crLFIndex + 2; // Skip over CRLF
|
||||
newline = "\r\n";
|
||||
return text.Substring(originalIndex, crLFIndex - originalIndex);
|
||||
}
|
||||
#endif
|
||||
|
||||
index = lfIndex + 1; // Skip over LF
|
||||
newline = "\n";
|
||||
return text.Substring(originalIndex, lfIndex - originalIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace GitHub.Runner.Worker
|
||||
"graphql_url",
|
||||
"head_ref",
|
||||
"job",
|
||||
"output",
|
||||
"path",
|
||||
"ref_name",
|
||||
"ref_protected",
|
||||
@@ -34,6 +35,7 @@ namespace GitHub.Runner.Worker
|
||||
"run_number",
|
||||
"server_url",
|
||||
"sha",
|
||||
"state",
|
||||
"step_summary",
|
||||
"triggering_actor",
|
||||
"workflow",
|
||||
|
||||
438
src/Test/L0/Worker/SaveStateFileCommandL0.cs
Normal file
438
src/Test/L0/Worker/SaveStateFileCommandL0.cs
Normal file
@@ -0,0 +1,438 @@
|
||||
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
|
||||
{
|
||||
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;
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SaveStateFileCommand_Heredoc_SpecialCharacters()
|
||||
{
|
||||
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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
#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<string>()))
|
||||
.Callback((DTWebApi.Issue issue, string logMessage) =>
|
||||
{
|
||||
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
|
||||
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
|
||||
_trace.Info($"Issue '{issue.Type}': {message}");
|
||||
});
|
||||
_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
444
src/Test/L0/Worker/SetOutputFileCommandL0.cs
Normal file
444
src/Test/L0/Worker/SetOutputFileCommandL0.cs
Normal file
@@ -0,0 +1,444 @@
|
||||
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
|
||||
{
|
||||
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;
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SetOutputFileCommand_Heredoc_SpecialCharacters()
|
||||
{
|
||||
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"]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[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"]);
|
||||
}
|
||||
}
|
||||
#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<string>()))
|
||||
.Callback((DTWebApi.Issue issue, string logMessage) =>
|
||||
{
|
||||
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
|
||||
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
|
||||
_trace.Info($"Issue '{issue.Type}': {message}");
|
||||
});
|
||||
_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user