| | | 1 | | using System; |
| | | 2 | | using System.Collections.Concurrent; |
| | | 3 | | using System.Collections.Generic; |
| | | 4 | | using System.IO; |
| | | 5 | | using System.Linq; |
| | | 6 | | using System.Threading; |
| | | 7 | | using System.Threading.Tasks; |
| | | 8 | | using System.Xml; |
| | | 9 | | using System.Xml.Linq; |
| | | 10 | | using DirectSight.Common; |
| | | 11 | | using DirectSight.Logging; |
| | | 12 | | using DirectSight.Parser.Preprocessing; |
| | | 13 | | |
| | | 14 | | namespace DirectSight.Parser; |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Initiates the corresponding parser to the given report file. |
| | | 18 | | /// </summary> |
| | | 19 | | public class CoverageReportParser |
| | | 20 | | { |
| | | 21 | | /// <summary> |
| | | 22 | | /// The current merge count. |
| | | 23 | | /// </summary> |
| | | 24 | | private int mergeCount; |
| | | 25 | | |
| | | 26 | | /// <summary> |
| | | 27 | | /// Initializes a new instance of the <see cref="CoverageReportParser" /> class. |
| | | 28 | | /// </summary> |
| | 21 | 29 | | public CoverageReportParser() { } |
| | | 30 | | |
| | | 31 | | /// <summary> |
| | | 32 | | /// Tries to initiate the correct parsers for the given reports. |
| | | 33 | | /// </summary> |
| | | 34 | | /// <param name="reportFiles">The report files to parse.</param> |
| | | 35 | | /// <returns> |
| | | 36 | | /// The IParser instance. |
| | | 37 | | /// </returns> |
| | | 38 | | public ParserResult ParseFiles(IReadOnlyCollection<string> reportFiles) |
| | 7 | 39 | | { |
| | 7 | 40 | | ArgumentNullException.ThrowIfNull(reportFiles); |
| | | 41 | | |
| | 7 | 42 | | List<Task<ParserResult>> consumers = []; |
| | | 43 | | |
| | | 44 | | try |
| | 7 | 45 | | { |
| | 7 | 46 | | using BlockingCollection<ParserResult> blockingCollection = []; |
| | 7 | 47 | | consumers.Add(this.CreateConsumer(blockingCollection)); |
| | | 48 | | |
| | 7 | 49 | | Task producer = CreateProducer(reportFiles, blockingCollection); |
| | 7 | 50 | | Task.WaitAll([.. consumers, producer]); |
| | 7 | 51 | | } |
| | 0 | 52 | | catch (AggregateException ae) |
| | 0 | 53 | | { |
| | 0 | 54 | | foreach (var e in ae.Flatten().InnerExceptions) |
| | 0 | 55 | | { |
| | 0 | 56 | | if (e is UnsupportedParserException) |
| | 0 | 57 | | { |
| | 0 | 58 | | throw e; |
| | | 59 | | } |
| | 0 | 60 | | } |
| | | 61 | | |
| | 0 | 62 | | throw; |
| | | 63 | | } |
| | | 64 | | |
| | 14 | 65 | | List<ParserResult> results = [.. consumers.Select(t => t.Result)]; |
| | 7 | 66 | | ParserResult finalResult = results.First(); |
| | 21 | 67 | | foreach (ParserResult toBeMerged in results.Skip(1)) |
| | 0 | 68 | | { |
| | 0 | 69 | | this.MergeResults(finalResult, toBeMerged); |
| | 0 | 70 | | } |
| | | 71 | | |
| | 7 | 72 | | return finalResult; |
| | 7 | 73 | | } |
| | | 74 | | |
| | | 75 | | /// <summary> |
| | | 76 | | /// Creates the consumer which merges the results. |
| | | 77 | | /// </summary> |
| | | 78 | | /// <param name="collection">The collection to pick results from.</param> |
| | | 79 | | /// <returns>The task containing merged results or null in case this consumer has not merged any result.</returns> |
| | | 80 | | private Task<ParserResult> CreateConsumer(BlockingCollection<ParserResult> collection) |
| | 7 | 81 | | { |
| | 7 | 82 | | return Task.Factory.StartNew(() => |
| | 7 | 83 | | { |
| | 7 | 84 | | var result = new ParserResult(); |
| | 39 | 85 | | foreach (ParserResult parserResult in collection.GetConsumingEnumerable()) |
| | 9 | 86 | | { |
| | 9 | 87 | | this.MergeResults(result, parserResult); |
| | 9 | 88 | | } |
| | 7 | 89 | | |
| | 7 | 90 | | return result; |
| | 14 | 91 | | }); |
| | 7 | 92 | | } |
| | | 93 | | |
| | | 94 | | /// <summary> |
| | | 95 | | /// Merges the result1 with the result2. |
| | | 96 | | /// </summary> |
| | | 97 | | /// <param name="result1">The first result.</param> |
| | | 98 | | /// <param name="result2">The second result.</param> |
| | | 99 | | private void MergeResults(ParserResult result1, ParserResult result2) |
| | 9 | 100 | | { |
| | 9 | 101 | | Interlocked.Increment(ref this.mergeCount); |
| | 9 | 102 | | int currentProgress = this.mergeCount; |
| | 9 | 103 | | ConsoleLogger.Debug("Starting merging result {0}", currentProgress); |
| | 9 | 104 | | result1.Merge(result2); |
| | 9 | 105 | | ConsoleLogger.Debug("Finished merging result {0}", currentProgress); |
| | 9 | 106 | | } |
| | | 107 | | |
| | | 108 | | /// <summary> |
| | | 109 | | /// Creates the producer which parses the files in parallel and creates parser results out of it. |
| | | 110 | | /// </summary> |
| | | 111 | | /// <param name="reportFiles">The files to parse.</param> |
| | | 112 | | /// <param name="collection">The block collection to add the parsed results to.</param> |
| | | 113 | | /// <returns>The Task.</returns> |
| | | 114 | | private static Task CreateProducer(IReadOnlyCollection<string> reportFiles, BlockingCollection<ParserResult> collect |
| | 7 | 115 | | { |
| | 7 | 116 | | return Task.Factory.StartNew(() => |
| | 7 | 117 | | { |
| | 7 | 118 | | try |
| | 7 | 119 | | { |
| | 7 | 120 | | int counter = 0; |
| | 7 | 121 | | Parallel.ForEach( |
| | 7 | 122 | | reportFiles, |
| | 7 | 123 | | new ParallelOptions { MaxDegreeOfParallelism = 1 }, |
| | 7 | 124 | | reportFile => |
| | 7 | 125 | | { |
| | 7 | 126 | | int number = Interlocked.Increment(ref counter); |
| | 7 | 127 | | ConsoleLogger.Debug("Loading report '{0}' {1}/{2} in memory", reportFile, number, reportFiles.Count) |
| | 7 | 128 | | try |
| | 7 | 129 | | { |
| | 7 | 130 | | List<ParserResult> parserResults = [.. ParseXmlFile(reportFile)]; |
| | 36 | 131 | | foreach (ParserResult parserResult in parserResults) |
| | 9 | 132 | | { |
| | 9 | 133 | | collection.Add(parserResult); |
| | 9 | 134 | | } |
| | 7 | 135 | | |
| | 6 | 136 | | if (parserResults.Count == 0 && reportFile.EndsWith(".coverage", StringComparison.OrdinalIgnoreC |
| | 0 | 137 | | { |
| | 0 | 138 | | ConsoleLogger.Warn( |
| | 0 | 139 | | "It seems that the report file '{0}' is a binary format generated by a Visual Studio cod |
| | 0 | 140 | | + "Please convert to XML format with 'CodeCoverage.exe'", |
| | 0 | 141 | | reportFile); |
| | 0 | 142 | | } |
| | 7 | 143 | | |
| | 6 | 144 | | ConsoleLogger.Debug("Finished parsing '{0}' {1}/{2}", reportFile, number, reportFiles.Count); |
| | 6 | 145 | | } |
| | 1 | 146 | | catch (Exception ex) when (ex is not UnsupportedParserException) |
| | 1 | 147 | | { |
| | 1 | 148 | | ConsoleLogger.Error( |
| | 1 | 149 | | "Error during reading report '{0}' (Size: {1}): {2}", |
| | 1 | 150 | | reportFile, |
| | 1 | 151 | | GetHumanReadableFileSize(reportFile), |
| | 1 | 152 | | ex.GetExceptionMessageForDisplay()); |
| | 1 | 153 | | } |
| | 14 | 154 | | }); |
| | 7 | 155 | | } |
| | 7 | 156 | | finally |
| | 7 | 157 | | { |
| | 7 | 158 | | ConsoleLogger.Debug("Parsing of {0} files completed", reportFiles.Count); |
| | 7 | 159 | | collection.CompleteAdding(); |
| | 7 | 160 | | } |
| | 14 | 161 | | }); |
| | 7 | 162 | | } |
| | | 163 | | |
| | | 164 | | /// <summary> |
| | | 165 | | /// Load elements in memory balanced manner. |
| | | 166 | | /// </summary> |
| | | 167 | | /// <param name="filePath">The filepath of the covergae file to load.</param> |
| | | 168 | | /// <param name="elementName">The name of the elemens to load.</param> |
| | | 169 | | /// <returns>The elements matchig the name.</returns> |
| | | 170 | | private static IEnumerable<XElement> GetXElements(string filePath, string elementName) |
| | 7 | 171 | | { |
| | 7 | 172 | | var readerSettings = new XmlReaderSettings() { DtdProcessing = DtdProcessing.Parse, XmlResolver = null }; |
| | 7 | 173 | | using XmlReader reader = XmlReader.Create(filePath, readerSettings); |
| | 30 | 174 | | while (reader.Read()) |
| | 24 | 175 | | { |
| | 24 | 176 | | if (reader.NodeType == XmlNodeType.Element && |
| | 24 | 177 | | reader.Name == elementName) |
| | 9 | 178 | | { |
| | 9 | 179 | | if (XNode.ReadFrom(reader) is XElement element) |
| | 9 | 180 | | { |
| | 9 | 181 | | yield return element; |
| | 9 | 182 | | } |
| | 9 | 183 | | } |
| | | 184 | | |
| | 24 | 185 | | } |
| | 6 | 186 | | } |
| | | 187 | | |
| | | 188 | | /// <summary> |
| | | 189 | | /// Tries to initiate the correct parsers for the given XML report. The result is merged into the given result. |
| | | 190 | | /// The report may contain several reports. For every report an extra parser is initiated. |
| | | 191 | | /// </summary> |
| | | 192 | | /// <param name="filePath">The report file path to parse.</param> |
| | | 193 | | /// <returns>The parser result.</returns> |
| | | 194 | | private static IEnumerable<ParserResult> ParseXmlFile(string filePath) |
| | 7 | 195 | | { |
| | 7 | 196 | | var elements = GetXElements(filePath, "CoverageSession").ToArray(); |
| | | 197 | | |
| | 6 | 198 | | if (elements.Length > 0) |
| | 6 | 199 | | { |
| | 36 | 200 | | foreach (var item in elements) |
| | 9 | 201 | | { |
| | 9 | 202 | | ConsoleLogger.Debug("Preprocessing report"); |
| | 9 | 203 | | new OpenCoverReportPreprocessor().Execute(item); |
| | | 204 | | |
| | 9 | 205 | | ConsoleLogger.Debug("Initiating parser for OpenCover"); |
| | 9 | 206 | | yield return new OpenCoverParser().Parse(item); |
| | 9 | 207 | | } |
| | | 208 | | |
| | 6 | 209 | | yield break; |
| | | 210 | | } |
| | 0 | 211 | | } |
| | | 212 | | |
| | | 213 | | /// <summary> |
| | | 214 | | /// Get the file size in human readable format. |
| | | 215 | | /// If size information is not available (e.g. IOException), '-' is returned. |
| | | 216 | | /// </summary> |
| | | 217 | | /// <param name="fileName">The name of the file.</param> |
| | | 218 | | /// <returns>The file size.</returns> |
| | | 219 | | private static string GetHumanReadableFileSize(string fileName) |
| | 1 | 220 | | { |
| | | 221 | | try |
| | 1 | 222 | | { |
| | 1 | 223 | | long byteCount = new FileInfo(fileName).Length; |
| | | 224 | | |
| | 0 | 225 | | string[] suffixes = ["B", "KB", "MB", "GB"]; |
| | | 226 | | |
| | 0 | 227 | | if (byteCount == 0) |
| | 0 | 228 | | { |
| | 0 | 229 | | return "0" + suffixes[0]; |
| | | 230 | | } |
| | | 231 | | |
| | 0 | 232 | | long bytes = Math.Abs(byteCount); |
| | 0 | 233 | | int place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); |
| | 0 | 234 | | double num = Math.Round(bytes / Math.Pow(1024, place), 1); |
| | 0 | 235 | | return (Math.Sign(byteCount) * num).ToString() + suffixes[place]; |
| | | 236 | | } |
| | 1 | 237 | | catch (Exception) |
| | 1 | 238 | | { |
| | 1 | 239 | | return "-"; |
| | | 240 | | } |
| | 1 | 241 | | } |
| | | 242 | | } |