From 226514ede8434aaaeb06c5e99d96a61b2171eee5 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Mon, 27 Mar 2023 09:38:29 +0200 Subject: [PATCH 1/7] Populate location of attributes/elements created from other node with location --- src/Build/Construction/ProjectMetadataElement.cs | 3 ++- src/Build/Construction/ProjectRootElement.cs | 13 +++++++++++-- src/Build/ElementLocation/XmlElementWithLocation.cs | 5 +++++ src/Build/Evaluation/ProjectParser.cs | 4 ++-- src/Shared/XmlUtilities.cs | 2 ++ 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Build/Construction/ProjectMetadataElement.cs b/src/Build/Construction/ProjectMetadataElement.cs index a8ba5cd5c10..1537c253f83 100644 --- a/src/Build/Construction/ProjectMetadataElement.cs +++ b/src/Build/Construction/ProjectMetadataElement.cs @@ -100,13 +100,14 @@ public string Value /// Creates an unparented ProjectMetadataElement, wrapping an unparented XmlElement. /// Caller should then ensure the element is added to a parent. /// - internal static ProjectMetadataElement CreateDisconnected(string name, ProjectRootElement containingProject) + internal static ProjectMetadataElement CreateDisconnected(string name, ProjectRootElement containingProject, ElementLocation location = null) { XmlUtilities.VerifyThrowArgumentValidElementName(name); ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name); ErrorUtilities.VerifyThrowInvalidOperation(!XMakeElements.ReservedItemNames.Contains(name), "CannotModifyReservedItemMetadata", name); XmlElementWithLocation element = containingProject.CreateElement(name); + element.Location = location; return new ProjectMetadataElement(element, containingProject); } diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index b0ff4459c4a..cf70e243107 100644 --- a/src/Build/Construction/ProjectRootElement.cs +++ b/src/Build/Construction/ProjectRootElement.cs @@ -1326,14 +1326,14 @@ public ProjectMetadataElement CreateMetadataElement(string name) /// Creates a metadata node. /// Caller must add it to the location of choice in the project. /// - public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue) + public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue, ElementLocation location = null) { if (Link != null) { return RootLink.CreateMetadataElement(name, unevaluatedValue); } - ProjectMetadataElement metadatum = ProjectMetadataElement.CreateDisconnected(name, this); + ProjectMetadataElement metadatum = ProjectMetadataElement.CreateDisconnected(name, this, location); metadatum.Value = unevaluatedValue; @@ -1785,6 +1785,15 @@ internal static ProjectRootElement OpenProjectOrSolution(string fullPath, IDicti return projectRootElement; } + /// + /// Creates a metadata node. + /// Caller must add it to the location of choice in the project. + /// + internal ProjectMetadataElement CreateMetadataElement(XmlAttributeWithLocation attribute) + { + return CreateMetadataElement(attribute.Name, attribute.Value, attribute.Location); + } + /// /// Creates a XmlElement with the specified name in the document /// containing this project. diff --git a/src/Build/ElementLocation/XmlElementWithLocation.cs b/src/Build/ElementLocation/XmlElementWithLocation.cs index ddbf27a08de..9c88b56ce8f 100644 --- a/src/Build/ElementLocation/XmlElementWithLocation.cs +++ b/src/Build/ElementLocation/XmlElementWithLocation.cs @@ -101,6 +101,11 @@ internal ElementLocation Location return _elementLocation; } + + set + { + _elementLocation = value; + } } /// diff --git a/src/Build/Evaluation/ProjectParser.cs b/src/Build/Evaluation/ProjectParser.cs index e622acbfdc5..5de3520dab8 100644 --- a/src/Build/Evaluation/ProjectParser.cs +++ b/src/Build/Evaluation/ProjectParser.cs @@ -324,7 +324,7 @@ private ProjectItemElement ParseProjectItemElement(XmlElementWithLocation elemen } else if (isValidMetadataNameInAttribute) { - ProjectMetadataElement metadatum = _project.CreateMetadataElement(attribute.Name, attribute.Value); + ProjectMetadataElement metadatum = _project.CreateMetadataElement(attribute); metadatum.ExpressedAsAttribute = true; metadatum.Parent = item; @@ -744,7 +744,7 @@ private ProjectItemDefinitionElement ParseProjectItemDefinitionXml(XmlElementWit } else if (isValidMetadataNameInAttribute) { - ProjectMetadataElement metadatum = _project.CreateMetadataElement(attribute.Name, attribute.Value); + ProjectMetadataElement metadatum = _project.CreateMetadataElement(attribute); metadatum.ExpressedAsAttribute = true; metadatum.Parent = itemDefinition; diff --git a/src/Shared/XmlUtilities.cs b/src/Shared/XmlUtilities.cs index e37749e172c..ef1747a6429 100644 --- a/src/Shared/XmlUtilities.cs +++ b/src/Shared/XmlUtilities.cs @@ -36,6 +36,8 @@ internal static XmlElementWithLocation RenameXmlElement(XmlElementWithLocation o ? (XmlElementWithLocation)oldElement.OwnerDocument.CreateElement(newElementName) : (XmlElementWithLocation)oldElement.OwnerDocument.CreateElement(newElementName, xmlNamespace); + newElement.Location = oldElement.Location; + // Copy over all the attributes. foreach (XmlAttribute oldAttribute in oldElement.Attributes) { From 5ab53ea217c98897d419bb037303566e40a98de9 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Mon, 27 Mar 2023 13:29:27 +0000 Subject: [PATCH 2/7] Handle nulls gracefully --- src/Build/Construction/ProjectMetadataElement.cs | 5 ++++- .../ConstructionObjectLinks/ProjectRootElementLink.cs | 2 +- src/Shared/XmlUtilities.cs | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Build/Construction/ProjectMetadataElement.cs b/src/Build/Construction/ProjectMetadataElement.cs index 1537c253f83..b026711dcf9 100644 --- a/src/Build/Construction/ProjectMetadataElement.cs +++ b/src/Build/Construction/ProjectMetadataElement.cs @@ -107,7 +107,10 @@ internal static ProjectMetadataElement CreateDisconnected(string name, ProjectRo ErrorUtilities.VerifyThrowInvalidOperation(!XMakeElements.ReservedItemNames.Contains(name), "CannotModifyReservedItemMetadata", name); XmlElementWithLocation element = containingProject.CreateElement(name); - element.Location = location; + if (location != null) + { + element.Location = location; + } return new ProjectMetadataElement(element, containingProject); } diff --git a/src/Build/ObjectModelRemoting/ConstructionObjectLinks/ProjectRootElementLink.cs b/src/Build/ObjectModelRemoting/ConstructionObjectLinks/ProjectRootElementLink.cs index d45cc3f213d..b485722d3e3 100644 --- a/src/Build/ObjectModelRemoting/ConstructionObjectLinks/ProjectRootElementLink.cs +++ b/src/Build/ObjectModelRemoting/ConstructionObjectLinks/ProjectRootElementLink.cs @@ -113,7 +113,7 @@ public abstract class ProjectRootElementLink : ProjectElementContainerLink public abstract ProjectMetadataElement CreateMetadataElement(string name); /// - /// Facilitate remoting the . + /// Facilitate remoting the . /// public abstract ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue); diff --git a/src/Shared/XmlUtilities.cs b/src/Shared/XmlUtilities.cs index ef1747a6429..4230c4c57c8 100644 --- a/src/Shared/XmlUtilities.cs +++ b/src/Shared/XmlUtilities.cs @@ -36,7 +36,10 @@ internal static XmlElementWithLocation RenameXmlElement(XmlElementWithLocation o ? (XmlElementWithLocation)oldElement.OwnerDocument.CreateElement(newElementName) : (XmlElementWithLocation)oldElement.OwnerDocument.CreateElement(newElementName, xmlNamespace); - newElement.Location = oldElement.Location; + if (oldElement.Location != null) + { + newElement.Location = oldElement.Location; + } // Copy over all the attributes. foreach (XmlAttribute oldAttribute in oldElement.Attributes) From 9ad18058019bc8a4bfe3d4aa6d6bcecf4c26cab9 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Mon, 27 Mar 2023 18:35:46 +0200 Subject: [PATCH 3/7] Fix API breaking change --- src/Build/Construction/ProjectRootElement.cs | 11 ++++++++++- .../ConstructionObjectLinks/ProjectRootElementLink.cs | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index cf70e243107..f75a03150c1 100644 --- a/src/Build/Construction/ProjectRootElement.cs +++ b/src/Build/Construction/ProjectRootElement.cs @@ -1326,7 +1326,16 @@ public ProjectMetadataElement CreateMetadataElement(string name) /// Creates a metadata node. /// Caller must add it to the location of choice in the project. /// - public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue, ElementLocation location = null) + public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue) + { + return this.CreateMetadataElement(name, unevaluatedValue, null); + } + + /// + /// Creates a metadata node. + /// Caller must add it to the location of choice in the project. + /// + public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue, ElementLocation location) { if (Link != null) { diff --git a/src/Build/ObjectModelRemoting/ConstructionObjectLinks/ProjectRootElementLink.cs b/src/Build/ObjectModelRemoting/ConstructionObjectLinks/ProjectRootElementLink.cs index b485722d3e3..d45cc3f213d 100644 --- a/src/Build/ObjectModelRemoting/ConstructionObjectLinks/ProjectRootElementLink.cs +++ b/src/Build/ObjectModelRemoting/ConstructionObjectLinks/ProjectRootElementLink.cs @@ -113,7 +113,7 @@ public abstract class ProjectRootElementLink : ProjectElementContainerLink public abstract ProjectMetadataElement CreateMetadataElement(string name); /// - /// Facilitate remoting the . + /// Facilitate remoting the . /// public abstract ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue); From 438d48b3756eaaeebd8f25724c0cbdee6c11eec0 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Mon, 17 Apr 2023 18:39:00 +0200 Subject: [PATCH 4/7] Replace setter with AsyncLocal --- .../Construction/ProjectMetadataElement.cs | 6 +--- src/Build/Construction/ProjectRootElement.cs | 4 +-- .../XmlDocumentWithLocation.cs | 28 +++++++++++++++++++ .../ElementLocation/XmlElementWithLocation.cs | 5 ---- src/Shared/XmlUtilities.cs | 10 ++----- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/Build/Construction/ProjectMetadataElement.cs b/src/Build/Construction/ProjectMetadataElement.cs index b026711dcf9..473f39c0476 100644 --- a/src/Build/Construction/ProjectMetadataElement.cs +++ b/src/Build/Construction/ProjectMetadataElement.cs @@ -106,11 +106,7 @@ internal static ProjectMetadataElement CreateDisconnected(string name, ProjectRo ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name); ErrorUtilities.VerifyThrowInvalidOperation(!XMakeElements.ReservedItemNames.Contains(name), "CannotModifyReservedItemMetadata", name); - XmlElementWithLocation element = containingProject.CreateElement(name); - if (location != null) - { - element.Location = location; - } + XmlElementWithLocation element = containingProject.CreateElement(name, location); return new ProjectMetadataElement(element, containingProject); } diff --git a/src/Build/Construction/ProjectRootElement.cs b/src/Build/Construction/ProjectRootElement.cs index f75a03150c1..626751e11cf 100644 --- a/src/Build/Construction/ProjectRootElement.cs +++ b/src/Build/Construction/ProjectRootElement.cs @@ -1807,10 +1807,10 @@ internal ProjectMetadataElement CreateMetadataElement(XmlAttributeWithLocation a /// Creates a XmlElement with the specified name in the document /// containing this project. /// - internal XmlElementWithLocation CreateElement(string name) + internal XmlElementWithLocation CreateElement(string name, ElementLocation location = null) { ErrorUtilities.VerifyThrow(Link == null, "External project"); - return (XmlElementWithLocation)XmlDocument.CreateElement(name, XmlNamespace); + return (XmlElementWithLocation)XmlDocument.CreateElement(name, XmlNamespace, location); } /// diff --git a/src/Build/ElementLocation/XmlDocumentWithLocation.cs b/src/Build/ElementLocation/XmlDocumentWithLocation.cs index bbc34c86a57..18d021d5c61 100644 --- a/src/Build/ElementLocation/XmlDocumentWithLocation.cs +++ b/src/Build/ElementLocation/XmlDocumentWithLocation.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading; using System.Xml; using Microsoft.Build.Internal; using Microsoft.Build.Shared; @@ -59,6 +60,13 @@ internal class XmlDocumentWithLocation : XmlDocument /// private bool? _loadAsReadOnly; + /// + /// Location of the element to be created via 'CreateElement' call. So that we can + /// receive and use location from the caller up the stack even if we are being called via + /// internal methods. + /// + private readonly AsyncLocal _elementLocation = new AsyncLocal(); + /// /// Constructor /// @@ -180,6 +188,22 @@ public override void Load(string fullPath) } } + internal XmlElement CreateElement(string localName, string namespaceURI, ElementLocation location) + { + if (location != null) + { + this._elementLocation.Value = location; + } + try + { + return CreateElement(localName, namespaceURI); + } + finally + { + this._elementLocation.Value = null; + } + } + /// /// Called during load, to add an element. /// @@ -192,6 +216,10 @@ public override XmlElement CreateElement(string prefix, string localName, string { return new XmlElementWithLocation(prefix, localName, namespaceURI, this, _reader.LineNumber, _reader.LinePosition); } + else if (_elementLocation?.Value != null) + { + return new XmlElementWithLocation(prefix, localName, namespaceURI, this, _elementLocation.Value.Line, _elementLocation.Value.Column); + } // Must be a subsequent edit; we can't provide location information return new XmlElementWithLocation(prefix, localName, namespaceURI, this); diff --git a/src/Build/ElementLocation/XmlElementWithLocation.cs b/src/Build/ElementLocation/XmlElementWithLocation.cs index 9c88b56ce8f..ddbf27a08de 100644 --- a/src/Build/ElementLocation/XmlElementWithLocation.cs +++ b/src/Build/ElementLocation/XmlElementWithLocation.cs @@ -101,11 +101,6 @@ internal ElementLocation Location return _elementLocation; } - - set - { - _elementLocation = value; - } } /// diff --git a/src/Shared/XmlUtilities.cs b/src/Shared/XmlUtilities.cs index 4230c4c57c8..27e32d603cb 100644 --- a/src/Shared/XmlUtilities.cs +++ b/src/Shared/XmlUtilities.cs @@ -32,14 +32,8 @@ internal static XmlElementWithLocation RenameXmlElement(XmlElementWithLocation o return oldElement; } - XmlElementWithLocation newElement = (xmlNamespace == null) - ? (XmlElementWithLocation)oldElement.OwnerDocument.CreateElement(newElementName) - : (XmlElementWithLocation)oldElement.OwnerDocument.CreateElement(newElementName, xmlNamespace); - - if (oldElement.Location != null) - { - newElement.Location = oldElement.Location; - } + XmlElementWithLocation newElement = + (XmlElementWithLocation)((XmlDocumentWithLocation)oldElement.OwnerDocument).CreateElement(newElementName, xmlNamespace ?? string.Empty, oldElement.Location); // Copy over all the attributes. foreach (XmlAttribute oldAttribute in oldElement.Attributes) From c03fd0f483d71b1e0aa508f328e6da91f1102e1c Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Mon, 17 Apr 2023 19:44:37 +0200 Subject: [PATCH 5/7] Remove temporary hack --- .../RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs index fcf7564d228..096c90e5ff9 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs @@ -185,7 +185,6 @@ private void ExecuteAdd(ProjectItemGroupTaskItemInstance child, ItemBucket bucke if (condition) { ExpanderOptions expanderOptions = ExpanderOptions.ExpandAll; - ElementLocation location = metadataInstance.Location; if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_6) && // If multiple buckets were expanded - we do not want to repeat same error for same metadatum on a same line bucket.BucketSequenceNumber == 0 && @@ -193,11 +192,9 @@ private void ExecuteAdd(ProjectItemGroupTaskItemInstance child, ItemBucket bucke child.Include.IndexOf("@(", StringComparison.Ordinal) == -1) { expanderOptions |= ExpanderOptions.LogOnItemMetadataSelfReference; - // Temporary workaround of unavailability of full Location info on metadata: https://github.com/dotnet/msbuild/issues/8579 - location = child.Location; } - string evaluatedValue = bucket.Expander.ExpandIntoStringLeaveEscaped(metadataInstance.Value, expanderOptions, location, loggingContext); + string evaluatedValue = bucket.Expander.ExpandIntoStringLeaveEscaped(metadataInstance.Value, expanderOptions, metadataInstance.Location, loggingContext); // This both stores the metadata so we can add it to all the items we just created later, and // exposes this metadata to further metadata evaluations in subsequent loop iterations. From c912e28dd91aa39f761fe1e4231c36d88232b17a Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Mon, 17 Apr 2023 19:50:23 +0200 Subject: [PATCH 6/7] Add missing comment --- src/Build/ElementLocation/XmlDocumentWithLocation.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Build/ElementLocation/XmlDocumentWithLocation.cs b/src/Build/ElementLocation/XmlDocumentWithLocation.cs index 18d021d5c61..88b0de71dbb 100644 --- a/src/Build/ElementLocation/XmlDocumentWithLocation.cs +++ b/src/Build/ElementLocation/XmlDocumentWithLocation.cs @@ -188,6 +188,15 @@ public override void Load(string fullPath) } } + /// + /// Called during parse, to add an element. + /// + /// + /// We create our own kind of element, that we can give location information to. + /// In order to pass the location through the callchain, that contains XmlDocument function + /// that then calls back to our XmlDocumentWithLocation (so we cannot use call stack via passing via parameters), + /// we use async local field, that simulates variable on call stack. + /// internal XmlElement CreateElement(string localName, string namespaceURI, ElementLocation location) { if (location != null) From d87a96f04dd308f1dce7cd75ff77b7f2f4b7c375 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Tue, 18 Apr 2023 12:17:53 +0200 Subject: [PATCH 7/7] Add unit tests --- .../Construction/ProjectItemElement_Tests.cs | 24 +++++++++++++++++++ src/Build.UnitTests/BackEnd/MSBuild_Tests.cs | 8 +++++-- src/Shared/UnitTests/MockLogger.cs | 13 ++++++---- src/Shared/UnitTests/ObjectModelHelpers.cs | 5 ++++ 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/Build.OM.UnitTests/Construction/ProjectItemElement_Tests.cs b/src/Build.OM.UnitTests/Construction/ProjectItemElement_Tests.cs index ac62b8e21d7..721a4521fdc 100644 --- a/src/Build.OM.UnitTests/Construction/ProjectItemElement_Tests.cs +++ b/src/Build.OM.UnitTests/Construction/ProjectItemElement_Tests.cs @@ -82,6 +82,30 @@ public void ReadNoChildren(string project) Assert.Equal(0, Helpers.Count(item.Metadata)); } + [Fact] + public void ReadMetadataLocationPreserved() + { + string project = """ + + + + + + + + """; + + ProjectItemElement item = GetItemFromContent(project); + Assert.Equal(2, item.Metadata.Count); + ProjectMetadataElement metadatum1 = item.Metadata.First(); + ProjectMetadataElement metadatum2 = item.Metadata.Skip(1).First(); + + Assert.Equal(4, metadatum1.Location.Line); + Assert.Equal(4, metadatum2.Location.Line); + Assert.Equal(27, metadatum1.Location.Column); + Assert.Equal(43, metadatum2.Location.Column); + } + /// /// Read item with no include /// diff --git a/src/Build.UnitTests/BackEnd/MSBuild_Tests.cs b/src/Build.UnitTests/BackEnd/MSBuild_Tests.cs index 98d37bfc4cb..9b0a1eae9bb 100644 --- a/src/Build.UnitTests/BackEnd/MSBuild_Tests.cs +++ b/src/Build.UnitTests/BackEnd/MSBuild_Tests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; - using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; using Microsoft.Build.Framework; @@ -824,7 +823,8 @@ public void ItemsRecursionWithinTarget() """; - var projectFile = env.CreateFile("test.proj", ObjectModelHelpers.CleanupFileContents(projectContent)); + string projFileName = "test.proj"; + var projectFile = env.CreateFile(projFileName, ObjectModelHelpers.CleanupFileContents(projectContent)); MockLogger logger = new MockLogger(_testOutput); ObjectModelHelpers.BuildTempProjectFileExpectSuccess(projectFile.Path, logger); @@ -839,6 +839,10 @@ public void ItemsRecursionWithinTarget() logger.AssertLogContains(string.Format(ResourceUtilities.GetResourceString("ItemReferencingSelfInTarget"), "iin1", "Filename")); logger.AssertLogContains(string.Format(ResourceUtilities.GetResourceString("ItemReferencingSelfInTarget"), "iin1", "Extension")); logger.AssertMessageCount("MSB4120", 6); + // The location of the offending attribute (TargetPath) is transferred - for both metadatums (%(Filename) and %(Extension)) on correct locations in xml + logger.AssertMessageCount($"{projFileName}(4,34):", 2, false); + logger.AssertMessageCount($"{projFileName}(5,34):", 2, false); + logger.AssertMessageCount($"{projFileName}(6,34):", 2, false); Assert.Equal(0, logger.WarningCount); Assert.Equal(0, logger.ErrorCount); } diff --git a/src/Shared/UnitTests/MockLogger.cs b/src/Shared/UnitTests/MockLogger.cs index 9a9bec6d5c9..c52765ccd49 100644 --- a/src/Shared/UnitTests/MockLogger.cs +++ b/src/Shared/UnitTests/MockLogger.cs @@ -282,8 +282,13 @@ internal void LoggerEventHandler(object sender, BuildEventArgs eventArgs) bool logMessage = !(eventArgs is BuildFinishedEventArgs) || LogBuildFinished; if (logMessage) { - _fullLog.AppendLine(eventArgs.Message); - _testOutputHelper?.WriteLine(eventArgs.Message); + string msg = eventArgs.Message; + if (eventArgs is BuildMessageEventArgs m && m.LineNumber != 0) + { + msg = $"{m.File}({m.LineNumber},{m.ColumnNumber}): {msg}"; + } + _fullLog.AppendLine(msg); + _testOutputHelper?.WriteLine(msg); } break; } @@ -496,9 +501,9 @@ internal void AssertLogDoesntContain(string contains) /// internal void AssertNoWarnings() => Assert.Equal(0, WarningCount); - internal void AssertMessageCount(string message, int expectedCount) + internal void AssertMessageCount(string message, int expectedCount, bool regexSearch = true) { - var matches = Regex.Matches(FullLog, message); + var matches = Regex.Matches(FullLog, regexSearch ? message : Regex.Escape(message)); matches.Count.ShouldBe(expectedCount); } } diff --git a/src/Shared/UnitTests/ObjectModelHelpers.cs b/src/Shared/UnitTests/ObjectModelHelpers.cs index 31ce5b63c29..b5b4ad1b610 100644 --- a/src/Shared/UnitTests/ObjectModelHelpers.cs +++ b/src/Shared/UnitTests/ObjectModelHelpers.cs @@ -1155,6 +1155,11 @@ internal static string GetOSPlatformAsString() /// internal static int Count(IEnumerable enumerable) { + if (enumerable is ICollection c) + { + return c.Count; + } + int i = 0; foreach (object _ in enumerable) {