diff --git a/src/Runner.Worker/FileCommandManager.cs b/src/Runner.Worker/FileCommandManager.cs index 72ed5de0a..a084a5757 100644 --- a/src/Runner.Worker/FileCommandManager.cs +++ b/src/Runner.Worker/FileCommandManager.cs @@ -322,9 +322,21 @@ namespace GitHub.Runner.Worker var equalsIndex = line.IndexOf("=", StringComparison.Ordinal); var heredocIndex = line.IndexOf("<<", StringComparison.Ordinal); - // Heredoc style NAME<= 0 && (equalsIndex < 0 || heredocIndex < equalsIndex)) { var split = line.Split(new[] { "<<" }, 2, StringSplitOptions.None); if (string.IsNullOrEmpty(split[0]) || string.IsNullOrEmpty(split[1])) @@ -352,18 +364,6 @@ namespace GitHub.Runner.Worker output = endIndex > startIndex ? text.Substring(startIndex, endIndex - startIndex) : string.Empty; } - // Normal style NAME=VALUE - else if (equalsIndex >= 0 && heredocIndex < 0) - { - var split = line.Split(new[] { '=' }, 2, StringSplitOptions.None); - if (string.IsNullOrEmpty(line)) - { - throw new Exception($"Invalid format '{line}'. Name must not be empty"); - } - - key = split[0]; - output = split[1]; - } else { throw new Exception($"Invalid format '{line}'"); diff --git a/src/Test/L0/TestUtil.cs b/src/Test/L0/TestUtil.cs index 6d5458782..c6c60be56 100644 --- a/src/Test/L0/TestUtil.cs +++ b/src/Test/L0/TestUtil.cs @@ -1,21 +1,10 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; -using System.Text; +using System.IO; using Xunit; using GitHub.Runner.Sdk; -using System.Linq; +using System.Runtime.CompilerServices; namespace GitHub.Runner.Common.Tests { - public enum LineEndingType - { - Native, - Linux = 0x__0A, - Windows = 0x0D0A - } - public static class TestUtil { private const string Src = "src"; @@ -52,24 +41,5 @@ namespace GitHub.Runner.Common.Tests Assert.True(Directory.Exists(testDataDir)); return testDataDir; } - - public static void WriteContent(string path, string content, LineEndingType lineEnding = LineEndingType.Native) - { - WriteContent(path, Enumerable.Repeat(content, 1), lineEnding); - } - - public static void WriteContent(string path, IEnumerable 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 e1a2c537d..19f956fa8 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", }; - TestUtil.WriteContent(stepSummaryFile, content); + 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", }; - TestUtil.WriteContent(stepSummaryFile, content); + WriteContent(stepSummaryFile, content); _createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null); @@ -167,6 +167,21 @@ 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 deleted file mode 100644 index f3bcd8e73..000000000 --- a/src/Test/L0/Worker/FileCommandTestBase.cs +++ /dev/null @@ -1,420 +0,0 @@ -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 1157e29cc..15cba9d82 100644 --- a/src/Test/L0/Worker/SaveStateFileCommandL0.cs +++ b/src/Test/L0/Worker/SaveStateFileCommandL0.cs @@ -1,27 +1,44 @@ 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 : FileCommandTestBase + public sealed class SaveStateFileCommandL0 { - - protected override IDictionary PostSetup() - { - var intraActionState = new Dictionary(); - _executionContext.Setup(x => x.IntraActionState).Returns(intraActionState); - return intraActionState; - } + private Mock _executionContext; + private List> _issues; + private string _rootDirectory; + private SaveStateFileCommand _saveStateFileCommand; + private Dictionary _intraActionState; + private ITraceWriter _trace; [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void SaveStateFileCommand_DirectoryNotFound() { - base.TestDirectoryNotFound(); + 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] @@ -29,7 +46,13 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_NotFound() { - base.TestNotFound(); + 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] @@ -37,7 +60,15 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_EmptyFile() { - base.TestEmptyFile(); + 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); + } } [Fact] @@ -45,7 +76,19 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Simple() { - base.TestSimple(); + 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"]); + } } [Fact] @@ -53,7 +96,24 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Simple_SkipEmptyLines() { - base.TestSimple_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"]); + } } [Fact] @@ -61,7 +121,19 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Simple_EmptyValue() { - base.TestSimple_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"]); + } } [Fact] @@ -69,7 +141,23 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Simple_MultipleValues() { - base.TestSimple_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"]); + } } [Fact] @@ -77,7 +165,23 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Simple_SpecialCharacters() { - base.TestSimple_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"]); + } } [Fact] @@ -85,7 +189,23 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Heredoc() { - base.TestHeredoc(); + 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); + } } [Fact] @@ -162,7 +330,21 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Heredoc_MissingNewLineMultipleLines() { - base.TestHeredoc_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); + } } #if OS_WINDOWS @@ -171,9 +353,90 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SaveStateFileCommand_Heredoc_PreservesNewline() { - base.TestHeredoc_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 71625038a..62e8b6b9b 100644 --- a/src/Test/L0/Worker/SetEnvFileCommandL0.cs +++ b/src/Test/L0/Worker/SetEnvFileCommandL0.cs @@ -1,25 +1,43 @@ 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 : FileCommandTestBase + public sealed class SetEnvFileCommandL0 { - - protected override IDictionary PostSetup() - { - return _executionContext.Object.Global.EnvironmentVariables; - } + private Mock _executionContext; + private List> _issues; + private string _rootDirectory; + private SetEnvFileCommand _setEnvFileCommand; + private ITraceWriter _trace; [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void SetEnvFileCommand_DirectoryNotFound() { - base.TestDirectoryNotFound(); + 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); + } } [Fact] @@ -27,7 +45,13 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_NotFound() { - base.TestNotFound(); + 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); + } } [Fact] @@ -35,7 +59,15 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_EmptyFile() { - base.TestEmptyFile(); + 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); + } } [Fact] @@ -43,7 +75,19 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Simple() { - base.TestSimple(); + 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"]); + } } [Fact] @@ -51,7 +95,24 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Simple_SkipEmptyLines() { - base.TestSimple_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"]); + } } [Fact] @@ -59,7 +120,19 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Simple_EmptyValue() { - base.TestSimple_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"]); + } } [Fact] @@ -67,7 +140,23 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Simple_MultipleValues() { - base.TestSimple_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"]); + } } [Fact] @@ -75,7 +164,23 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Simple_SpecialCharacters() { - base.TestSimple_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"]); + } } [Fact] @@ -83,7 +188,23 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Heredoc() { - base.TestHeredoc(); + 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); + } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public void SetEnvFileCommand_Heredoc_MissingNewLineMultipleLines() + public void SetEnvFileCommand_Heredoc_MissingNewLineMultipleLinesEnv() { - base.TestHeredoc_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); + } } #if OS_WINDOWS @@ -169,9 +352,87 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetEnvFileCommand_Heredoc_PreservesNewline() { - base.TestHeredoc_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 910a97b93..67e6c5907 100644 --- a/src/Test/L0/Worker/SetOutputFileCommandL0.cs +++ b/src/Test/L0/Worker/SetOutputFileCommandL0.cs @@ -1,36 +1,44 @@ 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 : FileCommandTestBase + public sealed class SetOutputFileCommandL0 { - - 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; - - } + private Mock _executionContext; + private List> _issues; + private Dictionary _outputs; + private string _rootDirectory; + private SetOutputFileCommand _setOutputFileCommand; + private ITraceWriter _trace; [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void SetOutputFileCommand_DirectoryNotFound() { - base.TestDirectoryNotFound(); + 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] @@ -38,7 +46,13 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_NotFound() { - base.TestNotFound(); + 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] @@ -46,7 +60,15 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_EmptyFile() { - base.TestEmptyFile(); + 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); + } } [Fact] @@ -54,7 +76,19 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Simple() { - base.TestSimple(); + 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"]); + } } [Fact] @@ -62,7 +96,24 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Simple_SkipEmptyLines() { - base.TestSimple_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"]); + } } [Fact] @@ -70,7 +121,19 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Simple_EmptyValue() { - base.TestSimple_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"]); + } } [Fact] @@ -78,7 +141,23 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Simple_MultipleValues() { - base.TestSimple_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"]); + } } [Fact] @@ -86,7 +165,23 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Simple_SpecialCharacters() { - base.TestSimple_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"]); + } } [Fact] @@ -94,7 +189,23 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Heredoc() { - base.TestHeredoc(); + 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); + } } [Fact] @@ -171,7 +330,21 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Heredoc_MissingNewLineMultipleLines() { - base.TestHeredoc_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); + } } #if OS_WINDOWS @@ -180,9 +353,96 @@ namespace GitHub.Runner.Common.Tests.Worker [Trait("Category", "Worker")] public void SetOutputFileCommand_Heredoc_PreservesNewline() { - base.TestHeredoc_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; + } } }