Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Microsoft.TestPlatform.ObjectModel\Microsoft.TestPlatform.ObjectModel.csproj" />
<ProjectReference Include="..\..\Microsoft.TestPlatform.Utilities\Microsoft.TestPlatform.Utilities.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Internal.CodeCoverage">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Microsoft.VisualStudio.Coverage
using System.Xml;
using Interfaces;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection;
using Microsoft.VisualStudio.TestPlatform.Utilities;
using TestPlatform.ObjectModel;
using TraceCollector;
using TraceCollector.Interfaces;
Expand Down Expand Up @@ -107,10 +108,29 @@ public virtual void Initialize(
IDataCollectionSink dataSink,
IDataCollectionLogger logger)
{
var defaultConfigurationElement = DynamicCoverageDataCollectorImpl.GetDefaultConfiguration();

try
{
var processor = new CodeCoverageRunSettingsProcessor(defaultConfigurationElement);
configurationElement = (XmlElement)processor.Process(configurationElement);
}
catch (Exception ex)
{
EqtTrace.Warning(
string.Format(
CultureInfo.CurrentCulture,
string.Join(
" ",
"DynamicCoverageDataCollectorImpl.Initialize: Exception encountered while processing the configuration element.",
"Keeping the configuration element unaltered. More info about the exception: {0}"),
ex.Message));
}

EqtTrace.Info("DynamicCoverageDataCollectorImpl.Initialize: Initialize configuration. ");
if (string.IsNullOrEmpty(configurationElement?.InnerXml))
{
configurationElement = DynamicCoverageDataCollectorImpl.GetDefaultConfiguration();
configurationElement = defaultConfigurationElement;
}

this.logger = logger;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.VisualStudio.TestPlatform.Utilities
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.XPath;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;

/// <summary>
/// Represents the run settings processor for code coverage data collectors.
/// </summary>
public class CodeCoverageRunSettingsProcessor
{
#region Members
/// <summary>
/// Represents the default settings loaded as an <see cref="XmlNode"/>.
/// </summary>
private XmlNode defaultSettingsRootNode;
#endregion

#region Constructors & Helpers
/// <summary>
/// Constructs an <see cref="CodeCoverageRunSettingsProcessor"/> object.
/// </summary>
///
/// <param name="defaultSettingsRootNode">The default settings root node.</param>
public CodeCoverageRunSettingsProcessor(XmlNode defaultSettingsRootNode)
{
if (defaultSettingsRootNode == null)
{
throw new ArgumentNullException("Default settings root node is null.");
}

this.defaultSettingsRootNode = defaultSettingsRootNode;
}
#endregion

#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 (string.IsNullOrEmpty(currentSettings))
{
return null;
}

// Load current settings from string.
var document = new XmlDocument();
document.LoadXml(currentSettings);

return this.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)
{
if (currentSettingsDocument == null)
{
return null;
}

return this.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 = this.SelectNodeOrAddDefaults(
currentSettingsRootNode,
this.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 = this.ExtractNode(
this.defaultSettingsRootNode,
this.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 = this.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 = this.ExtractNode(
defaultCodeCoverageNode,
this.BuildPath(exclusion));

// Merge the current and default settings for the current exclusion rule.
this.MergeNodes(currentNode, defaultNode);
}

return currentSettingsRootNode;
}
#endregion

#region Private Methods
/// <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 cref="null"/> otherwise.</returns>
private 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 = this.ExtractNode(currentNode, "." + currentPathComponent);

// Extraction is pruned here because we shouldn't be processing the current node.
if (tempNode != null && !this.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 = this.ExtractNode(
defaultRootNode,
partialPath.ToString());

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 cref="true"/> if the node should be processed, <see cref="false"/> otherwise.
/// </returns>
private bool ShouldProcessCurrentExclusion(XmlNode node)
{
const string attributeName = "mergeDefaults";

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 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 cref="null"/> otherwise.</returns>
private 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 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);
}

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