using MediaBrowser.Tests.ConsistencyTests.TextIndexing; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml; namespace MediaBrowser.Tests.ConsistencyTests { /// /// This class contains tests for reporting the usage of localization string tokens /// in the dashboard-ui or similar. /// /// /// Run one of the two tests using Visual Studio's "Test Explorer": /// /// /// /// /// /// /// /// On successful run, the bottom section of the test explorer will contain a link "Output". /// This link will open the test results, displaying the trace and two attachment links. /// One link will open the output folder, the other link will open the output xml file. /// /// /// The output xml file contains a stylesheet link to render the results as html. /// How that works depends on the default application configured for XML files: /// /// /// Visual Studio /// Will open in XML source view. To view the html result, click menu /// 'XML' => 'Start XSLT without debugging' /// Internet Explorer /// XSL transform will be applied automatically. /// Firefox /// XSL transform will be applied automatically. /// Chrome /// Does not work. Chrome is unable/unwilling to apply xslt transforms from local files. /// /// [TestClass] public class StringUsageReporter { /// /// Root path of the web application /// /// /// Can be an absolute path or a path relative to the binaries folder (bin\Debug). /// public const string WebFolder = @"..\..\..\MediaBrowser.WebDashboard\dashboard-ui"; /// /// Path to the strings file, relative to . /// public const string StringsFile = @"strings\en-US.json"; /// /// Path to the output folder /// /// /// Can be an absolute path or a path relative to the binaries folder (bin\Debug). /// Important: When changing the output path, make sure that "StringCheck.xslt" is present /// to make the XML transform work. /// public const string OutputPath = @"."; /// /// List of file extension to search. /// public static string[] TargetExtensions = new[] { "js", "html" }; /// /// List of paths to exclude from search. /// public static string[] ExcludePaths = new[] { @"\bower_components\", @"\thirdparty\" }; private TestContext testContextInstance; /// ///Gets or sets the test context which provides ///information about and functionality for the current test run. /// public TestContext TestContext { get { return testContextInstance; } set { testContextInstance = value; } } [TestMethod] public void ReportStringUsage() { this.CheckDashboardStrings(false); } [TestMethod] public void ReportUnusedStrings() { this.CheckDashboardStrings(true); } private void CheckDashboardStrings(Boolean unusedOnly) { // Init Folders var currentDir = System.IO.Directory.GetCurrentDirectory(); Trace("CurrentDir: {0}", currentDir); var rootFolderInfo = ResolveFolder(currentDir, WebFolder); Trace("Web Root: {0}", rootFolderInfo.FullName); var outputFolderInfo = ResolveFolder(currentDir, OutputPath); Trace("Output Path: {0}", outputFolderInfo.FullName); // Load Strings var stringsFileName = Path.Combine(rootFolderInfo.FullName, StringsFile); if (!File.Exists(stringsFileName)) { throw new Exception(string.Format("Strings file not found: {0}", stringsFileName)); } int lineNumbers; var stringsDic = this.CreateStringsDictionary(new FileInfo(stringsFileName), out lineNumbers); Trace("Loaded {0} strings from strings file containing {1} lines", stringsDic.Count, lineNumbers); var allFiles = rootFolderInfo.GetFiles("*", SearchOption.AllDirectories); var filteredFiles1 = allFiles.Where(f => TargetExtensions.Any(e => f.Name.EndsWith(e))); var filteredFiles2 = filteredFiles1.Where(f => !ExcludePaths.Any(p => f.FullName.Contains(p))); var selectedFiles = filteredFiles2.OrderBy(f => f.FullName).ToList(); var wordIndex = IndexBuilder.BuildIndexFromFiles(selectedFiles, rootFolderInfo.FullName); Trace("Created word index from {0} files containing {1} individual words", selectedFiles.Count, wordIndex.Keys.Count); var outputFileName = Path.Combine(outputFolderInfo.FullName, string.Format("StringCheck_{0:yyyyMMddHHmmss}.xml", DateTime.Now)); var settings = new XmlWriterSettings { Indent = true, Encoding = Encoding.UTF8, WriteEndDocumentOnClose = true }; Trace("Output file: {0}", outputFileName); using (XmlWriter writer = XmlWriter.Create(outputFileName, settings)) { writer.WriteStartDocument(true); // Write the Processing Instruction node. string xslText = "type=\"text/xsl\" href=\"StringCheck.xslt\""; writer.WriteProcessingInstruction("xml-stylesheet", xslText); writer.WriteStartElement("StringUsages"); writer.WriteAttributeString("ReportTitle", unusedOnly ? "Unused Strings Report" : "String Usage Report"); writer.WriteAttributeString("Mode", unusedOnly ? "UnusedOnly" : "All"); foreach (var kvp in stringsDic) { var occurences = wordIndex.Find(kvp.Key); if (occurences == null || !unusedOnly) { ////Trace("{0}: {1}", kvp.Key, kvp.Value); writer.WriteStartElement("Dictionary"); writer.WriteAttributeString("Token", kvp.Key); writer.WriteAttributeString("Text", kvp.Value); if (occurences != null && !unusedOnly) { foreach (var occurence in occurences) { writer.WriteStartElement("Occurence"); writer.WriteAttributeString("FileName", occurence.FileName); writer.WriteAttributeString("FullPath", occurence.FullPath); writer.WriteAttributeString("LineNumber", occurence.LineNumber.ToString()); writer.WriteEndElement(); ////Trace(" {0}:{1}", occurence.FileName, occurence.LineNumber); } } writer.WriteEndElement(); } } } TestContext.AddResultFile(outputFileName); TestContext.AddResultFile(outputFolderInfo.FullName); } private SortedDictionary CreateStringsDictionary(FileInfo file, out int lineNumbers) { var dic = new SortedDictionary(); lineNumbers = 0; using (var reader = file.OpenText()) { while (!reader.EndOfStream) { lineNumbers++; var words = reader .ReadLine() .Split(new[] { "\":" }, StringSplitOptions.RemoveEmptyEntries); if (words.Length == 2) { var token = words[0].Replace("\"", string.Empty).Trim(); var text = words[1].Replace("\",", string.Empty).Replace("\"", string.Empty).Trim(); if (dic.Keys.Contains(token)) { throw new Exception(string.Format("Double string entry found: {0}", token)); } dic.Add(token, text); } } } return dic; } private DirectoryInfo ResolveFolder(string currentDir, string folderPath) { if (folderPath.IndexOf(@"\:") != 1) { folderPath = Path.Combine(currentDir, folderPath); } var folderInfo = new DirectoryInfo(folderPath); if (!folderInfo.Exists) { throw new Exception(string.Format("Folder not found: {0}", folderInfo.FullName)); } return folderInfo; } private void Trace(string message, params object[] parameters) { var formatted = string.Format(message, parameters); System.Diagnostics.Trace.WriteLine(formatted); } } }