A game about forced loneliness, made by TACStudios
1using System; 2using System.Collections.Generic; 3using System.Globalization; 4using System.IO; 5using System.Linq; 6using System.Text; 7using NUnit.Framework.Interfaces; 8using UnityEditor.TestTools.TestRunner.GUI; 9using UnityEngine; 10 11namespace UnityEditor.TestRunner.TestLaunchers 12{ 13 internal static class FilePathMetaInfo 14 { 15 [Serializable] 16 private struct FileReference 17 { 18 public string FilePath; 19 public int LineNumber; 20 } 21 22 private enum PathType 23 { 24 ProjectRepositoryPath, 25 ProjectPath, 26 } 27 28 public static void TryCreateFile(ITest runnerLoadedTest, BuildPlayerOptions playerBuildOptions) 29 { 30 try 31 { 32 var metaFileDestinationPath = GetMetaDestinationPath(playerBuildOptions); 33 var repositoryPath = GetPathFromArgs(PathType.ProjectRepositoryPath); 34 // if no path is given, early out so we do not pollute the build player folder with the file path file. 35 if (string.IsNullOrEmpty(repositoryPath)) 36 { 37 return; 38 } 39 40 // Create a dictionary for the test names and their file paths 41 var testFilePaths = new Dictionary<string, FileReference>(); 42 RecursivelyPopulateFileReferences(runnerLoadedTest, testFilePaths, repositoryPath, new GuiHelper(new MonoCecilHelper(), new AssetsDatabaseHelper())); 43 SaveToJsonFile(testFilePaths, metaFileDestinationPath); 44 } 45 catch (Exception e) 46 { 47 Debug.LogWarning("Saving test file path meta info failed: " + e.Message); 48 } 49 } 50 51 // This function serializes dictionary to json file, all the logic would not be necessary if Unity was able to serialize Dictionaries, or if we could use Newtonsoft.Json. 52 // This function could be changed later on, or we can use different data structure than Dictionary. 53 private static void SaveToJsonFile(Dictionary<string, FileReference> testFilePaths, string metaFileDestinationPath) 54 { 55 using (var fileStream = File.CreateText(Path.Combine(metaFileDestinationPath, "TestFileReferences.json"))) 56 { 57 fileStream.WriteLine("{"); 58 59 foreach (var testFilePath in testFilePaths) 60 { 61 fileStream.WriteLine($" \"{JavaScriptStringEncode(testFilePath.Key)}\": {{"); 62 fileStream.WriteLine($" \"filePath\": \"{JavaScriptStringEncode(testFilePath.Value.FilePath)}\","); 63 fileStream.WriteLine($" \"lineNumber\": {testFilePath.Value.LineNumber}"); 64 // check if it is the last element in the dictionary 65 if (testFilePath.Key != testFilePaths.Keys.Last()) 66 { 67 fileStream.WriteLine(" },"); 68 } 69 else 70 { 71 fileStream.WriteLine(" }"); 72 } 73 } 74 75 fileStream.WriteLine("}"); 76 } 77 } 78 79 private static string GetMetaDestinationPath(BuildPlayerOptions playerBuildOptions) 80 { 81 // If we are Auto-Running the player, use project path instead of player build path because it will be wiped out after successful run. 82 if ((playerBuildOptions.options & BuildOptions.AutoRunPlayer) != 0) 83 { 84 return Path.Combine(GetPathFromArgs(PathType.ProjectPath)); 85 } 86 87 // if the buildOutputPath is for a file, then get the directory of it 88 return File.Exists(playerBuildOptions.locationPathName) ? Path.GetDirectoryName(playerBuildOptions.locationPathName) : playerBuildOptions.locationPathName; 89 } 90 91 private static void RecursivelyPopulateFileReferences(ITest test, Dictionary<string, FileReference> testFilePaths, string repositoryPath, IGuiHelper guiHelper) 92 { 93 if (test.HasChildren) 94 { 95 foreach (var child in test.Tests) 96 { 97 RecursivelyPopulateFileReferences(child, testFilePaths, repositoryPath, guiHelper); 98 } 99 100 return; 101 } 102 103 var testMethod = test.Method; 104 if (testMethod == null) 105 { 106 testMethod = test.Parent.Method; 107 if (testMethod == null) 108 { 109 return; 110 } 111 } 112 113 var methodInfo = test.Method.MethodInfo; 114 var type = test.TypeInfo.Type; 115 var fileOpenInfo = guiHelper.GetFileOpenInfo(type, methodInfo); 116 var filePathString = Path.Combine(repositoryPath, fileOpenInfo.FilePath); 117 var lineNumber = fileOpenInfo.LineNumber; 118 var fileReference = new FileReference 119 { 120 FilePath = filePathString, 121 LineNumber = lineNumber 122 }; 123 // Cannot be simplified with .TryAdd because Unity 2020.3 and below does not have it. 124 if (!testFilePaths.ContainsKey(test.FullName)) 125 { 126 testFilePaths.Add(test.FullName, fileReference); 127 } 128 } 129 130 private static string GetPathFromArgs(PathType type) 131 { 132 var commandLineArgs = Environment.GetCommandLineArgs(); 133 134 string lookFor; 135 switch (type) 136 { 137 case PathType.ProjectRepositoryPath: 138 lookFor = "-projectRepositoryPath"; 139 break; 140 case PathType.ProjectPath: 141 lookFor = "-projectPath"; 142 break; 143 default: 144 throw new ArgumentException("Invalid PathType"); 145 } 146 147 for (var i = 0; i < commandLineArgs.Length; i++) 148 { 149 if (commandLineArgs[i].Equals(lookFor)) 150 { 151 return commandLineArgs[i + 1]; 152 } 153 } 154 155 return string.Empty; 156 } 157 158 // Below implementation is copy-paste from HttpUtility.JavaScriptStringEncode 159 private static string JavaScriptStringEncode(string value) { 160 if (String.IsNullOrEmpty(value)) { 161 return String.Empty; 162 } 163 164 StringBuilder b = null; 165 int startIndex = 0; 166 int count = 0; 167 for (int i = 0; i < value.Length; i++) { 168 char c = value[i]; 169 170 // Append the unhandled characters (that do not require special treament) 171 // to the string builder when special characters are detected. 172 if (CharRequiresJavaScriptEncoding(c)) { 173 if (b == null) { 174 b = new StringBuilder(value.Length + 5); 175 } 176 177 if (count > 0) { 178 b.Append(value, startIndex, count); 179 } 180 181 startIndex = i + 1; 182 count = 0; 183 } 184 185 switch (c) { 186 case '\r': 187 b.Append("\\r"); 188 break; 189 case '\t': 190 b.Append("\\t"); 191 break; 192 case '\"': 193 b.Append("\\\""); 194 break; 195 case '\\': 196 b.Append("\\\\"); 197 break; 198 case '\n': 199 b.Append("\\n"); 200 break; 201 case '\b': 202 b.Append("\\b"); 203 break; 204 case '\f': 205 b.Append("\\f"); 206 break; 207 default: 208 if (CharRequiresJavaScriptEncoding(c)) { 209 AppendCharAsUnicodeJavaScript(b, c); 210 } 211 else { 212 count++; 213 } 214 break; 215 } 216 } 217 218 if (b == null) { 219 return value; 220 } 221 222 if (count > 0) { 223 b.Append(value, startIndex, count); 224 } 225 226 return b.ToString(); 227 } 228 229 private static bool CharRequiresJavaScriptEncoding(char c) { 230 return c < 0x20 // control chars always have to be encoded 231 || c == '\"' // chars which must be encoded per JSON spec 232 || c == '\\' 233 || c == '\'' // HTML-sensitive chars encoded for safety 234 || c == '<' 235 || c == '>' 236 || c == '&' 237 || c == '\u0085' // newline chars (see Unicode 6.2, Table 5-1 [http://www.unicode.org/versions/Unicode6.2.0/ch05.pdf]) have to be encoded (DevDiv #663531) 238 || c == '\u2028' 239 || c == '\u2029'; 240 } 241 242 private static void AppendCharAsUnicodeJavaScript(StringBuilder builder, char c) { 243 builder.Append("\\u"); 244 builder.Append(((int)c).ToString("x4", CultureInfo.InvariantCulture)); 245 } 246 } 247}