using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using UnityEditor.IMGUI.Controls; using UnityEditor.TestTools.TestRunner.Api; using UnityEngine; namespace UnityEditor.TestTools.TestRunner.GUI { internal sealed class TestTreeViewItem : TreeViewItem { public TestRunnerResult result; internal ITestAdaptor m_Test; public Type type; public MethodInfo method; private const int k_ResultTestMaxLength = 15000; public bool IsGroupNode { get { return m_Test.IsSuite; } } public string FullName { get { return m_Test.FullName; } } public string UniqueName { get { return m_Test.UniqueName; } } public override string displayName { get => $"{base.displayName}{(hasChildren ? $" ({TotalChildrenCount} tests) {(TotalSuccessChildrenCount > 0 ? $" {TotalSuccessChildrenCount} tests failed" : null)}" : null)}"; set => base.displayName = value; } public string GetAssemblyName() { var test = m_Test; while (test != null) { if (test.IsTestAssembly) { return test.FullName; } test = test.Parent; } return null; } public IEnumerable GetMinimizedSelectedTree() { if (!m_Test.HasChildren) { yield return m_Test; yield break; } var minimizedDescendants = children.OfType().SelectMany(c => c.GetMinimizedSelectedTree()).ToArray(); var includeChildren = minimizedDescendants.Count(c => c.Parent == m_Test); if (includeChildren == m_Test.Children.Count()) { // All children are included in the filter, so we can just return the parent yield return m_Test; yield break; } foreach (var child in minimizedDescendants) { yield return child; } } public TestTreeViewItem(ITestAdaptor test, int depth, TreeViewItem parent) : base(GetId(test), depth, parent, test.Name) { m_Test = test; if (test.TypeInfo != null) { type = test.TypeInfo.Type; } if (test.Method != null) { method = test.Method.MethodInfo; } displayName = test.Name.Replace("\n", ""); icon = Icons.s_UnknownImg; } public int TotalChildrenCount { get; set; } public int TotalSuccessChildrenCount { get; set; } private static int GetId(ITestAdaptor test) { return test.UniqueName.GetHashCode(); } public void SetResult(TestRunnerResult testResult) { result = testResult; result.SetResultChangedCallback(ResultUpdated); ResultUpdated(result); } public string GetResultText() { if (result.resultStatus == TestRunnerResult.ResultStatus.NotRun) { if (result.ignoredOrSkipped) { return result.messages; } return string.Empty; } var durationString = String.Format("{0:0.000}", result.duration); var sb = new StringBuilder(string.Format("{0} ({1}s)", displayName.Trim(), durationString)); if (!string.IsNullOrEmpty(result.description)) { sb.AppendFormat("\n{0}", result.description); } if (!string.IsNullOrEmpty(result.messages)) { sb.Append("\n---\n"); sb.Append(result.messages); } if (!string.IsNullOrEmpty(result.stacktrace)) { sb.Append("\n---\n"); sb.Append(StacktraceWithHyperlinks(result.stacktrace)); } if (!string.IsNullOrEmpty(result.output)) { sb.Append("\n---\n"); sb.Append(result.output); } if (sb.Length > k_ResultTestMaxLength) { sb.Length = k_ResultTestMaxLength; sb.AppendFormat("...\n\n---MESSAGE TRUNCATED AT {0} CHARACTERS---", k_ResultTestMaxLength); } return sb.ToString().Trim(); } private void ResultUpdated(TestRunnerResult testResult) { switch (testResult.resultStatus) { case TestRunnerResult.ResultStatus.Passed: icon = Icons.s_SuccessImg; break; case TestRunnerResult.ResultStatus.Failed: icon = Icons.s_FailImg; break; case TestRunnerResult.ResultStatus.Inconclusive: icon = Icons.s_InconclusiveImg; break; case TestRunnerResult.ResultStatus.Skipped: icon = Icons.s_IgnoreImg; break; default: if (testResult.ignoredOrSkipped) { icon = Icons.s_IgnoreImg; } else if (testResult.notRunnable) { icon = Icons.s_FailImg; } else { icon = Icons.s_UnknownImg; } break; } } private static string StacktraceWithHyperlinks(string stacktraceText) { StringBuilder textWithHyperlinks = new StringBuilder(); var lines = stacktraceText.Split(new string[] { "\n" }, StringSplitOptions.None); for (int i = 0; i < lines.Length; ++i) { string textBeforeFilePath = "] in "; int filePathIndex = lines[i].IndexOf(textBeforeFilePath, StringComparison.Ordinal); if (filePathIndex > 0) { filePathIndex += textBeforeFilePath.Length; if (lines[i][filePathIndex] != '<') // sometimes no url is given, just an id between <>, we can't do an hyperlink { string filePathPart = lines[i].Substring(filePathIndex); int lineIndex = filePathPart.LastIndexOf(":", StringComparison.Ordinal); // LastIndex because the url can contain ':' ex:"C:" if (lineIndex > 0) { string lineString = filePathPart.Substring(lineIndex + 1); string filePath = filePathPart.Substring(0, lineIndex); #if UNITY_2021_3_OR_NEWER var displayedPath = Path.GetRelativePath(Directory.GetCurrentDirectory(), filePath); #else var displayedPath = filePath; #endif textWithHyperlinks.Append($"{lines[i].Substring(0, filePathIndex)}{displayedPath}:{lineString}\n"); continue; // continue to evade the default case } } } // default case if no hyperlink : we just write the line textWithHyperlinks.Append(lines[i] + "\n"); } // Remove the last \n if (textWithHyperlinks.Length > 0) // textWithHyperlinks always ends with \n if it is not empty textWithHyperlinks.Remove(textWithHyperlinks.Length - 1, 1); return textWithHyperlinks.ToString(); } } }