|
// 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);
}
}
}
|