2
0

StringUsageReporter.cs 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. using MediaBrowser.Tests.ConsistencyTests.TextIndexing;
  2. using Microsoft.VisualStudio.TestTools.UnitTesting;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Xml;
  9. namespace MediaBrowser.Tests.ConsistencyTests
  10. {
  11. /// <summary>
  12. /// This class contains tests for reporting the usage of localization string tokens
  13. /// in the dashboard-ui or similar.
  14. /// </summary>
  15. /// <remarks>
  16. /// <para>Run one of the two tests using Visual Studio's "Test Explorer":</para>
  17. /// <para>
  18. /// <list type="bullet">
  19. /// <item><see cref="ReportStringUsage"/></item>
  20. /// <item><see cref="ReportUnusedStrings"/></item>
  21. /// </list>
  22. /// </para>
  23. /// <para>
  24. /// On successful run, the bottom section of the test explorer will contain a link "Output".
  25. /// This link will open the test results, displaying the trace and two attachment links.
  26. /// One link will open the output folder, the other link will open the output xml file.
  27. /// </para>
  28. /// <para>
  29. /// The output xml file contains a stylesheet link to render the results as html.
  30. /// How that works depends on the default application configured for XML files:
  31. /// </para>
  32. /// <para><list type="bullet">
  33. /// <item><term>Visual Studio</term>
  34. /// <description>Will open in XML source view. To view the html result, click menu
  35. /// 'XML' => 'Start XSLT without debugging'</description></item>
  36. /// <item><term>Internet Explorer</term>
  37. /// <description>XSL transform will be applied automatically.</description></item>
  38. /// <item><term>Firefox</term>
  39. /// <description>XSL transform will be applied automatically.</description></item>
  40. /// <item><term>Chrome</term>
  41. /// <description>Does not work. Chrome is unable/unwilling to apply xslt transforms from local files.</description></item>
  42. /// </list></para>
  43. /// </remarks>
  44. [TestClass]
  45. public class StringUsageReporter
  46. {
  47. /// <summary>
  48. /// Root path of the web application
  49. /// </summary>
  50. /// <remarks>
  51. /// Can be an absolute path or a path relative to the binaries folder (bin\Debug).
  52. /// </remarks>
  53. public const string WebFolder = @"..\..\..\MediaBrowser.WebDashboard\dashboard-ui";
  54. /// <summary>
  55. /// Path to the strings file, relative to <see cref="WebFolder"/>.
  56. /// </summary>
  57. public const string StringsFile = @"strings\en-US.json";
  58. /// <summary>
  59. /// Path to the output folder
  60. /// </summary>
  61. /// <remarks>
  62. /// Can be an absolute path or a path relative to the binaries folder (bin\Debug).
  63. /// Important: When changing the output path, make sure that "StringCheck.xslt" is present
  64. /// to make the XML transform work.
  65. /// </remarks>
  66. public const string OutputPath = @".";
  67. /// <summary>
  68. /// List of file extension to search.
  69. /// </summary>
  70. public static string[] TargetExtensions = new[] { ".js", ".html" };
  71. /// <summary>
  72. /// List of paths to exclude from search.
  73. /// </summary>
  74. public static string[] ExcludePaths = new[] { @"\bower_components\", @"\thirdparty\" };
  75. private TestContext testContextInstance;
  76. /// <summary>
  77. ///Gets or sets the test context which provides
  78. ///information about and functionality for the current test run.
  79. ///</summary>
  80. public TestContext TestContext
  81. {
  82. get
  83. {
  84. return testContextInstance;
  85. }
  86. set
  87. {
  88. testContextInstance = value;
  89. }
  90. }
  91. //[TestMethod]
  92. //public void ReportStringUsage()
  93. //{
  94. // this.CheckDashboardStrings(false);
  95. //}
  96. [TestMethod]
  97. public void ReportUnusedStrings()
  98. {
  99. this.CheckDashboardStrings(true);
  100. }
  101. private void CheckDashboardStrings(Boolean unusedOnly)
  102. {
  103. // Init Folders
  104. var currentDir = System.IO.Directory.GetCurrentDirectory();
  105. Trace("CurrentDir: {0}", currentDir);
  106. var rootFolderInfo = ResolveFolder(currentDir, WebFolder);
  107. Trace("Web Root: {0}", rootFolderInfo.FullName);
  108. var outputFolderInfo = ResolveFolder(currentDir, OutputPath);
  109. Trace("Output Path: {0}", outputFolderInfo.FullName);
  110. // Load Strings
  111. var stringsFileName = Path.Combine(rootFolderInfo.FullName, StringsFile);
  112. if (!File.Exists(stringsFileName))
  113. {
  114. throw new Exception(string.Format("Strings file not found: {0}", stringsFileName));
  115. }
  116. int lineNumbers;
  117. var stringsDic = this.CreateStringsDictionary(new FileInfo(stringsFileName), out lineNumbers);
  118. Trace("Loaded {0} strings from strings file containing {1} lines", stringsDic.Count, lineNumbers);
  119. var allFiles = rootFolderInfo.GetFiles("*", SearchOption.AllDirectories);
  120. var filteredFiles1 = allFiles.Where(f => TargetExtensions.Any(e => string.Equals(e, f.Extension, StringComparison.OrdinalIgnoreCase)));
  121. var filteredFiles2 = filteredFiles1.Where(f => !ExcludePaths.Any(p => f.FullName.Contains(p)));
  122. var selectedFiles = filteredFiles2.OrderBy(f => f.FullName).ToList();
  123. var wordIndex = IndexBuilder.BuildIndexFromFiles(selectedFiles, rootFolderInfo.FullName);
  124. Trace("Created word index from {0} files containing {1} individual words", selectedFiles.Count, wordIndex.Keys.Count);
  125. var outputFileName = Path.Combine(outputFolderInfo.FullName, string.Format("StringCheck_{0:yyyyMMddHHmmss}.xml", DateTime.Now));
  126. var settings = new XmlWriterSettings
  127. {
  128. Indent = true,
  129. Encoding = Encoding.UTF8,
  130. WriteEndDocumentOnClose = true
  131. };
  132. Trace("Output file: {0}", outputFileName);
  133. using (XmlWriter writer = XmlWriter.Create(outputFileName, settings))
  134. {
  135. writer.WriteStartDocument(true);
  136. // Write the Processing Instruction node.
  137. string xslText = "type=\"text/xsl\" href=\"StringCheck.xslt\"";
  138. writer.WriteProcessingInstruction("xml-stylesheet", xslText);
  139. writer.WriteStartElement("StringUsages");
  140. writer.WriteAttributeString("ReportTitle", unusedOnly ? "Unused Strings Report" : "String Usage Report");
  141. writer.WriteAttributeString("Mode", unusedOnly ? "UnusedOnly" : "All");
  142. foreach (var kvp in stringsDic)
  143. {
  144. var occurences = wordIndex.Find(kvp.Key);
  145. if (occurences == null || !unusedOnly)
  146. {
  147. ////Trace("{0}: {1}", kvp.Key, kvp.Value);
  148. writer.WriteStartElement("Dictionary");
  149. writer.WriteAttributeString("Token", kvp.Key);
  150. writer.WriteAttributeString("Text", kvp.Value);
  151. if (occurences != null && !unusedOnly)
  152. {
  153. foreach (var occurence in occurences)
  154. {
  155. writer.WriteStartElement("Occurence");
  156. writer.WriteAttributeString("FileName", occurence.FileName);
  157. writer.WriteAttributeString("FullPath", occurence.FullPath);
  158. writer.WriteAttributeString("LineNumber", occurence.LineNumber.ToString());
  159. writer.WriteEndElement();
  160. ////Trace(" {0}:{1}", occurence.FileName, occurence.LineNumber);
  161. }
  162. }
  163. writer.WriteEndElement();
  164. }
  165. }
  166. }
  167. TestContext.AddResultFile(outputFileName);
  168. TestContext.AddResultFile(outputFolderInfo.FullName);
  169. }
  170. private SortedDictionary<string, string> CreateStringsDictionary(FileInfo file, out int lineNumbers)
  171. {
  172. var dic = new SortedDictionary<string, string>();
  173. lineNumbers = 0;
  174. using (var reader = file.OpenText())
  175. {
  176. while (!reader.EndOfStream)
  177. {
  178. lineNumbers++;
  179. var words = reader
  180. .ReadLine()
  181. .Split(new[] { "\":" }, StringSplitOptions.RemoveEmptyEntries);
  182. if (words.Length == 2)
  183. {
  184. var token = words[0].Replace("\"", string.Empty).Trim();
  185. var text = words[1].Replace("\",", string.Empty).Replace("\"", string.Empty).Trim();
  186. if (dic.Keys.Contains(token))
  187. {
  188. throw new Exception(string.Format("Double string entry found: {0}", token));
  189. }
  190. dic.Add(token, text);
  191. }
  192. }
  193. }
  194. return dic;
  195. }
  196. private DirectoryInfo ResolveFolder(string currentDir, string folderPath)
  197. {
  198. if (folderPath.IndexOf(@"\:") != 1)
  199. {
  200. folderPath = Path.Combine(currentDir, folderPath);
  201. }
  202. var folderInfo = new DirectoryInfo(folderPath);
  203. if (!folderInfo.Exists)
  204. {
  205. throw new Exception(string.Format("Folder not found: {0}", folderInfo.FullName));
  206. }
  207. return folderInfo;
  208. }
  209. private void Trace(string message, params object[] parameters)
  210. {
  211. var formatted = string.Format(message, parameters);
  212. System.Diagnostics.Trace.WriteLine(formatted);
  213. }
  214. }
  215. }