diff --git a/src/Runner.Worker/FileCommandManager.cs b/src/Runner.Worker/FileCommandManager.cs index a084a5757..72ed5de0a 100644 --- a/src/Runner.Worker/FileCommandManager.cs +++ b/src/Runner.Worker/FileCommandManager.cs @@ -322,21 +322,9 @@ namespace GitHub.Runner.Worker var equalsIndex = line.IndexOf("=", StringComparison.Ordinal); var heredocIndex = line.IndexOf("<<", StringComparison.Ordinal); - // Normal style NAME=VALUE - if (equalsIndex >= 0 && (heredocIndex < 0 || equalsIndex < heredocIndex)) - { - var split = line.Split(new[] { '=' }, 2, StringSplitOptions.None); - if (string.IsNullOrEmpty(line)) - { - throw new Exception($"Invalid format '{line}'. Name must not be empty"); - } - - key = split[0]; - output = split[1]; - } - - // Heredoc style NAME<= 0 && (equalsIndex < 0 || heredocIndex < equalsIndex)) + // Heredoc style NAME< 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); + } + } } diff --git a/src/Test/L0/Worker/CreateStepSummaryCommandL0.cs b/src/Test/L0/Worker/CreateStepSummaryCommandL0.cs index 19f956fa8..e1a2c537d 100644 --- a/src/Test/L0/Worker/CreateStepSummaryCommandL0.cs +++ b/src/Test/L0/Worker/CreateStepSummaryCommandL0.cs @@ -124,7 +124,7 @@ namespace GitHub.Runner.Common.Tests.Worker "", "## This is more markdown content", }; - WriteContent(stepSummaryFile, content); + TestUtil.WriteContent(stepSummaryFile, content); _createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null); _jobExecutionContext.Complete(); @@ -153,7 +153,7 @@ namespace GitHub.Runner.Common.Tests.Worker "", "# GITHUB_TOKEN ghs_verysecuretoken", }; - WriteContent(stepSummaryFile, content); + TestUtil.WriteContent(stepSummaryFile, content); _createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null); @@ -167,21 +167,6 @@ namespace GitHub.Runner.Common.Tests.Worker } } - private void WriteContent( - string path, - List content, - string newline = null) - { - if (string.IsNullOrEmpty(newline)) - { - newline = Environment.NewLine; - } - - var encoding = new UTF8Encoding(true); // Emit BOM - var contentStr = string.Join(newline, content); - File.WriteAllText(path, contentStr, encoding); - } - private TestHostContext Setup([CallerMemberName] string name = "") { var hostContext = new TestHostContext(this, name); diff --git a/src/Test/L0/Worker/FileCommandTestBase.cs b/src/Test/L0/Worker/FileCommandTestBase.cs new file mode 100644 index 000000000..f3bcd8e73 --- /dev/null +++ b/src/Test/L0/Worker/FileCommandTestBase.cs @@ -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 + 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(); + 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 + { + "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.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 + { + "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 + { + "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 + { + "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 + { + "MY_KEY< + { + "MY_KEY< + { + string.Empty, + "MY_KEY< + { + "MY_KEY_1< + { + $"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 + { + // 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(() => _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<(() => _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<(() => _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 + { + "MY_KEY<>(); + + 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(); + _executionContext.Setup(x => x.Global) + .Returns(new GlobalContext + { + EnvironmentVariables = new Dictionary(VarUtil.EnvironmentVariableKeyComparer), + WriteDebug = true, + }); + _executionContext.Setup(x => x.AddIssue(It.IsAny(), It.IsAny())) + .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(), It.IsAny())) + .Callback((string tag, string message) => + { + _trace.Info($"{tag}{message}"); + }); + + _store = PostSetup(); + + _fileCmdExtension = new T(); + _fileCmdExtension.Initialize(hostContext); + + return hostContext; + } + + protected abstract IDictionary PostSetup(); + + protected static readonly string BREAK = Environment.NewLine; + + protected IFileCommandExtension _fileCmdExtension { get; private set; } + protected Mock _executionContext { get; private set; } + protected List> _issues { get; private set; } + protected IDictionary _store { get; private set; } + protected string _rootDirectory { get; private set; } + protected ITraceWriter _trace { get; private set; } + } +} diff --git a/src/Test/L0/Worker/SaveStateFileCommandL0.cs b/src/Test/L0/Worker/SaveStateFileCommandL0.cs index 15cba9d82..1157e29cc 100644 --- a/src/Test/L0/Worker/SaveStateFileCommandL0.cs +++ b/src/Test/L0/Worker/SaveStateFileCommandL0.cs @@ -1,44 +1,27 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Runtime.CompilerServices; -using GitHub.Runner.Common.Util; -using GitHub.Runner.Sdk; using GitHub.Runner.Worker; -using GitHub.Runner.Worker.Container; -using GitHub.Runner.Worker.Handlers; -using Moq; using Xunit; -using DTWebApi = GitHub.DistributedTask.WebApi; namespace GitHub.Runner.Common.Tests.Worker { - public sealed class SaveStateFileCommandL0 + public sealed class SaveStateFileCommandL0 : FileCommandTestBase { - private Mock _executionContext; - private List> _issues; - private string _rootDirectory; - private SaveStateFileCommand _saveStateFileCommand; - private Dictionary _intraActionState; - private ITraceWriter _trace; + + protected override IDictionary PostSetup() + { + var intraActionState = new Dictionary(); + _executionContext.Setup(x => x.IntraActionState).Returns(intraActionState); + return intraActionState; + } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void SaveStateFileCommand_DirectoryNotFound() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "directory-not-found", "env"); - _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(0, _intraActionState.Count); - } + base.TestDirectoryNotFound(); } [Fact] @@ -46,13 +29,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_NotFound() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "file-not-found"); - _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(0, _intraActionState.Count); - } + base.TestNotFound(); } [Fact] @@ -60,15 +37,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_EmptyFile() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "empty-file"); - var content = new List(); - WriteContent(stateFile, content); - _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(0, _intraActionState.Count); - } + base.TestEmptyFile(); } [Fact] @@ -76,19 +45,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Simple() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - "MY_STATE=MY VALUE", - }; - WriteContent(stateFile, content); - _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(1, _intraActionState.Count); - Assert.Equal("MY VALUE", _intraActionState["MY_STATE"]); - } + base.TestSimple(); } [Fact] @@ -96,24 +53,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Simple_SkipEmptyLines() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - string.Empty, - "MY_STATE=my value", - string.Empty, - "MY_STATE_2=my second value", - string.Empty, - }; - WriteContent(stateFile, content); - _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(2, _intraActionState.Count); - Assert.Equal("my value", _intraActionState["MY_STATE"]); - Assert.Equal("my second value", _intraActionState["MY_STATE_2"]); - } + base.TestSimple_SkipEmptyLines(); } [Fact] @@ -121,19 +61,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Simple_EmptyValue() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "simple-empty-value"); - var content = new List - { - "MY_STATE=", - }; - WriteContent(stateFile, content); - _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(1, _intraActionState.Count); - Assert.Equal(string.Empty, _intraActionState["MY_STATE"]); - } + base.TestSimple_EmptyValue(); } [Fact] @@ -141,23 +69,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Simple_MultipleValues() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - "MY_STATE=my value", - "MY_STATE_2=", - "MY_STATE_3=my third value", - }; - WriteContent(stateFile, content); - _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(3, _intraActionState.Count); - Assert.Equal("my value", _intraActionState["MY_STATE"]); - Assert.Equal(string.Empty, _intraActionState["MY_STATE_2"]); - Assert.Equal("my third value", _intraActionState["MY_STATE_3"]); - } + base.TestSimple_MultipleValues(); } [Fact] @@ -165,23 +77,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Simple_SpecialCharacters() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - "MY_STATE==abc", - "MY_STATE_2=def=ghi", - "MY_STATE_3=jkl=", - }; - WriteContent(stateFile, content); - _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(3, _intraActionState.Count); - Assert.Equal("=abc", _intraActionState["MY_STATE"]); - Assert.Equal("def=ghi", _intraActionState["MY_STATE_2"]); - Assert.Equal("jkl=", _intraActionState["MY_STATE_3"]); - } + base.TestSimple_SpecialCharacters(); } [Fact] @@ -189,23 +85,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Heredoc() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "heredoc"); - var content = new List - { - "MY_STATE< - { - "MY_STATE< - { - string.Empty, - "MY_STATE< - { - "MY_STATE<<=EOF", - "hello", - "one", - "=EOF", - "MY_STATE_2<< - { - "MY_STATE<(() => _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null)); - Assert.Contains("Matching delimiter not found", ex.Message); - } + base.TestHeredoc_MissingNewLine(); } [Fact] @@ -330,21 +162,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Heredoc_MissingNewLineMultipleLines() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "heredoc"); - var content = new List - { - "MY_STATE<(() => _saveStateFileCommand.ProcessCommand(_executionContext.Object, stateFile, null)); - Assert.Contains("EOF marker missing new line", ex.Message); - } + base.TestHeredoc_MissingNewLineMultipleLines(); } #if OS_WINDOWS @@ -353,90 +171,9 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Heredoc_PreservesNewline() { - using (var hostContext = Setup()) - { - var newline = "\n"; - var stateFile = Path.Combine(_rootDirectory, "heredoc"); - var content = new List - { - "MY_STATE< 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>(); - _intraActionState = new Dictionary(); - - 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(); - _executionContext.Setup(x => x.Global) - .Returns(new GlobalContext - { - EnvironmentVariables = new Dictionary(VarUtil.EnvironmentVariableKeyComparer), - WriteDebug = true, - }); - _executionContext.Setup(x => x.AddIssue(It.IsAny(), It.IsAny())) - .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(), It.IsAny())) - .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; - } } } diff --git a/src/Test/L0/Worker/SetEnvFileCommandL0.cs b/src/Test/L0/Worker/SetEnvFileCommandL0.cs index 62e8b6b9b..71625038a 100644 --- a/src/Test/L0/Worker/SetEnvFileCommandL0.cs +++ b/src/Test/L0/Worker/SetEnvFileCommandL0.cs @@ -1,43 +1,25 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Runtime.CompilerServices; -using GitHub.Runner.Common.Util; -using GitHub.Runner.Sdk; using GitHub.Runner.Worker; -using GitHub.Runner.Worker.Container; -using GitHub.Runner.Worker.Handlers; -using Moq; using Xunit; -using DTWebApi = GitHub.DistributedTask.WebApi; namespace GitHub.Runner.Common.Tests.Worker { - public sealed class SetEnvFileCommandL0 + public sealed class SetEnvFileCommandL0 : FileCommandTestBase { - private Mock _executionContext; - private List> _issues; - private string _rootDirectory; - private SetEnvFileCommand _setEnvFileCommand; - private ITraceWriter _trace; + + protected override IDictionary PostSetup() + { + return _executionContext.Object.Global.EnvironmentVariables; + } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void SetEnvFileCommand_DirectoryNotFound() { - using (var hostContext = Setup()) - { - var envFile = Path.Combine(_rootDirectory, "directory-not-found", "env"); - _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(0, _executionContext.Object.Global.EnvironmentVariables.Count); - } + base.TestDirectoryNotFound(); } [Fact] @@ -45,13 +27,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_NotFound() { - using (var hostContext = Setup()) - { - var envFile = Path.Combine(_rootDirectory, "file-not-found"); - _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(0, _executionContext.Object.Global.EnvironmentVariables.Count); - } + base.TestNotFound(); } [Fact] @@ -59,15 +35,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_EmptyFile() { - using (var hostContext = Setup()) - { - var envFile = Path.Combine(_rootDirectory, "empty-file"); - var content = new List(); - WriteContent(envFile, content); - _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(0, _executionContext.Object.Global.EnvironmentVariables.Count); - } + base.TestEmptyFile(); } [Fact] @@ -75,19 +43,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Simple() { - using (var hostContext = Setup()) - { - var envFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - "MY_ENV=MY VALUE", - }; - WriteContent(envFile, content); - _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count); - Assert.Equal("MY VALUE", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]); - } + base.TestSimple(); } [Fact] @@ -95,24 +51,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Simple_SkipEmptyLines() { - using (var hostContext = Setup()) - { - var envFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - string.Empty, - "MY_ENV=my value", - string.Empty, - "MY_ENV_2=my second value", - string.Empty, - }; - WriteContent(envFile, content); - _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(2, _executionContext.Object.Global.EnvironmentVariables.Count); - Assert.Equal("my value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]); - Assert.Equal("my second value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]); - } + base.TestSimple_SkipEmptyLines(); } [Fact] @@ -120,19 +59,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Simple_EmptyValue() { - using (var hostContext = Setup()) - { - var envFile = Path.Combine(_rootDirectory, "simple-empty-value"); - var content = new List - { - "MY_ENV=", - }; - WriteContent(envFile, content); - _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(1, _executionContext.Object.Global.EnvironmentVariables.Count); - Assert.Equal(string.Empty, _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]); - } + base.TestSimple_EmptyValue(); } [Fact] @@ -140,23 +67,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Simple_MultipleValues() { - using (var hostContext = Setup()) - { - var envFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - "MY_ENV=my value", - "MY_ENV_2=", - "MY_ENV_3=my third value", - }; - WriteContent(envFile, content); - _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(3, _executionContext.Object.Global.EnvironmentVariables.Count); - Assert.Equal("my value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]); - Assert.Equal(string.Empty, _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]); - Assert.Equal("my third value", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_3"]); - } + base.TestSimple_MultipleValues(); } [Fact] @@ -164,23 +75,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Simple_SpecialCharacters() { - using (var hostContext = Setup()) - { - var envFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - "MY_ENV==abc", - "MY_ENV_2=def=ghi", - "MY_ENV_3=jkl=", - }; - WriteContent(envFile, content); - _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(3, _executionContext.Object.Global.EnvironmentVariables.Count); - Assert.Equal("=abc", _executionContext.Object.Global.EnvironmentVariables["MY_ENV"]); - Assert.Equal("def=ghi", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_2"]); - Assert.Equal("jkl=", _executionContext.Object.Global.EnvironmentVariables["MY_ENV_3"]); - } + base.TestSimple_SpecialCharacters(); } [Fact] @@ -188,23 +83,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Heredoc() { - using (var hostContext = Setup()) - { - var envFile = Path.Combine(_rootDirectory, "heredoc"); - var content = new List - { - "MY_ENV< - { - "MY_ENV< - { - string.Empty, - "MY_ENV< - { - "MY_ENV<<=EOF", - "hello", - "one", - "=EOF", - "MY_ENV_2<< - { - "MY_ENV<(() => _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null)); - Assert.Contains("Matching delimiter not found", ex.Message); - } + base.TestHeredoc_MissingNewLine(); } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public void SetEnvFileCommand_Heredoc_MissingNewLineMultipleLinesEnv() + public void SetEnvFileCommand_Heredoc_MissingNewLineMultipleLines() { - using (var hostContext = Setup()) - { - var envFile = Path.Combine(_rootDirectory, "heredoc"); - var content = new List - { - "MY_ENV<(() => _setEnvFileCommand.ProcessCommand(_executionContext.Object, envFile, null)); - Assert.Contains("EOF marker missing new line", ex.Message); - } + base.TestHeredoc_MissingNewLineMultipleLines(); } #if OS_WINDOWS @@ -352,87 +169,9 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Heredoc_PreservesNewline() { - using (var hostContext = Setup()) - { - var newline = "\n"; - var envFile = Path.Combine(_rootDirectory, "heredoc"); - var content = new List - { - "MY_ENV< 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>(); - - 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(); - _executionContext.Setup(x => x.Global) - .Returns(new GlobalContext - { - EnvironmentVariables = new Dictionary(VarUtil.EnvironmentVariableKeyComparer), - WriteDebug = true, - }); - _executionContext.Setup(x => x.AddIssue(It.IsAny(), It.IsAny())) - .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(), It.IsAny())) - .Callback((string tag, string message) => - { - _trace.Info($"{tag}{message}"); - }); - - // SetEnvFileCommand - _setEnvFileCommand = new SetEnvFileCommand(); - _setEnvFileCommand.Initialize(hostContext); - - return hostContext; - } } } diff --git a/src/Test/L0/Worker/SetOutputFileCommandL0.cs b/src/Test/L0/Worker/SetOutputFileCommandL0.cs index 67e6c5907..910a97b93 100644 --- a/src/Test/L0/Worker/SetOutputFileCommandL0.cs +++ b/src/Test/L0/Worker/SetOutputFileCommandL0.cs @@ -1,44 +1,36 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Runtime.CompilerServices; -using GitHub.Runner.Common.Util; -using GitHub.Runner.Sdk; using GitHub.Runner.Worker; -using GitHub.Runner.Worker.Container; -using GitHub.Runner.Worker.Handlers; using Moq; using Xunit; -using DTWebApi = GitHub.DistributedTask.WebApi; namespace GitHub.Runner.Common.Tests.Worker { - public sealed class SetOutputFileCommandL0 + public sealed class SetOutputFileCommandL0 : FileCommandTestBase { - private Mock _executionContext; - private List> _issues; - private Dictionary _outputs; - private string _rootDirectory; - private SetOutputFileCommand _setOutputFileCommand; - private ITraceWriter _trace; + + protected override IDictionary PostSetup() + { + var outputs = new Dictionary(); + var reference = string.Empty; + _executionContext.Setup(x => x.SetOutput(It.IsAny(), It.IsAny(), out reference)) + .Callback((string name, string value, out string reference) => + { + reference = value; + outputs[name] = value; + }); + + return outputs; + + } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void SetOutputFileCommand_DirectoryNotFound() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "directory-not-found", "env"); - _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(0, _outputs.Count); - } + base.TestDirectoryNotFound(); } [Fact] @@ -46,13 +38,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_NotFound() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "file-not-found"); - _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(0, _outputs.Count); - } + base.TestNotFound(); } [Fact] @@ -60,15 +46,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_EmptyFile() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "empty-file"); - var content = new List(); - WriteContent(stateFile, content); - _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(0, _outputs.Count); - } + base.TestEmptyFile(); } [Fact] @@ -76,19 +54,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Simple() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - "MY_OUTPUT=MY VALUE", - }; - WriteContent(stateFile, content); - _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(1, _outputs.Count); - Assert.Equal("MY VALUE", _outputs["MY_OUTPUT"]); - } + base.TestSimple(); } [Fact] @@ -96,24 +62,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Simple_SkipEmptyLines() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - string.Empty, - "MY_OUTPUT=my value", - string.Empty, - "MY_OUTPUT_2=my second value", - string.Empty, - }; - WriteContent(stateFile, content); - _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(2, _outputs.Count); - Assert.Equal("my value", _outputs["MY_OUTPUT"]); - Assert.Equal("my second value", _outputs["MY_OUTPUT_2"]); - } + base.TestSimple_SkipEmptyLines(); } [Fact] @@ -121,19 +70,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Simple_EmptyValue() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "simple-empty-value"); - var content = new List - { - "MY_OUTPUT=", - }; - WriteContent(stateFile, content); - _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(1, _outputs.Count); - Assert.Equal(string.Empty, _outputs["MY_OUTPUT"]); - } + base.TestSimple_EmptyValue(); } [Fact] @@ -141,23 +78,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Simple_MultipleValues() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - "MY_OUTPUT=my value", - "MY_OUTPUT_2=", - "MY_OUTPUT_3=my third value", - }; - WriteContent(stateFile, content); - _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(3, _outputs.Count); - Assert.Equal("my value", _outputs["MY_OUTPUT"]); - Assert.Equal(string.Empty, _outputs["MY_OUTPUT_2"]); - Assert.Equal("my third value", _outputs["MY_OUTPUT_3"]); - } + base.TestSimple_MultipleValues(); } [Fact] @@ -165,23 +86,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Simple_SpecialCharacters() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "simple"); - var content = new List - { - "MY_OUTPUT==abc", - "MY_OUTPUT_2=def=ghi", - "MY_OUTPUT_3=jkl=", - }; - WriteContent(stateFile, content); - _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null); - Assert.Equal(0, _issues.Count); - Assert.Equal(3, _outputs.Count); - Assert.Equal("=abc", _outputs["MY_OUTPUT"]); - Assert.Equal("def=ghi", _outputs["MY_OUTPUT_2"]); - Assert.Equal("jkl=", _outputs["MY_OUTPUT_3"]); - } + base.TestSimple_SpecialCharacters(); } [Fact] @@ -189,23 +94,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Heredoc() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "heredoc"); - var content = new List - { - "MY_OUTPUT< - { - "MY_OUTPUT< - { - string.Empty, - "MY_OUTPUT< - { - "MY_OUTPUT<<=EOF", - "hello", - "one", - "=EOF", - "MY_OUTPUT_2<< - { - "MY_OUTPUT<(() => _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null)); - Assert.Contains("Matching delimiter not found", ex.Message); - } + base.TestHeredoc_MissingNewLine(); } [Fact] @@ -330,21 +171,7 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Heredoc_MissingNewLineMultipleLines() { - using (var hostContext = Setup()) - { - var stateFile = Path.Combine(_rootDirectory, "heredoc"); - var content = new List - { - "MY_OUTPUT<(() => _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null)); - Assert.Contains("EOF marker missing new line", ex.Message); - } + base.TestHeredoc_MissingNewLineMultipleLines(); } #if OS_WINDOWS @@ -353,96 +180,9 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Heredoc_PreservesNewline() { - using (var hostContext = Setup()) - { - var newline = "\n"; - var stateFile = Path.Combine(_rootDirectory, "heredoc"); - var content = new List - { - "MY_OUTPUT< 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>(); - _outputs = new Dictionary(); - - 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(); - _executionContext.Setup(x => x.Global) - .Returns(new GlobalContext - { - EnvironmentVariables = new Dictionary(VarUtil.EnvironmentVariableKeyComparer), - WriteDebug = true, - }); - _executionContext.Setup(x => x.AddIssue(It.IsAny(), It.IsAny())) - .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(), It.IsAny())) - .Callback((string tag, string message) => - { - _trace.Info($"{tag}{message}"); - }); - - var reference = string.Empty; - _executionContext.Setup(x => x.SetOutput(It.IsAny(), It.IsAny(), out reference)) - .Callback((string name, string value, out string reference) => - { - reference = value; - _outputs[name] = value; - }); - - // SetOutputFileCommand - _setOutputFileCommand = new SetOutputFileCommand(); - _setOutputFileCommand.Initialize(hostContext); - - return hostContext; - } } }