using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; 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 OutputManagerL0 { private Mock _executionContext; private Mock _commandManager; private Variables _variables; private OnMatcherChanged _onMatcherChanged; private List> _issues; private List _messages; private List _commands; private OutputManager _outputManager; [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void AddMatcher_Clobber() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = "ERROR: (.+)", Message = 1, }, }, }, new IssueMatcherConfig { Owner = "my-matcher-2", Patterns = new[] { new IssuePatternConfig { Pattern = "NOT GOOD: (.+)", Message = 1, }, }, }, }, }; using (Setup(matchers: matchers)) using (_outputManager) { Process("ERROR: message 1"); Process("NOT GOOD: message 2"); Add(new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = "ERROR: (.+) END MESSAGE", Message = 1, }, }, }); Process("ERROR: message 3 END MESSAGE"); Process("ERROR: message 4"); Process("NOT GOOD: message 5"); Assert.Equal(4, _issues.Count); Assert.Equal("message 1", _issues[0].Item1.Message); Assert.Equal("message 2", _issues[1].Item1.Message); Assert.Equal("message 3", _issues[2].Item1.Message); Assert.Equal("message 5", _issues[3].Item1.Message); Assert.Equal(0, _commands.Count); Assert.Equal(1, _messages.Count); Assert.Equal("ERROR: message 4", _messages[0]); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void AddMatcher_Prepend() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = "ERROR: (.+)", Message = 1, }, }, }, new IssueMatcherConfig { Owner = "my-matcher-2", Patterns = new[] { new IssuePatternConfig { Pattern = "NOT GOOD: (.+)", Message = 1, }, }, }, }, }; using (Setup(matchers: matchers)) using (_outputManager) { Process("ERROR: message 1"); Process("NOT GOOD: message 2"); Add(new IssueMatcherConfig { Owner = "new-matcher", Patterns = new[] { new IssuePatternConfig { Pattern = "ERROR: (.+) END MESSAGE", Message = 1, }, }, }); Process("ERROR: message 3 END MESSAGE"); Process("ERROR: message 4"); Process("NOT GOOD: message 5"); Assert.Equal(5, _issues.Count); Assert.Equal("message 1", _issues[0].Item1.Message); Assert.Equal("message 2", _issues[1].Item1.Message); Assert.Equal("message 3", _issues[2].Item1.Message); Assert.Equal("message 4", _issues[3].Item1.Message); Assert.Equal("message 5", _issues[4].Item1.Message); Assert.Equal(0, _commands.Count); Assert.Equal(0, _messages.Count); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void MatcherCode() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = @"(.*): (.+)", Code = 1, Message = 2, }, }, }, }, }; using (Setup(matchers: matchers)) using (_outputManager) { Process("BAD: real bad"); Process(": not working"); Assert.Equal(2, _issues.Count); Assert.Equal("real bad", _issues[0].Item1.Message); Assert.Equal("BAD", _issues[0].Item1.Data["code"]); Assert.Equal("not working", _issues[1].Item1.Message); Assert.False(_issues[1].Item1.Data.ContainsKey("code")); Assert.Equal(0, _commands.Count); Assert.Equal(0, _messages.Count); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void DoesNotResetMatchingMatcher() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = "Start: .+", }, new IssuePatternConfig { Pattern = "Error: (.+)", Message = 1, Loop = true, }, }, }, }, }; using (Setup(matchers: matchers)) using (_outputManager) { Process("Start: hello"); Process("Error: it broke"); Process("Error: oh no"); Process("Error: not good"); Process("regular message 1"); Process("Start: hello again"); Process("Error: it broke again"); Process("Error: real bad"); Process("regular message 2"); Assert.Equal(5, _issues.Count); Assert.Equal("it broke", _issues[0].Item1.Message); Assert.Equal("oh no", _issues[1].Item1.Message); Assert.Equal("not good", _issues[2].Item1.Message); Assert.Equal("it broke again", _issues[3].Item1.Message); Assert.Equal("real bad", _issues[4].Item1.Message); Assert.Equal(0, _commands.Count); Assert.Equal(4, _messages.Count); Assert.Equal("Start: hello", _messages[0]); Assert.Equal("regular message 1", _messages[1]); Assert.Equal("Start: hello again", _messages[2]); Assert.Equal("regular message 2", _messages[3]); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void InitialMatchers() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = "ERROR: (.+)", Message = 1, }, }, }, new IssueMatcherConfig { Owner = "my-matcher-2", Patterns = new[] { new IssuePatternConfig { Pattern = "NOT GOOD: (.+)", Message = 1, }, }, }, }, }; using (Setup(matchers: matchers)) using (_outputManager) { Process("ERROR: it is broken"); Process("NOT GOOD: that did not work"); Assert.Equal(2, _issues.Count); Assert.Equal("it is broken", _issues[0].Item1.Message); Assert.Equal("that did not work", _issues[1].Item1.Message); Assert.Equal(0, _commands.Count); Assert.Equal(0, _messages.Count); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void MatcherLineColumn() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = @"\((.+),(.+)\): (.+)", Line = 1, Column = 2, Message = 3, }, }, }, }, }; using (Setup(matchers: matchers)) using (_outputManager) { Process("(12,34): real bad"); Process("(12,thirty-four): it is broken"); Process("(twelve,34): not working"); Assert.Equal(3, _issues.Count); Assert.Equal("real bad", _issues[0].Item1.Message); Assert.Equal("12", _issues[0].Item1.Data["line"]); Assert.Equal("34", _issues[0].Item1.Data["col"]); Assert.Equal("it is broken", _issues[1].Item1.Message); Assert.Equal("12", _issues[1].Item1.Data["line"]); Assert.False(_issues[1].Item1.Data.ContainsKey("col")); Assert.Equal("not working", _issues[2].Item1.Message); Assert.False(_issues[2].Item1.Data.ContainsKey("line")); Assert.Equal("34", _issues[2].Item1.Data["col"]); Assert.Equal(0, _commands.Count); Assert.Equal(2, _messages.Count); Assert.Equal("##[debug]Unable to parse column number 'thirty-four'", _messages[0]); Assert.Equal("##[debug]Unable to parse line number 'twelve'", _messages[1]); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void MatcherDoesNotReceiveCommand() { using (Setup()) using (_outputManager) { Add(new IssueMatcherConfig { Owner = "my-matcher", Patterns = new[] { new IssuePatternConfig { Pattern = "ERROR: (.+)", Message = 1, }, }, }); Process("this line is an ERROR: it is broken"); Process("##[some-command]this line is a command even though it contains ERROR: not working"); Process("this line is a command too ##[some-command]even though it contains ERROR: not working again"); Process("##[not-command]this line is an ERROR: it is broken again"); Assert.Equal(2, _issues.Count); Assert.Equal("it is broken", _issues[0].Item1.Message); Assert.Equal("it is broken again", _issues[1].Item1.Message); Assert.Equal(2, _commands.Count); Assert.Equal("##[some-command]this line is a command even though it contains ERROR: not working", _commands[0]); Assert.Equal("this line is a command too ##[some-command]even though it contains ERROR: not working again", _commands[1]); Assert.Equal(0, _messages.Count); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void MatcherRemoveColorCodes() { using (Setup()) using (_outputManager) { Add(new IssueMatcherConfig { Owner = "my-matcher", Patterns = new[] { new IssuePatternConfig { Pattern = "^the error: (.+)$", Message = 1, }, }, }); Process("the error: \033[31mred, \033[1;31mbright red, \033[mreset"); Assert.Equal(1, _issues.Count); Assert.Equal("red, bright red, reset", _issues[0].Item1.Message); Assert.Equal("the error: red, bright red, reset", _issues[0].Item2); Assert.Equal(0, _commands.Count); Assert.Equal(0, _messages.Count); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void RemoveMatcher() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = "ERROR: (.+)", Message = 1, }, }, }, new IssueMatcherConfig { Owner = "my-matcher-2", Patterns = new[] { new IssuePatternConfig { Pattern = "NOT GOOD: (.+)", Message = 1, }, }, }, }, }; using (Setup(matchers: matchers)) using (_outputManager) { Process("ERROR: message 1"); Process("NOT GOOD: message 2"); Remove("my-matcher-1"); Process("ERROR: message 3"); Process("NOT GOOD: message 4"); Assert.Equal(3, _issues.Count); Assert.Equal("message 1", _issues[0].Item1.Message); Assert.Equal("message 2", _issues[1].Item1.Message); Assert.Equal("message 4", _issues[2].Item1.Message); Assert.Equal(0, _commands.Count); Assert.Equal(1, _messages.Count); Assert.Equal("ERROR: message 3", _messages[0]); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void ResetsOtherMatchers() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = "Matches both line 1: .+", }, new IssuePatternConfig { Pattern = "Matches 1 only line 2: (.+)", Message = 1, }, }, }, new IssueMatcherConfig { Owner = "my-matcher-2", Patterns = new[] { new IssuePatternConfig { Pattern = "Matches both line 1: (.+)", }, new IssuePatternConfig { Pattern = "(.+)", Message = 1, }, }, }, }, }; using (Setup(matchers: matchers)) using (_outputManager) { Process("Matches both line 1: hello"); Process("Matches 1 only line 2: it broke"); Process("regular message 1"); Process("regular message 2"); Process("Matches both line 1: hello again"); Process("oh no, another error"); Assert.Equal(2, _issues.Count); Assert.Equal("it broke", _issues[0].Item1.Message); Assert.Equal("oh no, another error", _issues[1].Item1.Message); Assert.Equal(0, _commands.Count); Assert.Equal(4, _messages.Count); Assert.Equal("Matches both line 1: hello", _messages[0]); Assert.Equal("regular message 1", _messages[1]); Assert.Equal("regular message 2", _messages[2]); Assert.Equal("Matches both line 1: hello again", _messages[3]); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void MatcherSeverity() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = "(.*): (.+)", Severity = 1, Message = 2, }, }, }, new IssueMatcherConfig { Owner = "my-matcher-2", Patterns = new[] { new IssuePatternConfig { Pattern = "ERROR! (.+)", Message = 1, }, }, }, }, }; using (Setup(matchers: matchers)) using (_outputManager) { Process("ERRor: real bad"); Process("WARNing: not great"); Process("info: hey"); Process(": not working"); Process("ERROR! uh oh"); Assert.Equal(4, _issues.Count); Assert.Equal("real bad", _issues[0].Item1.Message); Assert.Equal(DTWebApi.IssueType.Error, _issues[0].Item1.Type); Assert.Equal("not great", _issues[1].Item1.Message); Assert.Equal(DTWebApi.IssueType.Warning, _issues[1].Item1.Type); Assert.Equal("not working", _issues[2].Item1.Message); Assert.Equal(DTWebApi.IssueType.Error, _issues[2].Item1.Type); Assert.Equal("uh oh", _issues[3].Item1.Message); Assert.Equal(DTWebApi.IssueType.Error, _issues[3].Item1.Type); Assert.Equal(0, _commands.Count); Assert.Equal(2, _messages.Count); Assert.StartsWith("##[debug]Skipped", _messages[0]); Assert.Contains("'info'", _messages[0]); Assert.Equal("info: hey", _messages[1]); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void MatcherTimeout() { Environment.SetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_ISSUE_MATCHER_TIMEOUT", "0:0:0.01"); var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "email", Patterns = new[] { new IssuePatternConfig { Pattern = @"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$", Message = 0, }, }, }, new IssueMatcherConfig { Owner = "err", Patterns = new[] { new IssuePatternConfig { Pattern = @"ERR: (.+)", Message = 1, }, }, }, }, }; using (Setup(matchers: matchers)) using (_outputManager) { Process("john.doe@contoso.com"); Process("t@t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.c%20"); Process("jane.doe@contoso.com"); Process("ERR: this error"); Assert.Equal(3, _issues.Count); Assert.Equal("john.doe@contoso.com", _issues[0].Item1.Message); Assert.Contains("Removing issue matcher 'email'", _issues[1].Item1.Message); Assert.Equal("this error", _issues[2].Item1.Message); Assert.Equal(0, _commands.Count); Assert.Equal(2, _messages.Where(x => x.StartsWith("##[debug]Timeout processing issue matcher")).Count()); Assert.Equal(1, _messages.Where(x => x.Equals("t@t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.c%20")).Count()); Assert.Equal(1, _messages.Where(x => x.StartsWith("jane.doe@contoso.com")).Count()); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public async void MatcherFile() { Environment.SetEnvironmentVariable("RUNNER_TEST_GET_REPOSITORY_PATH_FAILSAFE", "2"); var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = @"(.+): (.+)", File = 1, Message = 2, }, }, }, }, }; using (var hostContext = Setup(matchers: matchers)) using (_outputManager) { // Setup github.workspace, github.repository var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work); ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory)); Directory.CreateDirectory(workDirectory); var workspaceDirectory = Path.Combine(workDirectory, "workspace"); Directory.CreateDirectory(workspaceDirectory); _executionContext.Setup(x => x.GetGitHubContext("workspace")).Returns(workspaceDirectory); _executionContext.Setup(x => x.GetGitHubContext("repository")).Returns("my-org/workflow-repo"); // Setup some git repositories // /workflow-repo // /workflow-repo/nested-other-repo // /other-repo // /other-repo/nested-workflow-repo // /workflow-repo-using-ssh var workflowRepository = Path.Combine(workspaceDirectory, "workflow-repo"); var nestedOtherRepository = Path.Combine(workspaceDirectory, "workflow-repo", "nested-other-repo"); var otherRepository = Path.Combine(workspaceDirectory, workflowRepository, "nested-other-repo"); var nestedWorkflowRepository = Path.Combine(workspaceDirectory, "other-repo", "nested-workflow-repo"); var workflowRepositoryUsingSsh = Path.Combine(workspaceDirectory, "workflow-repo-using-ssh"); await CreateRepository(hostContext, workflowRepository, "https://github.com/my-org/workflow-repo"); await CreateRepository(hostContext, nestedOtherRepository, "https://github.com/my-org/other-repo"); await CreateRepository(hostContext, otherRepository, "https://github.com/my-org/other-repo"); await CreateRepository(hostContext, nestedWorkflowRepository, "https://github.com/my-org/workflow-repo"); await CreateRepository(hostContext, workflowRepositoryUsingSsh, "git@github.com:my-org/workflow-repo.git"); // Create test files var file_noRepository = Path.Combine(workspaceDirectory, "no-repo.txt"); var file_workflowRepository = Path.Combine(workflowRepository, "workflow-repo.txt"); var file_workflowRepository_nestedDirectory = Path.Combine(workflowRepository, "subdir", "subdir2", "workflow-repo-nested-dir.txt"); var file_workflowRepository_failsafe = Path.Combine(workflowRepository, "failsafe-subdir", "failsafe-subdir2", "failsafe-subdir3", "workflow-repo-failsafe.txt"); var file_nestedOtherRepository = Path.Combine(nestedOtherRepository, "nested-other-repo"); var file_otherRepository = Path.Combine(otherRepository, "other-repo.txt"); var file_nestedWorkflowRepository = Path.Combine(nestedWorkflowRepository, "nested-workflow-repo.txt"); var file_workflowRepositoryUsingSsh = Path.Combine(workflowRepositoryUsingSsh, "workflow-repo-using-ssh.txt"); foreach (var file in new[] { file_noRepository, file_workflowRepository, file_workflowRepository_nestedDirectory, file_workflowRepository_failsafe, file_nestedOtherRepository, file_otherRepository, file_nestedWorkflowRepository, file_workflowRepositoryUsingSsh }) { Directory.CreateDirectory(Path.GetDirectoryName(file)); File.WriteAllText(file, ""); } // Process Process($"{file_noRepository}: some error 1"); Process($"{file_workflowRepository}: some error 2"); Process($"{file_workflowRepository.Substring(workspaceDirectory.Length + 1)}: some error 3"); // Relative path from workspace dir Process($"{file_workflowRepository_nestedDirectory}: some error 4"); Process($"{file_workflowRepository_failsafe}: some error 5"); Process($"{file_nestedOtherRepository}: some error 6"); Process($"{file_otherRepository}: some error 7"); Process($"{file_nestedWorkflowRepository}: some error 8"); Process($"{file_workflowRepositoryUsingSsh}: some error 9"); Assert.Equal(9, _issues.Count); Assert.Equal("some error 1", _issues[0].Item1.Message); Assert.False(_issues[0].Item1.Data.ContainsKey("file")); Assert.Equal("some error 2", _issues[1].Item1.Message); Assert.Equal(file_workflowRepository.Substring(workflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[1].Item1.Data["file"]); Assert.Equal("some error 3", _issues[2].Item1.Message); Assert.Equal(file_workflowRepository.Substring(workflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[2].Item1.Data["file"]); Assert.Equal("some error 4", _issues[3].Item1.Message); Assert.Equal(file_workflowRepository_nestedDirectory.Substring(workflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[3].Item1.Data["file"]); Assert.Equal("some error 5", _issues[4].Item1.Message); Assert.False(_issues[4].Item1.Data.ContainsKey("file")); Assert.Equal("some error 6", _issues[5].Item1.Message); Assert.False(_issues[5].Item1.Data.ContainsKey("file")); Assert.Equal("some error 7", _issues[6].Item1.Message); Assert.False(_issues[6].Item1.Data.ContainsKey("file")); Assert.Equal("some error 8", _issues[7].Item1.Message); Assert.Equal(file_nestedWorkflowRepository.Substring(nestedWorkflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[7].Item1.Data["file"]); Assert.Equal("some error 9", _issues[8].Item1.Message); Assert.Equal(file_workflowRepositoryUsingSsh.Substring(workflowRepositoryUsingSsh.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[8].Item1.Data["file"]); } Environment.SetEnvironmentVariable("RUNNER_TEST_GET_REPOSITORY_PATH_FAILSAFE", ""); } #if OS_LINUX [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public async void MatcherFile_JobContainer() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = @"(.+): (.+)", File = 1, Message = 2, }, }, }, }, }; var container = new ContainerInfo(); using (var hostContext = Setup(matchers: matchers, jobContainer: container)) using (_outputManager) { // Setup github.workspace, github.repository var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work); ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory)); Directory.CreateDirectory(workDirectory); var workspaceDirectory = Path.Combine(workDirectory, "workspace"); Directory.CreateDirectory(workspaceDirectory); _executionContext.Setup(x => x.GetGitHubContext("workspace")).Returns(workspaceDirectory); _executionContext.Setup(x => x.GetGitHubContext("repository")).Returns("my-org/workflow-repo"); // Setup a git repository await CreateRepository(hostContext, workspaceDirectory, "https://github.com/my-org/workflow-repo"); // Create test files var file = Path.Combine(workspaceDirectory, "some-file.txt"); File.WriteAllText(file, ""); // Add translation path container.AddPathTranslateMapping(workspaceDirectory, "/container/path/to/workspace"); // Process Process($"/container/path/to/workspace/some-file.txt: some error 1"); Process($"some-file.txt: some error 2"); Assert.Equal(2, _issues.Count); Assert.Equal("some error 1", _issues[0].Item1.Message); Assert.Equal("some-file.txt", _issues[0].Item1.Data["file"]); Assert.Equal("some error 2", _issues[1].Item1.Message); Assert.Equal("some-file.txt", _issues[1].Item1.Data["file"]); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public async void MatcherFile_StepContainer() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = @"(.+): (.+)", File = 1, Message = 2, }, }, }, }, }; var container = new ContainerInfo(); using (var hostContext = Setup(matchers: matchers, stepContainer: container)) using (_outputManager) { // Setup github.workspace, github.repository var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work); ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory)); Directory.CreateDirectory(workDirectory); var workspaceDirectory = Path.Combine(workDirectory, "workspace"); Directory.CreateDirectory(workspaceDirectory); _executionContext.Setup(x => x.GetGitHubContext("workspace")).Returns(workspaceDirectory); _executionContext.Setup(x => x.GetGitHubContext("repository")).Returns("my-org/workflow-repo"); // Setup a git repository await CreateRepository(hostContext, workspaceDirectory, "https://github.com/my-org/workflow-repo"); // Create test files var file = Path.Combine(workspaceDirectory, "some-file.txt"); File.WriteAllText(file, ""); // Add translation path container.AddPathTranslateMapping(workspaceDirectory, "/container/path/to/workspace"); // Process Process($"/container/path/to/workspace/some-file.txt: some error 1"); Process($"some-file.txt: some error 2"); Assert.Equal(2, _issues.Count); Assert.Equal("some error 1", _issues[0].Item1.Message); Assert.Equal("some-file.txt", _issues[0].Item1.Data["file"]); Assert.Equal("some error 2", _issues[1].Item1.Message); Assert.Equal("some-file.txt", _issues[1].Item1.Data["file"]); } } #endif [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public async void MatcherFromPath() { var matchers = new IssueMatchersConfig { Matchers = { new IssueMatcherConfig { Owner = "my-matcher-1", Patterns = new[] { new IssuePatternConfig { Pattern = @"(.+): (.+) \[(.+)\]", File = 1, Message = 2, FromPath = 3, }, }, }, }, }; using (var hostContext = Setup(matchers: matchers)) using (_outputManager) { // Setup github.workspace, github.repository var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work); ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory)); Directory.CreateDirectory(workDirectory); var workspaceDirectory = Path.Combine(workDirectory, "workspace"); Directory.CreateDirectory(workspaceDirectory); _executionContext.Setup(x => x.GetGitHubContext("workspace")).Returns(workspaceDirectory); _executionContext.Setup(x => x.GetGitHubContext("repository")).Returns("my-org/workflow-repo"); // Setup a git repository var repositoryPath = Path.Combine(workspaceDirectory, "workflow-repo"); await CreateRepository(hostContext, repositoryPath, "https://github.com/my-org/workflow-repo"); // Create a test file var filePath = Path.Combine(repositoryPath, "some-project", "some-directory", "some-file.txt"); Directory.CreateDirectory(Path.GetDirectoryName(filePath)); File.WriteAllText(filePath, ""); // Process Process("some-directory/some-file.txt: some error [workflow-repo/some-project/some-project.proj]"); Assert.Equal(1, _issues.Count); Assert.Equal("some error", _issues[0].Item1.Message); Assert.Equal("some-project/some-directory/some-file.txt", _issues[0].Item1.Data["file"]); Assert.Equal(0, _commands.Count); Assert.Equal(0, _messages.Count); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void CaptureTelemetryForGitUnsafeRepository() { using (Setup()) using (_outputManager) { Process("fatal: unsafe repository ('/github/workspace' is owned by someone else)"); Assert.Contains("fatal: unsafe repository ('/github/workspace' is owned by someone else)", _executionContext.Object.StepTelemetry.ErrorMessages); } } private TestHostContext Setup( [CallerMemberName] string name = "", IssueMatchersConfig matchers = null, ContainerInfo jobContainer = null, ContainerInfo stepContainer = null) { matchers?.Validate(); _onMatcherChanged = null; _issues = new List>(); _messages = new List(); _commands = new List(); var hostContext = new TestHostContext(this, name); _variables = new Variables(hostContext, new Dictionary()); _executionContext = new Mock(); _executionContext.Setup(x => x.Global) .Returns(new GlobalContext { Container = jobContainer, Variables = _variables, WriteDebug = true, }); _executionContext.Setup(x => x.StepTelemetry) .Returns(new DTWebApi.ActionsStepTelemetry()); _executionContext.Setup(x => x.GetMatchers()) .Returns(matchers?.Matchers ?? new List()); _executionContext.Setup(x => x.Add(It.IsAny())) .Callback((OnMatcherChanged handler) => { _onMatcherChanged = handler; }); _executionContext.Setup(x => x.AddIssue(It.IsAny(), It.IsAny())) .Callback((DTWebApi.Issue issue, string logMessage) => { _issues.Add(new Tuple(issue, logMessage)); }); _executionContext.Setup(x => x.Write(It.IsAny(), It.IsAny())) .Callback((string tag, string message) => { _messages.Add($"{tag}{message}"); hostContext.GetTrace().Info($"{tag}{message}"); }); _commandManager = new Mock(); _commandManager.Setup(x => x.TryProcessCommand(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((IExecutionContext executionContext, string line, ContainerInfo container) => { if (line.IndexOf("##[some-command]") >= 0) { _commands.Add(line); return true; } return false; }); _outputManager = new OutputManager(_executionContext.Object, _commandManager.Object, stepContainer); return hostContext; } private void Add(IssueMatcherConfig matcher) { var matchers = new IssueMatchersConfig { Matchers = { matcher, }, }; matchers.Validate(); _onMatcherChanged(null, new MatcherChangedEventArgs(matcher)); } private void Remove(string owner) { var matcher = new IssueMatcherConfig { Owner = owner }; _onMatcherChanged(null, new MatcherChangedEventArgs(matcher)); } private void Process(string line) { _outputManager.OnDataReceived(null, new ProcessDataReceivedEventArgs(line)); } private async Task CreateRepository(TestHostContext hostConetxt, string path, string url) { Directory.CreateDirectory(path); var gitPath = WhichUtil.Which("git", true); var environment = new Dictionary(); using (var processInvoker = new ProcessInvoker(hostConetxt.GetTrace())) { await processInvoker.ExecuteAsync(path, gitPath, "init", environment, CancellationToken.None); } using (var processInvoker = new ProcessInvoker(hostConetxt.GetTrace())) { await processInvoker.ExecuteAsync(path, gitPath, $"remote add origin {url}", environment, CancellationToken.None); } } } }