// ReSharper disable ClassNeverInstantiated.Global // ReSharper disable InconsistentNaming namespace TeamCity.CSharpInteractive; using System.Diagnostics.CodeAnalysis; using HostApi; using JetBrains.TeamCity.ServiceMessages; internal class BuildContext : IBuildContext { private readonly List _errors = new(); private readonly List _warnings = new(); private readonly List _tests = new(); private readonly HashSet _testKeys = new(); private readonly Dictionary _currentTests = new(); public IReadOnlyList ProcessMessage(in Output output, IServiceMessage message) => ProcessMessage(output.StartInfo, output.ProcessId, message); [SuppressMessage("ReSharper", "StringLiteralTypo")] public IReadOnlyList ProcessMessage(IStartInfo startInfo, int processId, IServiceMessage message) => ( message.Name.ToLowerInvariant() switch { "teststdout" => OnStdOut(message, startInfo, processId), "teststderr" => OnStdErr(message, startInfo, processId), "testfinished" => OnTestFinished(message), "testignored" => OnTestIgnored(message), "testfailed" => OnTestFailed(message), "message" => OnMessage(message), "buildproblem" => OnBuildProblem(message), _ => Enumerable.Empty() }).ToArray(); public IReadOnlyList ProcessOutput(in Output output) { BuildMessage message; if (output.IsError) { message = new BuildMessage(BuildMessageState.StdError, default, output.Line); _errors.Add(message); } else { message = new BuildMessage(BuildMessageState.StdOut, default, output.Line); } return new[] {message}; } public IBuildResult Create(IStartInfo startInfo, int? exitCode) => new BuildResult(startInfo, _errors.AsReadOnly(), _warnings.AsReadOnly(), _tests.AsReadOnly(), exitCode); private IEnumerable OnStdOut(IServiceMessage message, IStartInfo startInfo, int processId) { var testKey = CreateKey(message); var output = message.GetValue("out") ?? string.Empty; GetTestContext(testKey).AddStdOut(startInfo, processId, output); yield return new BuildMessage(BuildMessageState.StdOut).WithText(output); } private IEnumerable OnStdErr(IServiceMessage message, IStartInfo info, int processId) { var testKey = CreateKey(message); var output = message.GetValue("out") ?? string.Empty; GetTestContext(testKey).AddStdErr(info, processId, output); var buildMessage = new BuildMessage(BuildMessageState.StdError).WithText(output); _errors.Add(buildMessage); yield return buildMessage; } private IEnumerable OnTestFinished(IServiceMessage message) { var testKey = CreateKey(message); if (_testKeys.Remove(testKey)) { yield break; } var ctx = GetTestContext(testKey, true); var durationStrMs = message.GetValue("duration"); var duration = TimeSpan.Zero; if (!string.IsNullOrWhiteSpace(durationStrMs) && int.TryParse(durationStrMs, out var durationMs)) { duration = TimeSpan.FromMilliseconds(durationMs); } _tests.Add(CreateResult(testKey, message, TestState.Passed).WithDuration(duration).WithOutput(ctx.Output)); } private IEnumerable OnTestIgnored(IServiceMessage message) { var testKey = CreateKey(message); _testKeys.Add(testKey); var ctx = GetTestContext(testKey, true); _tests.Add(CreateResult(testKey, message, TestState.Ignored).WithMessage(message.GetValue("message") ?? string.Empty).WithOutput(ctx.Output)); yield break; } private IEnumerable OnTestFailed(IServiceMessage message) { var testKey = CreateKey(message); _testKeys.Add(testKey); var ctx = GetTestContext(testKey, true); _tests.Add(CreateResult(testKey, message, TestState.Failed).WithMessage(message.GetValue("message") ?? string.Empty).WithDetails(message.GetValue("details") ?? string.Empty).WithOutput(ctx.Output)); yield break; } private static TestResult CreateResult(TestKey key, IServiceMessage message, TestState state) { var testSource = message.GetValue("testSource") ?? string.Empty; var displayName = message.GetValue("displayName") ?? string.Empty; var codeFilePath = message.GetValue("codeFilePath") ?? string.Empty; var fullyQualifiedName = message.GetValue("fullyQualifiedName") ?? string.Empty; var (flowId, suiteName, testName) = key; var result = new TestResult(state, testName) .WithSuiteName(suiteName) .WithFlowId(flowId) .WithSource(testSource) .WithDisplayName(displayName) .WithCodeFilePath(codeFilePath) .WithFullyQualifiedName(fullyQualifiedName); if (Guid.TryParse(message.GetValue("id"), out var id)) { result = result.WithId(id); } if (Uri.TryCreate(message.GetValue("executorUri"), UriKind.RelativeOrAbsolute, out var executorUri)) { result = result.WithExecutorUri(executorUri); } if (int.TryParse(message.GetValue("lineNumber"), out var lineNumber)) { result = result.WithLineNumber(lineNumber); } return result; } private IEnumerable OnMessage(IServiceMessage message) { var text = message.GetValue("text") ?? string.Empty; var state = message.GetValue("status").ToUpperInvariant() switch { "WARNING" => BuildMessageState.Warning, "FAILURE" => BuildMessageState.Failure, "ERROR" => BuildMessageState.StdError, _ => BuildMessageState.StdOut }; var buildMessage = CreateMessage(message, state, text); // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault if (!string.IsNullOrWhiteSpace(buildMessage.Text)) { switch (state) { case BuildMessageState.Warning: _warnings.Add(buildMessage); break; case BuildMessageState.Failure: case BuildMessageState.StdError: // ReSharper disable once UnreachableSwitchCaseDueToIntegerAnalysis case BuildMessageState.BuildProblem: _errors.Add(buildMessage); break; } } yield return buildMessage; } private IEnumerable OnBuildProblem(IServiceMessage message) { var description = message.GetValue("description") ?? string.Empty; var buildMessage = CreateMessage(message, BuildMessageState.BuildProblem, description); _errors.Add(buildMessage); yield return buildMessage; } private static BuildMessage CreateMessage(IServiceMessage message, BuildMessageState state, string text) { var buildMessage = new BuildMessage( state, default, text, message.GetValue("errorDetails") ?? string.Empty, message.GetValue("code") ?? string.Empty, message.GetValue("file") ?? string.Empty, message.GetValue("subcategory") ?? string.Empty, message.GetValue("projectFile") ?? string.Empty, message.GetValue("senderName") ?? string.Empty); if (int.TryParse(message.GetValue("columnNumber"), out var columnNumber)) { buildMessage = buildMessage.WithColumnNumber(columnNumber); } if (int.TryParse(message.GetValue("endColumnNumber"), out var endColumnNumber)) { buildMessage = buildMessage.WithEndColumnNumber(endColumnNumber); } if (int.TryParse(message.GetValue("lineNumber"), out var lineNumber)) { buildMessage = buildMessage.WithLineNumber(lineNumber); } if (int.TryParse(message.GetValue("endLineNumber"), out var endLineNumber)) { buildMessage = buildMessage.WithEndLineNumber(endLineNumber); } if (Enum.TryParse(message.GetValue("importance"), out var importance)) { buildMessage = buildMessage.WithImportance(importance); } return buildMessage; } private TestContext GetTestContext(TestKey testKey, bool remove = false) { if (!_currentTests.TryGetValue(testKey, out var testContext)) { testContext = new TestContext(); if (!remove) { _currentTests.Add(testKey, testContext); } } else { if (remove) { _currentTests.Remove(testKey); } } return testContext; } private static TestKey CreateKey(IServiceMessage message) { var flowId = message.GetValue("flowId") ?? string.Empty; var suiteName = message.GetValue("suiteName") ?? string.Empty; var name = message.GetValue("name") ?? string.Empty; return new TestKey(flowId, suiteName, name); } // ReSharper disable once NotAccessedPositionalProperty.Local private readonly record struct TestKey(string FlowId, string SuiteName, string TestName); private class TestContext { public readonly List Output = new(); public void AddStdOut(IStartInfo info, int processId, string? text) { if (text != default) { Output.Add(new Output(info, false, text, processId)); } } public void AddStdErr(IStartInfo info, int processId, string? error) { if (error != default) { Output.Add(new Output(info, true, error, processId)); } } } }