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}