GuiHelper.cs 5.25 KB
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Unity.CodeEditor;
using UnityEditor.Utils;
using UnityEngine;

namespace UnityEditor.TestTools.TestRunner.GUI
{
    internal class GuiHelper : IGuiHelper
    {
        public GuiHelper(IMonoCecilHelper monoCecilHelper, IAssetsDatabaseHelper assetsDatabaseHelper)
        {
            MonoCecilHelper = monoCecilHelper;
            AssetsDatabaseHelper = assetsDatabaseHelper;
            Editor = new DefaultExternalCodeEditor();
            GetCSFiles = (dirPath, fileExtension) =>
            {
                return Directory.GetFiles(dirPath, $"*{fileExtension}", SearchOption.AllDirectories)
                    .Select(Paths.UnifyDirectorySeparator);
            };
        }
        internal Func<string, string, IEnumerable<string>> GetCSFiles;
        protected IMonoCecilHelper MonoCecilHelper { get; private set; }
        public IAssetsDatabaseHelper AssetsDatabaseHelper { get; private set; }
        public IExternalCodeEditor Editor { get; internal set; }
        private const string FileExtension = ".cs";

        public void OpenScriptInExternalEditor(Type type, MethodInfo method)
        {
            var fileOpenInfo = GetFileOpenInfo(type, method);

            if (string.IsNullOrEmpty(fileOpenInfo.FilePath))
            {
                Debug.LogWarning("Failed to open test method source code in external editor. Inconsistent filename and yield return operator in target method.");

                return;
            }

            if (fileOpenInfo.LineNumber == 1)
            {
                Debug.LogWarning("Failed to get a line number for unity test method. So please find it in opened file in external editor.");
            }

            if (!fileOpenInfo.FilePath.Contains("Assets"))
            {
                Editor.OpenProject(fileOpenInfo.FilePath, fileOpenInfo.LineNumber, 1);
            }
            else
            { 
                AssetsDatabaseHelper.OpenAssetInItsDefaultExternalEditor(fileOpenInfo.FilePath, fileOpenInfo.LineNumber);
            }
            
        }

        public IFileOpenInfo GetFileOpenInfo(Type type, MethodInfo method)
        {
            var fileOpenInfo = MonoCecilHelper.TryGetCecilFileOpenInfo(type, method);
            if (string.IsNullOrEmpty(fileOpenInfo.FilePath))
            {
                var dirPath = Paths.UnifyDirectorySeparator(Application.dataPath);
                var allCsFiles = GetCSFiles(dirPath, FileExtension);

                var fileName = allCsFiles.FirstOrDefault(x =>
                    x.Split(Path.DirectorySeparatorChar).Last().Equals(string.Concat(GetTestFileName(type), FileExtension)));

                fileOpenInfo.FilePath = fileName ?? string.Empty;
            }

            if (!fileOpenInfo.FilePath.Contains("Assets"))
            {
                return fileOpenInfo;
            }
            fileOpenInfo.FilePath = FilePathToAssetsRelativeAndUnified(fileOpenInfo.FilePath);

            return fileOpenInfo;
        }

        internal static string GetTestFileName(Type type)
        {
            //This handles the case of a test in a nested class, getting the name of the base class
            if (type.FullName != null && type.Namespace!=null && type.FullName.Contains("+"))
            {
                var removedNamespace = type.FullName.Substring(type.Namespace.Length+1);
                return removedNamespace.Substring(0,removedNamespace.IndexOf("+", StringComparison.Ordinal));
            }
            return type.Name;
        }
        public string FilePathToAssetsRelativeAndUnified(string filePath)
        {
            if (string.IsNullOrEmpty(filePath))
                return string.Empty;

            filePath = Paths.UnifyDirectorySeparator(filePath);
            var length = Paths.UnifyDirectorySeparator(Application.dataPath).Length - "Assets".Length;

            return filePath.Substring(length);
        }

        public bool OpenScriptInExternalEditor(string stacktrace)
        {
            if (string.IsNullOrEmpty(stacktrace))
                return false;

            var regex = new Regex("in (?<path>.*):{1}(?<line>[0-9]+)");

            var matchingLines = stacktrace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).Where(x => regex.IsMatch(x)).ToList();
            if (!matchingLines.Any())
                return false;

            var fileOpenInfos = matchingLines
                .Select(x => regex.Match(x))
                .Select(x =>
                    new FileOpenInfo
                    {
                        FilePath = x.Groups["path"].Value,
                        LineNumber = int.Parse(x.Groups["line"].Value)
                    }).ToList();

            var fileOpenInfo = fileOpenInfos
                .FirstOrDefault(openInfo => !string.IsNullOrEmpty(openInfo.FilePath) && File.Exists(openInfo.FilePath));

            if (fileOpenInfo == null)
            {
                return false;
            }

            var filePath = FilePathToAssetsRelativeAndUnified(fileOpenInfo.FilePath);
            AssetsDatabaseHelper.OpenAssetInItsDefaultExternalEditor(filePath, fileOpenInfo.LineNumber);

            return true;
        }
    }
}