Files
runner/src/Test/L0/Worker/OutputManagerL0.cs
2025-05-05 20:45:23 +00:00

1119 lines
48 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Actions.RunService.WebApi;
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<IExecutionContext> _executionContext;
private Mock<IActionCommandManager> _commandManager;
private Variables _variables;
private OnMatcherChanged _onMatcherChanged;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private List<string> _messages;
private List<string> _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
// <WORKSPACE>/workflow-repo
// <WORKSPACE>/workflow-repo/nested-other-repo
// <WORKSPACE>/other-repo
// <WORKSPACE>/other-repo/nested-workflow-repo
// <WORKSPACE>/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 async void MatcherDefaultFromPath()
{
var matchers = new IssueMatchersConfig
{
Matchers =
{
new IssueMatcherConfig
{
Owner = "my-matcher-1",
FromPath = "workflow-repo/some-project/some-project.proj",
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 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");
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<Tuple<DTWebApi.Issue, string>>();
_messages = new List<string>();
_commands = new List<string>();
var hostContext = new TestHostContext(this, name);
_variables = new Variables(hostContext, new Dictionary<string, DTWebApi.VariableValue>());
_executionContext = new Mock<IExecutionContext>();
_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<IssueMatcherConfig>());
_executionContext.Setup(x => x.Add(It.IsAny<OnMatcherChanged>()))
.Callback((OnMatcherChanged handler) =>
{
_onMatcherChanged = handler;
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
{
var resolvedMessage = issue.Message;
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
resolvedMessage = logOptions.LogMessageOverride;
}
_issues.Add(new(issue, resolvedMessage));
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>
{
_messages.Add($"{tag}{message}");
hostContext.GetTrace().Info($"{tag}{message}");
});
_commandManager = new Mock<IActionCommandManager>();
_commandManager.Setup(x => x.TryProcessCommand(It.IsAny<IExecutionContext>(), It.IsAny<string>(), It.IsAny<ContainerInfo>()))
.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<string, string>();
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);
}
}
}
}