| File: CodeCoverageRunSettingsProcessor.cs | Web Access |
| Project: src\vstest\src\Microsoft.TestPlatform.Utilities\Microsoft.TestPlatform.Utilities.csproj (Microsoft.TestPlatform.Utilities) |
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml; using System.Xml.XPath; using Microsoft.VisualStudio.TestPlatform.ObjectModel; namespace Microsoft.VisualStudio.TestPlatform.Utilities; /// <summary> /// Represents the run settings processor for code coverage data collectors. /// </summary> public class CodeCoverageRunSettingsProcessor { /// <summary> /// Represents the default settings loaded as an <see cref="XmlNode"/>. /// </summary> private readonly XmlNode _defaultSettingsRootNode; /// <summary> /// Constructs an <see cref="CodeCoverageRunSettingsProcessor"/> object. /// </summary> /// /// <param name="defaultSettingsRootNode">The default settings root node.</param> public CodeCoverageRunSettingsProcessor(XmlNode defaultSettingsRootNode) { _defaultSettingsRootNode = defaultSettingsRootNode ?? throw new ArgumentNullException(nameof(defaultSettingsRootNode)); } #region Public Interface /// <summary> /// Processes the current settings for the code coverage data collector. /// </summary> /// /// <param name="currentSettings">The code coverage settings.</param> /// /// <returns>An updated version of the current run settings.</returns> public XmlNode? Process(string? currentSettings) { if (currentSettings.IsNullOrEmpty()) { return null; } // Load current settings from string. var document = new XmlDocument(); document.LoadXml(currentSettings); return Process(document.DocumentElement); } /// <summary> /// Processes the current settings for the code coverage data collector. /// </summary> /// /// <param name="currentSettingsDocument"> /// The code coverage settings document. /// </param> /// /// <returns>An updated version of the current run settings.</returns> public XmlNode? Process(XmlDocument? currentSettingsDocument) { return currentSettingsDocument == null ? null : Process(currentSettingsDocument.DocumentElement); } /// <summary> /// Processes the current settings for the code coverage data collector. /// </summary> /// /// <param name="currentSettingsRootNode">The code coverage root element.</param> /// /// <returns>An updated version of the current run settings.</returns> public XmlNode? Process(XmlNode? currentSettingsRootNode) { if (currentSettingsRootNode == null) { return null; } // Get the code coverage node from the current settings. If unable to get any // particular component down the path just add the default values for that component // from the default settings document and return since there's nothing else to be done. var codeCoveragePathComponents = new List<string>() { "CodeCoverage" }; var currentCodeCoverageNode = SelectNodeOrAddDefaults( currentSettingsRootNode, _defaultSettingsRootNode, codeCoveragePathComponents); // Cannot extract current code coverage node from the given settings so we bail out. // However, the default code coverage node has already been added to the document's // root. if (currentCodeCoverageNode == null) { return currentSettingsRootNode; } // Get the code coverage node from the default settings. var defaultCodeCoverageNode = ExtractNode( _defaultSettingsRootNode, BuildPath(codeCoveragePathComponents)); // Create the exclusion type list. var exclusions = new List<IList<string>> { new List<string> { "ModulePaths", "Exclude" }, new List<string> { "Attributes", "Exclude" }, new List<string> { "Sources", "Exclude" }, new List<string> { "Functions", "Exclude" } }; foreach (var exclusion in exclusions) { // Get the <Exclude> node for the current exclusion type. If unable to get any // particular component down the path just add the default values for that // component from the default settings document and continue since there's nothing // else to be done. var currentNode = SelectNodeOrAddDefaults( currentCodeCoverageNode, defaultCodeCoverageNode, exclusion); // Check if the node extraction was successful and we should process the current // node in order to merge the current exclusion rules with the default ones. if (currentNode == null) { continue; } // Extract the <Exclude> node from the default settings. var defaultNode = ExtractNode( defaultCodeCoverageNode, BuildPath(exclusion)); // Merge the current and default settings for the current exclusion rule. MergeNodes(currentNode, defaultNode); } return currentSettingsRootNode; } #endregion /// <summary> /// Selects the node from the current settings node using the given /// <see cref="XPathNavigator"/> style path. If unable to select the requested node it adds /// default settings along the path. /// </summary> /// /// <param name="currentRootNode"> /// The root node from the current settings document for the extraction. /// </param> /// <param name="defaultRootNode"> /// The corresponding root node from the default settings document. /// </param> /// <param name="pathComponents">The path components.</param> /// /// <returns>The requested node if successful, <see langword="null"/> otherwise.</returns> private static XmlNode? SelectNodeOrAddDefaults( XmlNode currentRootNode, XmlNode? defaultRootNode, IList<string> pathComponents) { var currentNode = currentRootNode; var partialPath = new StringBuilder(); partialPath.Append('.'); foreach (var component in pathComponents) { var currentPathComponent = "/" + component; // Append the current path component to the partial path. partialPath.Append(currentPathComponent); // Extract the node corresponding to the latest path component. var tempNode = ExtractNode(currentNode, "." + currentPathComponent); // Extraction is pruned here because we shouldn't be processing the current node. if (tempNode != null && !ShouldProcessCurrentExclusion(tempNode)) { return null; } // If the current node extraction is unsuccessful then add the corresponding // default settings node and bail out. if (tempNode == null) { var defaultNode = ExtractNode(defaultRootNode, partialPath.ToString()); if (defaultNode == null) { return null; } var importedChild = currentNode.OwnerDocument!.ImportNode(defaultNode, true); currentNode.AppendChild(importedChild); return null; } // Node corresponding to the latest path component is the new root node for the // next extraction. currentNode = tempNode; } return currentNode; } /// <summary> /// Checks if we should process the current exclusion node. /// </summary> /// /// <param name="node">The current exclusion node.</param> /// /// <returns> /// <see langword="true"/> if the node should be processed, <see langword="false"/> otherwise. /// </returns> private static bool ShouldProcessCurrentExclusion(XmlNode node) { const string attributeName = "mergeDefaults"; if (node.Attributes == null) { return true; } foreach (XmlAttribute attribute in node.Attributes) { // If the attribute is present and set on 'false' we skip processing for the // current exclusion. if (attribute.Name == attributeName && bool.TryParse(attribute.Value, out var value) && !value) { return false; } } return true; } /// <summary> /// Assembles a relative path from the path given as components. /// </summary> /// /// <returns>A relative path built from path components.</returns> private static string BuildPath(IList<string> pathComponents) { return string.Join("/", new[] { "." }.Concat(pathComponents)); } /// <summary> /// Extracts the node specified by the current path using the provided node as root. /// </summary> /// /// <param name="node">The root to be used for extraction.</param> /// <param name="path">The path used to specify the requested node.</param> /// /// <returns>The extracted node if successful, <see langword="null"/> otherwise.</returns> private static XmlNode? ExtractNode(XmlNode? node, string path) { try { return node?.SelectSingleNode(path); } catch (XPathException ex) { EqtTrace.Error( "CodeCoverageRunSettingsProcessor.ExtractNode: Cannot select single node \"{0}\".", ex.Message); } return null; } /// <summary> /// Merges the current settings rules with the default settings rules. /// </summary> /// /// <param name="currentNode">The current settings root node.</param> /// <param name="defaultNode">The default settings root node.</param> private static void MergeNodes(XmlNode currentNode, XmlNode? defaultNode) { var exclusionCache = new HashSet<string>(); // Add current exclusions to the exclusion cache. foreach (XmlNode child in currentNode.ChildNodes) { exclusionCache.Add(child.OuterXml); } if (defaultNode is null) { return; } // Iterate through default exclusions and import missing ones. foreach (XmlNode child in defaultNode.ChildNodes) { if (exclusionCache.Contains(child.OuterXml)) { continue; } // Import missing default exclusions. var importedChild = currentNode.OwnerDocument!.ImportNode(child, true); currentNode.AppendChild(importedChild); } } }