-
Notifications
You must be signed in to change notification settings - Fork 343
Added support for default exclusion merging for code coverage #2431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cvpoienaru
merged 15 commits into
microsoft:master
from
cvpoienaru:copoiena/code-coverage-default-exclusions
Jun 3, 2020
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
752244a
Initial support for default exclusion merging
cvpoienaru 25a8e84
Refined default exclusion merging
cvpoienaru 3e1d208
Added partial path matching
cvpoienaru b4de430
Removed mergeDefaults attribute from template settings exclude tag
cvpoienaru 4717119
Added documentation
cvpoienaru a82fb7b
Inserted the settings-processing logic on the data collector level
cvpoienaru 0b98be9
Reworked settings processor interface
cvpoienaru f6c566f
Fixed compilation error
cvpoienaru bd63693
Enhanced default code coverage config string
cvpoienaru 771c5d8
Fixed unit tests
cvpoienaru b72b9b8
Fixed code review comments
cvpoienaru 0ca7f81
Removed unnecessary using directive
cvpoienaru 2e2b9f6
Added unit tests
cvpoienaru 6ead129
Fixed code review comments
cvpoienaru fdd840f
Added exception handling
cvpoienaru File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
318 changes: 318 additions & 0 deletions
318
src/Microsoft.TestPlatform.Utilities/CodeCoverageRunSettingsProcessor.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.