From b3df9172ffe13c5cd5f2719a2899351b6eee3766 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Thu, 5 Dec 2024 03:53:31 -0500 Subject: [PATCH 1/7] Add tests for KeyValuePairListEncoding, allow multiple separators. Convert DefineConstants input to semicolon-separated list before saving --- .../Debug/KeyValuePairListEncoding.cs | 17 +++---- .../DefineConstantsCSharpValueProvider.cs | 15 ++++-- .../KeyValuePairListEncodingTests.cs | 47 +++++++++++++++++++ 3 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs index ba0afb4d2c1..8bffbaac5af 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs @@ -4,14 +4,14 @@ namespace Microsoft.VisualStudio.ProjectSystem.Debug; internal static class KeyValuePairListEncoding { - public static IEnumerable<(string Name, string Value)> Parse(string input) + public static IEnumerable<(string Name, string Value)> Parse(string input, char separator = ',') { if (string.IsNullOrWhiteSpace(input)) { yield break; } - foreach (var entry in ReadEntries(input)) + foreach (var entry in ReadEntries(input, separator)) { var (entryKey, entryValue) = SplitEntry(entry); var decodedEntryKey = Decode(entryKey); @@ -23,13 +23,13 @@ internal static class KeyValuePairListEncoding } } - static IEnumerable ReadEntries(string rawText) + static IEnumerable ReadEntries(string rawText, char separator) { bool escaped = false; int entryStart = 0; for (int i = 0; i < rawText.Length; i++) { - if (rawText[i] == ',' && !escaped) + if (separator == rawText[i] && !escaped) { yield return rawText.Substring(entryStart, i - entryStart); entryStart = i + 1; @@ -67,7 +67,7 @@ static IEnumerable ReadEntries(string rawText) } } - return (string.Empty, string.Empty); + return (entry, string.Empty); } static string Decode(string value) @@ -76,12 +76,13 @@ static string Decode(string value) } } - public static string Format(IEnumerable<(string Name, string Value)> pairs) + public static string Format(IEnumerable<(string Name, string Value)> pairs, string separator = ",") { // Copied from ActiveLaunchProfileEnvironmentVariableValueProvider in the .NET Project System. // In future, EnvironmentVariablesNameValueListEncoding should be exported from that code base and imported here. - - return string.Join(",", pairs.Select(kvp => $"{Encode(kvp.Name)}={Encode(kvp.Value)}")); + return string.Join(separator, pairs.Select(kvp => string.IsNullOrEmpty(kvp.Value) + ? Encode(kvp.Name) + : $"{Encode(kvp.Name)}={Encode(kvp.Value)}")); static string Encode(string value) { diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs index 3aaef154158..7651b0d44bc 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs @@ -40,7 +40,7 @@ public override async Task OnGetUnevaluatedPropertyValueAsync(string pro return KeyValuePairListEncoding.Format( ParseDefinedConstantsFromUnevaluatedValue(unevaluatedDefineConstantsValue) - .Select(symbol => (Key: symbol, Value: bool.FalseString)) + .Select(symbol => (symbol, bool.FalseString)) ); } @@ -76,9 +76,18 @@ public override async Task IsValueDefinedInContextAsync(string propertyNam IEnumerable innerConstants = ParseDefinedConstantsFromUnevaluatedValue(await defaultProperties.GetUnevaluatedPropertyValueAsync(ConfiguredBrowseObject.DefineConstantsProperty) ?? string.Empty); - IEnumerable constantsToWrite = KeyValuePairListEncoding.Parse(unevaluatedPropertyValue) + // we receive a comma-separated list, we should convert it to a semicolon-separated list + // because each item may have multiple values separated by semicolons + // ie "A,B;C" should be converted to "A;B;C" + unevaluatedPropertyValue = KeyValuePairListEncoding.Format( + KeyValuePairListEncoding.Parse(unevaluatedPropertyValue, separator: ','), + separator: ";"); + + IEnumerable constantsToWrite = KeyValuePairListEncoding.Parse(unevaluatedPropertyValue, separator: ';') .Select(pair => pair.Name) - .Where(x => !innerConstants.Contains(x)) + .Select(constant => constant.Trim(';')) // trim any leading or trailing semicolons, because we will add our own separating semicolons + .Where(constant => !innerConstants.Contains(constant)) + .Where(constant => !string.IsNullOrEmpty(constant)) // you aren't allowed to add a semicolon as a constant .Distinct() .ToList(); diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs new file mode 100644 index 00000000000..ad2a020c024 --- /dev/null +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.VisualStudio.ProjectSystem.Debug; + +namespace Microsoft.VisualStudio.ProjectSystem.VS.Properties; + +public class KeyValuePairListEncodingTests +{ + [Theory] + [InlineData("key1=value1;key2=value2", new[] { "key1", "value1", "key2", "value2" })] + [InlineData("key1=value1;;key2=value2", new[] { "key1", "value1", "key2", "value2" })] + [InlineData("key1=value1;;;key2=value2", new[] { "key1", "value1", "key2", "value2" })] + [InlineData("key1=value1;key2=value2;key3=value3", new[] { "key1", "value1", "key2", "value2", "key3", "value3" })] + [InlineData("key1;key2=value2", new[] { "key1", "", "key2", "value2" })] + [InlineData("key1;key2;key3=value3", new[] { "key1", "", "key2", "", "key3", "value3" })] + [InlineData("key1;;;key3;;", new[] { "key1", "", "key3", "" })] + public void Parse_ValidInput_ReturnsExpectedPairs(string input, string[] expectedPairs) + { + var result = KeyValuePairListEncoding.Parse(input, ';').SelectMany(pair => new[] { pair.Name, pair.Value }).ToArray(); + Assert.Equal(expectedPairs, result); + } + + [Theory] + [InlineData(new[] { "key1", "value1", "key2", "value2" }, "key1=value1;key2=value2")] + [InlineData(new[] { "key1", "value1", "key2", "value2", "key3", "value3" }, "key1=value1;key2=value2;key3=value3")] + [InlineData(new[] { "key1", "", "key2", "value2" }, "key1;key2=value2")] + [InlineData(new[] { "key1", "", "key2", "", "key3", "value3" }, "key1;key2;key3=value3")] + public void Format_ValidPairs_ReturnsExpectedString(string[] pairs, string expectedString) + { + var nameValuePairs = ToNameValues(pairs); + var result = KeyValuePairListEncoding.Format(nameValuePairs, ";"); + Assert.Equal(expectedString, result); + return; + + static IEnumerable<(string Name, string Value)> ToNameValues(IEnumerable pairs) + { + using var e = pairs.GetEnumerator(); + while (e.MoveNext()) + { + var name = e.Current; + Assert.True(e.MoveNext()); + var value = e.Current; + yield return (name, value); + } + } + } +} From 2548986cd720088fb5b0df0a2f6d4b07ca5502d1 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Fri, 6 Dec 2024 10:59:45 -0500 Subject: [PATCH 2/7] add more test cases, parameter allowing for empty key --- .../Debug/KeyValuePairListEncoding.cs | 31 +++++++++++------- .../DefineConstantsCSharpValueProvider.cs | 14 ++++---- .../KeyValuePairListEncodingTests.cs | 32 +++++++++++++------ 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs index 8bffbaac5af..84049c49aeb 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs @@ -4,7 +4,13 @@ namespace Microsoft.VisualStudio.ProjectSystem.Debug; internal static class KeyValuePairListEncoding { - public static IEnumerable<(string Name, string Value)> Parse(string input, char separator = ',') + /// + /// Parses the input string into a collection of key-value pairs with the given separator. + /// + /// The input string to parse. + /// Indicates whether empty keys are allowed. If this is true, a pair will be returned if an empty key has a non-empty value. ie, =4 + /// The character used to separate entries in the input string. + public static IEnumerable<(string Name, string Value)> Parse(string input, bool allowsEmptyKey = false, char separator = ',') { if (string.IsNullOrWhiteSpace(input)) { @@ -13,11 +19,12 @@ internal static class KeyValuePairListEncoding foreach (var entry in ReadEntries(input, separator)) { - var (entryKey, entryValue) = SplitEntry(entry); + var (entryKey, entryValue) = SplitEntry(entry, allowsEmptyKey); var decodedEntryKey = Decode(entryKey); var decodedEntryValue = Decode(entryValue); - - if (!string.IsNullOrEmpty(decodedEntryKey)) + + if ((allowsEmptyKey && !string.IsNullOrEmpty(decodedEntryValue)) + || !string.IsNullOrEmpty(decodedEntryKey) || !string.IsNullOrEmpty(decodedEntryValue)) { yield return (decodedEntryKey, decodedEntryValue); } @@ -29,7 +36,7 @@ static IEnumerable ReadEntries(string rawText, char separator) int entryStart = 0; for (int i = 0; i < rawText.Length; i++) { - if (separator == rawText[i] && !escaped) + if (rawText[i] == separator && !escaped) { yield return rawText.Substring(entryStart, i - entryStart); entryStart = i + 1; @@ -48,12 +55,12 @@ static IEnumerable ReadEntries(string rawText, char separator) yield return rawText.Substring(entryStart); } - static (string EncodedKey, string EncodedValue) SplitEntry(string entry) + static (string EncodedKey, string EncodedValue) SplitEntry(string entry, bool allowsEmptyKey) { bool escaped = false; for (int i = 0; i < entry.Length; i++) { - if (entry[i] == '=' && !escaped) + if (entry[i] == '=' && !escaped && (allowsEmptyKey || i > 0)) { return (entry.Substring(0, i), entry.Substring(i + 1)); } @@ -76,13 +83,15 @@ static string Decode(string value) } } - public static string Format(IEnumerable<(string Name, string Value)> pairs, string separator = ",") + public static string Format(IEnumerable<(string Name, string Value)> pairs, char separator = ',') { // Copied from ActiveLaunchProfileEnvironmentVariableValueProvider in the .NET Project System. // In future, EnvironmentVariablesNameValueListEncoding should be exported from that code base and imported here. - return string.Join(separator, pairs.Select(kvp => string.IsNullOrEmpty(kvp.Value) - ? Encode(kvp.Name) - : $"{Encode(kvp.Name)}={Encode(kvp.Value)}")); + return string.Join( + separator.ToString(), + pairs.Select(kvp => string.IsNullOrEmpty(kvp.Value) + ? Encode(kvp.Name) + : $"{Encode(kvp.Name)}={Encode(kvp.Value)}")); static string Encode(string value) { diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs index 7651b0d44bc..770fc021716 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs @@ -75,15 +75,13 @@ public override async Task IsValueDefinedInContextAsync(string propertyNam // constants recursively obtained from above in this property's hierarchy (from imported files) IEnumerable innerConstants = ParseDefinedConstantsFromUnevaluatedValue(await defaultProperties.GetUnevaluatedPropertyValueAsync(ConfiguredBrowseObject.DefineConstantsProperty) ?? string.Empty); - - // we receive a comma-separated list, we should convert it to a semicolon-separated list - // because each item may have multiple values separated by semicolons - // ie "A,B;C" should be converted to "A;B;C" - unevaluatedPropertyValue = KeyValuePairListEncoding.Format( - KeyValuePairListEncoding.Parse(unevaluatedPropertyValue, separator: ','), - separator: ";"); - IEnumerable constantsToWrite = KeyValuePairListEncoding.Parse(unevaluatedPropertyValue, separator: ';') + IEnumerable constantsToWrite = KeyValuePairListEncoding + .Parse(unevaluatedPropertyValue, separator: ';') + // we receive a comma-separated list, and each item may have multiple values separated by semicolons + // so we should also parse each value using ; as the separator + // ie "A,B;C" -> [A, B;C] -> [A, B, C] + .SelectMany(pair => KeyValuePairListEncoding.Parse(pair.Name, separator: ';')) .Select(pair => pair.Name) .Select(constant => constant.Trim(';')) // trim any leading or trailing semicolons, because we will add our own separating semicolons .Where(constant => !innerConstants.Contains(constant)) diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs index ad2a020c024..b9c49436068 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs @@ -7,16 +7,27 @@ namespace Microsoft.VisualStudio.ProjectSystem.VS.Properties; public class KeyValuePairListEncodingTests { [Theory] - [InlineData("key1=value1;key2=value2", new[] { "key1", "value1", "key2", "value2" })] - [InlineData("key1=value1;;key2=value2", new[] { "key1", "value1", "key2", "value2" })] - [InlineData("key1=value1;;;key2=value2", new[] { "key1", "value1", "key2", "value2" })] - [InlineData("key1=value1;key2=value2;key3=value3", new[] { "key1", "value1", "key2", "value2", "key3", "value3" })] - [InlineData("key1;key2=value2", new[] { "key1", "", "key2", "value2" })] - [InlineData("key1;key2;key3=value3", new[] { "key1", "", "key2", "", "key3", "value3" })] - [InlineData("key1;;;key3;;", new[] { "key1", "", "key3", "" })] - public void Parse_ValidInput_ReturnsExpectedPairs(string input, string[] expectedPairs) + [InlineData("key1=value1;key2=value2", true, new[] { "key1", "value1", "key2", "value2" })] + [InlineData("key1=value1;;key2=value2", true, new[] { "key1", "value1", "key2", "value2" })] + [InlineData("key1=value1;;;key2=value2", true, new[] { "key1", "value1", "key2", "value2" })] + [InlineData("key1=value1;key2=value2;key3=value3", true, new[] { "key1", "value1", "key2", "value2", "key3", "value3" })] + [InlineData("key1;key2=value2", true, new[] { "key1", "", "key2", "value2" })] + [InlineData("key1;key2;key3=value3", true, new[] { "key1", "", "key2", "", "key3", "value3" })] + [InlineData("key1;;;key3;;", true, new[] { "key1", "", "key3", "" })] + [InlineData("", true, new string[0])] + [InlineData(" ", true, new string[0])] + [InlineData("=", true, new string[0])] + [InlineData("", false, new string[0])] + [InlineData(" ", false, new string[0])] + [InlineData("=", false, new[] { "=", "" })] // = can count as part of the key here + [InlineData("key1=value1;=value2=", true, new[] { "key1", "value1", "", "value2=" })] + [InlineData("key1=value1;=value2=", false, new[] { "key1", "value1", "=value2", "" })] + [InlineData("key1=value1;=value2", false, new[] { "key1", "value1", "=value2", "" })] + [InlineData("==", true, new[] { "", "=" })] + [InlineData(";", true, new string[0])] + public void Parse_ValidInput_ReturnsExpectedPairs(string input, bool allowsEmptyKey, string[] expectedPairs) { - var result = KeyValuePairListEncoding.Parse(input, ';').SelectMany(pair => new[] { pair.Name, pair.Value }).ToArray(); + var result = KeyValuePairListEncoding.Parse(input, allowsEmptyKey, ';').SelectMany(pair => new[] { pair.Name, pair.Value }).ToArray(); Assert.Equal(expectedPairs, result); } @@ -25,10 +36,11 @@ public void Parse_ValidInput_ReturnsExpectedPairs(string input, string[] expecte [InlineData(new[] { "key1", "value1", "key2", "value2", "key3", "value3" }, "key1=value1;key2=value2;key3=value3")] [InlineData(new[] { "key1", "", "key2", "value2" }, "key1;key2=value2")] [InlineData(new[] { "key1", "", "key2", "", "key3", "value3" }, "key1;key2;key3=value3")] + [InlineData(new string[0], "")] public void Format_ValidPairs_ReturnsExpectedString(string[] pairs, string expectedString) { var nameValuePairs = ToNameValues(pairs); - var result = KeyValuePairListEncoding.Format(nameValuePairs, ";"); + var result = KeyValuePairListEncoding.Format(nameValuePairs, ';'); Assert.Equal(expectedString, result); return; From d955d17a2310f3aeb97ded6ff99308721218f769 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Sat, 7 Dec 2024 00:59:08 -0500 Subject: [PATCH 3/7] support notifying the property page control if a constant was removed in the property value directly --- .../DefineConstantsCSharpValueProvider.cs | 59 ++++++++++------ ...DefineConstantsCSharpValueProviderTests.cs | 69 +++++++++++++++++++ 2 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/CSharp/DefineConstantsCSharpValueProviderTests.cs diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs index 770fc021716..925fe399342 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs @@ -7,25 +7,28 @@ namespace Microsoft.VisualStudio.ProjectSystem.Properties; [ExportInterceptingPropertyValueProvider(ConfiguredBrowseObject.DefineConstantsProperty, ExportInterceptingPropertyValueProviderFile.ProjectFile)] [AppliesTo(ProjectCapability.CSharpOrFSharp)] -internal class DefineConstantsValueProvider : InterceptingPropertyValueProviderBase +internal class DefineConstantsCSharpValueProvider : InterceptingPropertyValueProviderBase { + private const string DefineConstantsRecursivePrefix = "$(DefineConstants)"; + private readonly IProjectAccessor _projectAccessor; private readonly ConfiguredProject _project; - - internal const string DefineConstantsRecursivePrefix = "$(DefineConstants)"; - + private HashSet _removedValues = []; + [ImportingConstructor] - public DefineConstantsValueProvider(IProjectAccessor projectAccessor, ConfiguredProject project) + public DefineConstantsCSharpValueProvider(IProjectAccessor projectAccessor, ConfiguredProject project) { _projectAccessor = projectAccessor; _project = project; } - internal static IEnumerable ParseDefinedConstantsFromUnevaluatedValue(string unevaluatedValue) + private static IEnumerable ParseDefinedConstantsFromUnevaluatedValue(string unevaluatedValue) { - return unevaluatedValue.Length <= DefineConstantsRecursivePrefix.Length || !unevaluatedValue.StartsWith(DefineConstantsRecursivePrefix) - ? Array.Empty() - : unevaluatedValue.Substring(DefineConstantsRecursivePrefix.Length).Split(';').Where(x => x.Length > 0); + string substring = unevaluatedValue.Length <= DefineConstantsRecursivePrefix.Length || !unevaluatedValue.StartsWith(DefineConstantsRecursivePrefix) + ? unevaluatedValue + : unevaluatedValue.Substring(DefineConstantsRecursivePrefix.Length); + + return substring.Split(';').Where(x => x.Length > 0); } public override async Task OnGetUnevaluatedPropertyValueAsync(string propertyName, string unevaluatedPropertyValue, IProjectProperties defaultProperties) @@ -38,10 +41,12 @@ public override async Task OnGetUnevaluatedPropertyValueAsync(string pro return string.Empty; } - return KeyValuePairListEncoding.Format( - ParseDefinedConstantsFromUnevaluatedValue(unevaluatedDefineConstantsValue) - .Select(symbol => (symbol, bool.FalseString)) - ); + var pairs = KeyValuePairListEncoding.Parse(unevaluatedDefineConstantsValue, separator: ';').Select(pair => pair.Name) + .Where(symbol => !string.IsNullOrEmpty(symbol)) + .Select(symbol => (symbol, _removedValues.Contains(symbol) ? "null" : bool.FalseString)).ToList(); + pairs.AddRange(_removedValues.Select(value => (value, "null"))); + + return KeyValuePairListEncoding.Format(pairs, separator: ','); } // We cannot rely on the unevaluated property value as obtained through Project.GetProperty.UnevaluatedValue - the reason is that for a recursively-defined @@ -51,7 +56,11 @@ public override async Task OnGetUnevaluatedPropertyValueAsync(string pro // 2. to override IsValueDefinedInContextAsync, as this will always return false private async Task GetUnevaluatedDefineConstantsPropertyValueAsync() { - await ((ConfiguredProject2)_project).EnsureProjectEvaluatedAsync(); + if (_project is ConfiguredProject2 configuredProject2) + { + await configuredProject2.EnsureProjectEvaluatedAsync(); + } + return await _projectAccessor.OpenProjectForReadAsync(_project, project => { project.ReevaluateIfNecessary(); @@ -75,26 +84,34 @@ public override async Task IsValueDefinedInContextAsync(string propertyNam // constants recursively obtained from above in this property's hierarchy (from imported files) IEnumerable innerConstants = ParseDefinedConstantsFromUnevaluatedValue(await defaultProperties.GetUnevaluatedPropertyValueAsync(ConfiguredBrowseObject.DefineConstantsProperty) ?? string.Empty); + + var separatedPairs = KeyValuePairListEncoding.Parse(unevaluatedPropertyValue, separator: ',').ToList(); - IEnumerable constantsToWrite = KeyValuePairListEncoding - .Parse(unevaluatedPropertyValue, separator: ';') + var foundConstants = separatedPairs // we receive a comma-separated list, and each item may have multiple values separated by semicolons // so we should also parse each value using ; as the separator // ie "A,B;C" -> [A, B;C] -> [A, B, C] - .SelectMany(pair => KeyValuePairListEncoding.Parse(pair.Name, separator: ';')) + .SelectMany(pair => KeyValuePairListEncoding.Parse(pair.Name, allowsEmptyKey: true, separator: ';')) .Select(pair => pair.Name) + .Where(pair => !string.IsNullOrEmpty(pair)) .Select(constant => constant.Trim(';')) // trim any leading or trailing semicolons, because we will add our own separating semicolons - .Where(constant => !innerConstants.Contains(constant)) .Where(constant => !string.IsNullOrEmpty(constant)) // you aren't allowed to add a semicolon as a constant .Distinct() .ToList(); - if (!constantsToWrite.Any()) + // if a value included multiple constants (such as A;B), we should remove the value in the UI (since it's been replaced) + _removedValues = separatedPairs + .Select(pair => pair.Name) + .Where(name => !foundConstants.Contains(name) && !string.IsNullOrWhiteSpace(name.Replace(";", ""))) + .ToHashSet(); + + var writeableConstants = foundConstants.Where(constant => !innerConstants.Contains(constant)).ToList(); + if (writeableConstants.Count == 0) { await defaultProperties.DeletePropertyAsync(propertyName, dimensionalConditions); return null; } - - return $"{DefineConstantsRecursivePrefix};" + string.Join(";", constantsToWrite); + + return string.Join(";", writeableConstants); } } diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/CSharp/DefineConstantsCSharpValueProviderTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/CSharp/DefineConstantsCSharpValueProviderTests.cs new file mode 100644 index 00000000000..e864fb8ef3c --- /dev/null +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/CSharp/DefineConstantsCSharpValueProviderTests.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information. + +using Microsoft.Build.Construction; +using Microsoft.VisualStudio.ProjectSystem.Properties; + +namespace Microsoft.VisualStudio.ProjectSystem.VS.Properties; + +public class DefineConstantsCSharpValueProviderTests +{ + private const string PropertyName = "DefineConstants"; + + [Theory] + [InlineData("DEBUG;TRACE", "DEBUG=False,TRACE=False")] + [InlineData("", "")] + public async Task GetExistingUnevaluatedValue(string? defineConstantsValue, string expectedFormattedValue) + { + var provider = CreateInstance(defineConstantsValue, out _, out _); + + var actualPropertyValue = await provider.OnGetUnevaluatedPropertyValueAsync(string.Empty, string.Empty, null!); + Assert.Equal(expectedFormattedValue, actualPropertyValue); + } + + [Theory] + [InlineData("DEBUG,TRACE", null, "DEBUG;TRACE", "DEBUG=False,TRACE=False")] + [InlineData("$(DefineConstants),DEBUG,TRACE", "PROP1;PROP2", "$(DefineConstants);DEBUG;TRACE", "$(DefineConstants)=False,DEBUG=False,TRACE=False")] + [InlineData("Constant0,$(DefineConstants),Constant1;;Constant2;Constant3;,Constant4", "Constant4", "Constant0;$(DefineConstants);Constant1;Constant2;Constant3", "Constant0=False,$(DefineConstants)=False,Constant1=False,Constant2=False,Constant3=False,Constant1;;Constant2;Constant3;=null")] + public async Task SetUnevaluatedValue(string unevaluatedValueToSet, string? defineConstantsValue, string? expectedSetUnevaluatedValue, string expectedFormattedValue) + { + var provider = CreateInstance(null, out var projectAccessor, out var project); + Mock mockProjectProperties = new Mock(); + mockProjectProperties + .Setup(p => p.GetUnevaluatedPropertyValueAsync(ConfiguredBrowseObject.DefineConstantsProperty)) + .ReturnsAsync(defineConstantsValue); + + var setPropertyValue = await provider.OnSetPropertyValueAsync(PropertyName, unevaluatedValueToSet, mockProjectProperties.Object); + Assert.Equal(expectedSetUnevaluatedValue, setPropertyValue); + + await SetDefineConstantsPropertyAsync(projectAccessor, project, setPropertyValue); + + var actualPropertyFormattedValue = await provider.OnGetUnevaluatedPropertyValueAsync(string.Empty, string.Empty, null!); + Assert.Equal(expectedFormattedValue, actualPropertyFormattedValue); + } + + private static DefineConstantsCSharpValueProvider CreateInstance(string? defineConstantsValue, out IProjectAccessor projectAccessor, out ConfiguredProject project) + { + var projectXml = defineConstantsValue is not null + ? $""" + + + <{ConfiguredBrowseObject.DefineConstantsProperty}>{defineConstantsValue} + + + """ + : ""; + + projectAccessor = IProjectAccessorFactory.Create(ProjectRootElementFactory.Create(projectXml)); + project = ConfiguredProjectFactory.Create(); + + return new DefineConstantsCSharpValueProvider(projectAccessor, project); + } + + private static async Task SetDefineConstantsPropertyAsync(IProjectAccessor projectAccessor, ConfiguredProject project, string? setPropertyValue) + { + await projectAccessor.OpenProjectXmlForWriteAsync(project.UnconfiguredProject, projectXml => + { + projectXml.AddProperty(PropertyName, setPropertyValue); + }); + } +} From f96abbe6945f31c0d63cc6771c6641f9d8863b05 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Thu, 12 Dec 2024 12:11:54 -0500 Subject: [PATCH 4/7] Remove state, rename --- ...DefineConstantsCAndFSharpValueProvider.cs} | 42 ++++--------------- .../PropertyPages/BuildPropertyPage.xaml | 1 + .../Mocks/ConfiguredProjectFactory.cs | 5 ++- ...eConstantsCAndFSharpValueProviderTests.cs} | 7 ++-- 4 files changed, 16 insertions(+), 39 deletions(-) rename src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/{DefineConstantsCSharpValueProvider.cs => DefineConstantsCAndFSharpValueProvider.cs} (70%) rename tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/CSharp/{DefineConstantsCSharpValueProviderTests.cs => DefineConstantsCAndFSharpValueProviderTests.cs} (84%) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCAndFSharpValueProvider.cs similarity index 70% rename from src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs rename to src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCAndFSharpValueProvider.cs index 925fe399342..c5d8cb93864 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Properties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCAndFSharpValueProvider.cs @@ -7,21 +7,11 @@ namespace Microsoft.VisualStudio.ProjectSystem.Properties; [ExportInterceptingPropertyValueProvider(ConfiguredBrowseObject.DefineConstantsProperty, ExportInterceptingPropertyValueProviderFile.ProjectFile)] [AppliesTo(ProjectCapability.CSharpOrFSharp)] -internal class DefineConstantsCSharpValueProvider : InterceptingPropertyValueProviderBase +[method: ImportingConstructor] +internal class DefineConstantsCAndFSharpValueProvider(IProjectAccessor projectAccessor, ConfiguredProject project) : InterceptingPropertyValueProviderBase { private const string DefineConstantsRecursivePrefix = "$(DefineConstants)"; - private readonly IProjectAccessor _projectAccessor; - private readonly ConfiguredProject _project; - private HashSet _removedValues = []; - - [ImportingConstructor] - public DefineConstantsCSharpValueProvider(IProjectAccessor projectAccessor, ConfiguredProject project) - { - _projectAccessor = projectAccessor; - _project = project; - } - private static IEnumerable ParseDefinedConstantsFromUnevaluatedValue(string unevaluatedValue) { string substring = unevaluatedValue.Length <= DefineConstantsRecursivePrefix.Length || !unevaluatedValue.StartsWith(DefineConstantsRecursivePrefix) @@ -43,8 +33,7 @@ public override async Task OnGetUnevaluatedPropertyValueAsync(string pro var pairs = KeyValuePairListEncoding.Parse(unevaluatedDefineConstantsValue, separator: ';').Select(pair => pair.Name) .Where(symbol => !string.IsNullOrEmpty(symbol)) - .Select(symbol => (symbol, _removedValues.Contains(symbol) ? "null" : bool.FalseString)).ToList(); - pairs.AddRange(_removedValues.Select(value => (value, "null"))); + .Select(symbol => (symbol, bool.FalseString)).ToList(); return KeyValuePairListEncoding.Format(pairs, separator: ','); } @@ -56,15 +45,12 @@ public override async Task OnGetUnevaluatedPropertyValueAsync(string pro // 2. to override IsValueDefinedInContextAsync, as this will always return false private async Task GetUnevaluatedDefineConstantsPropertyValueAsync() { - if (_project is ConfiguredProject2 configuredProject2) - { - await configuredProject2.EnsureProjectEvaluatedAsync(); - } + await ((ConfiguredProject2)project).EnsureProjectEvaluatedAsync(); - return await _projectAccessor.OpenProjectForReadAsync(_project, project => + return await projectAccessor.OpenProjectForReadAsync(project, msbuildProject => { - project.ReevaluateIfNecessary(); - ProjectProperty defineConstantsProperty = project.GetProperty(ConfiguredBrowseObject.DefineConstantsProperty); + msbuildProject.ReevaluateIfNecessary(); + ProjectProperty defineConstantsProperty = msbuildProject.GetProperty(ConfiguredBrowseObject.DefineConstantsProperty); while (defineConstantsProperty.IsImported && defineConstantsProperty.Predecessor is not null) { defineConstantsProperty = defineConstantsProperty.Predecessor; @@ -85,13 +71,7 @@ public override async Task IsValueDefinedInContextAsync(string propertyNam IEnumerable innerConstants = ParseDefinedConstantsFromUnevaluatedValue(await defaultProperties.GetUnevaluatedPropertyValueAsync(ConfiguredBrowseObject.DefineConstantsProperty) ?? string.Empty); - var separatedPairs = KeyValuePairListEncoding.Parse(unevaluatedPropertyValue, separator: ',').ToList(); - - var foundConstants = separatedPairs - // we receive a comma-separated list, and each item may have multiple values separated by semicolons - // so we should also parse each value using ; as the separator - // ie "A,B;C" -> [A, B;C] -> [A, B, C] - .SelectMany(pair => KeyValuePairListEncoding.Parse(pair.Name, allowsEmptyKey: true, separator: ';')) + var foundConstants = KeyValuePairListEncoding.Parse(unevaluatedPropertyValue, separator: ',') .Select(pair => pair.Name) .Where(pair => !string.IsNullOrEmpty(pair)) .Select(constant => constant.Trim(';')) // trim any leading or trailing semicolons, because we will add our own separating semicolons @@ -99,12 +79,6 @@ public override async Task IsValueDefinedInContextAsync(string propertyNam .Distinct() .ToList(); - // if a value included multiple constants (such as A;B), we should remove the value in the UI (since it's been replaced) - _removedValues = separatedPairs - .Select(pair => pair.Name) - .Where(name => !foundConstants.Contains(name) && !string.IsNullOrWhiteSpace(name.Replace(";", ""))) - .ToHashSet(); - var writeableConstants = foundConstants.Where(constant => !innerConstants.Contains(constant)).ToList(); if (writeableConstants.Count == 0) { diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/PropertyPages/BuildPropertyPage.xaml b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/PropertyPages/BuildPropertyPage.xaml index d3d39a736f6..b4bee117d44 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/PropertyPages/BuildPropertyPage.xaml +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Rules/PropertyPages/BuildPropertyPage.xaml @@ -61,6 +61,7 @@ + diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs index 02116643cef..58656165995 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs @@ -6,10 +6,11 @@ internal static class ConfiguredProjectFactory { public static ConfiguredProject Create(IProjectCapabilitiesScope? capabilities = null, ProjectConfiguration? projectConfiguration = null, ConfiguredProjectServices? services = null, UnconfiguredProject? unconfiguredProject = null) { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(c => c.Capabilities).Returns(capabilities!); mock.Setup(c => c.ProjectConfiguration).Returns(projectConfiguration!); mock.Setup(c => c.Services).Returns(services!); + mock.Setup(c => c.EnsureProjectEvaluatedAsync()).Returns(Task.CompletedTask); mock.SetupGet(c => c.UnconfiguredProject).Returns(unconfiguredProject ?? UnconfiguredProjectFactory.Create()); return mock.Object; } @@ -33,4 +34,6 @@ public static ConfiguredProject ImplementUnconfiguredProject(UnconfiguredProject return mock.Object; } } + + internal interface ITestConfiguredProjectImpl : ConfiguredProject, ConfiguredProject2; } diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/CSharp/DefineConstantsCSharpValueProviderTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/CSharp/DefineConstantsCAndFSharpValueProviderTests.cs similarity index 84% rename from tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/CSharp/DefineConstantsCSharpValueProviderTests.cs rename to tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/CSharp/DefineConstantsCAndFSharpValueProviderTests.cs index e864fb8ef3c..445dd4f2156 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/CSharp/DefineConstantsCSharpValueProviderTests.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/CSharp/DefineConstantsCAndFSharpValueProviderTests.cs @@ -5,7 +5,7 @@ namespace Microsoft.VisualStudio.ProjectSystem.VS.Properties; -public class DefineConstantsCSharpValueProviderTests +public class DefineConstantsCAndFSharpValueProviderTests { private const string PropertyName = "DefineConstants"; @@ -23,7 +23,6 @@ public async Task GetExistingUnevaluatedValue(string? defineConstantsValue, stri [Theory] [InlineData("DEBUG,TRACE", null, "DEBUG;TRACE", "DEBUG=False,TRACE=False")] [InlineData("$(DefineConstants),DEBUG,TRACE", "PROP1;PROP2", "$(DefineConstants);DEBUG;TRACE", "$(DefineConstants)=False,DEBUG=False,TRACE=False")] - [InlineData("Constant0,$(DefineConstants),Constant1;;Constant2;Constant3;,Constant4", "Constant4", "Constant0;$(DefineConstants);Constant1;Constant2;Constant3", "Constant0=False,$(DefineConstants)=False,Constant1=False,Constant2=False,Constant3=False,Constant1;;Constant2;Constant3;=null")] public async Task SetUnevaluatedValue(string unevaluatedValueToSet, string? defineConstantsValue, string? expectedSetUnevaluatedValue, string expectedFormattedValue) { var provider = CreateInstance(null, out var projectAccessor, out var project); @@ -41,7 +40,7 @@ public async Task SetUnevaluatedValue(string unevaluatedValueToSet, string? defi Assert.Equal(expectedFormattedValue, actualPropertyFormattedValue); } - private static DefineConstantsCSharpValueProvider CreateInstance(string? defineConstantsValue, out IProjectAccessor projectAccessor, out ConfiguredProject project) + private static DefineConstantsCAndFSharpValueProvider CreateInstance(string? defineConstantsValue, out IProjectAccessor projectAccessor, out ConfiguredProject project) { var projectXml = defineConstantsValue is not null ? $""" @@ -56,7 +55,7 @@ private static DefineConstantsCSharpValueProvider CreateInstance(string? defineC projectAccessor = IProjectAccessorFactory.Create(ProjectRootElementFactory.Create(projectXml)); project = ConfiguredProjectFactory.Create(); - return new DefineConstantsCSharpValueProvider(projectAccessor, project); + return new DefineConstantsCAndFSharpValueProvider(projectAccessor, project); } private static async Task SetDefineConstantsPropertyAsync(IProjectAccessor projectAccessor, ConfiguredProject project, string? setPropertyValue) From 2330881316ff23dccb49488d61212e8f62b018c5 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Thu, 12 Dec 2024 12:18:12 -0500 Subject: [PATCH 5/7] update mock type --- .../Mocks/ConfiguredProjectFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs index 8873157885b..89af090ab4f 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs @@ -6,7 +6,7 @@ internal static class ConfiguredProjectFactory { public static ConfiguredProject Create(IProjectCapabilitiesScope? capabilities = null, ProjectConfiguration? projectConfiguration = null, ConfiguredProjectServices? services = null, UnconfiguredProject? unconfiguredProject = null) { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(c => c.Capabilities).Returns(capabilities!); mock.Setup(c => c.ProjectConfiguration).Returns(projectConfiguration!); mock.Setup(c => c.Services).Returns(services!); From 259125b725ad7d19c437c76d252dd6398bf11fa0 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Fri, 27 Dec 2024 10:39:25 -0500 Subject: [PATCH 6/7] Re-add = escaping --- .../Debug/KeyValuePairListEncoding.cs | 13 +++--- .../KeyValuePairListEncodingTests.cs | 40 +++++++++---------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs index 84049c49aeb..746462c8d53 100644 --- a/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs +++ b/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs @@ -10,7 +10,7 @@ internal static class KeyValuePairListEncoding /// The input string to parse. /// Indicates whether empty keys are allowed. If this is true, a pair will be returned if an empty key has a non-empty value. ie, =4 /// The character used to separate entries in the input string. - public static IEnumerable<(string Name, string Value)> Parse(string input, bool allowsEmptyKey = false, char separator = ',') + public static IEnumerable<(string Name, string Value)> Parse(string input, char separator = ',') { if (string.IsNullOrWhiteSpace(input)) { @@ -19,12 +19,11 @@ internal static class KeyValuePairListEncoding foreach (var entry in ReadEntries(input, separator)) { - var (entryKey, entryValue) = SplitEntry(entry, allowsEmptyKey); + var (entryKey, entryValue) = SplitEntry(entry); var decodedEntryKey = Decode(entryKey); var decodedEntryValue = Decode(entryValue); - if ((allowsEmptyKey && !string.IsNullOrEmpty(decodedEntryValue)) - || !string.IsNullOrEmpty(decodedEntryKey) || !string.IsNullOrEmpty(decodedEntryValue)) + if (!string.IsNullOrEmpty(decodedEntryKey)) { yield return (decodedEntryKey, decodedEntryValue); } @@ -55,12 +54,12 @@ static IEnumerable ReadEntries(string rawText, char separator) yield return rawText.Substring(entryStart); } - static (string EncodedKey, string EncodedValue) SplitEntry(string entry, bool allowsEmptyKey) + static (string EncodedKey, string EncodedValue) SplitEntry(string entry) { bool escaped = false; for (int i = 0; i < entry.Length; i++) { - if (entry[i] == '=' && !escaped && (allowsEmptyKey || i > 0)) + if (entry[i] == '=' && !escaped) { return (entry.Substring(0, i), entry.Substring(i + 1)); } @@ -85,8 +84,6 @@ static string Decode(string value) public static string Format(IEnumerable<(string Name, string Value)> pairs, char separator = ',') { - // Copied from ActiveLaunchProfileEnvironmentVariableValueProvider in the .NET Project System. - // In future, EnvironmentVariablesNameValueListEncoding should be exported from that code base and imported here. return string.Join( separator.ToString(), pairs.Select(kvp => string.IsNullOrEmpty(kvp.Value) diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs index b9c49436068..6f766269f2d 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs @@ -7,27 +7,26 @@ namespace Microsoft.VisualStudio.ProjectSystem.VS.Properties; public class KeyValuePairListEncodingTests { [Theory] - [InlineData("key1=value1;key2=value2", true, new[] { "key1", "value1", "key2", "value2" })] - [InlineData("key1=value1;;key2=value2", true, new[] { "key1", "value1", "key2", "value2" })] - [InlineData("key1=value1;;;key2=value2", true, new[] { "key1", "value1", "key2", "value2" })] - [InlineData("key1=value1;key2=value2;key3=value3", true, new[] { "key1", "value1", "key2", "value2", "key3", "value3" })] - [InlineData("key1;key2=value2", true, new[] { "key1", "", "key2", "value2" })] - [InlineData("key1;key2;key3=value3", true, new[] { "key1", "", "key2", "", "key3", "value3" })] - [InlineData("key1;;;key3;;", true, new[] { "key1", "", "key3", "" })] - [InlineData("", true, new string[0])] - [InlineData(" ", true, new string[0])] - [InlineData("=", true, new string[0])] - [InlineData("", false, new string[0])] - [InlineData(" ", false, new string[0])] - [InlineData("=", false, new[] { "=", "" })] // = can count as part of the key here - [InlineData("key1=value1;=value2=", true, new[] { "key1", "value1", "", "value2=" })] - [InlineData("key1=value1;=value2=", false, new[] { "key1", "value1", "=value2", "" })] - [InlineData("key1=value1;=value2", false, new[] { "key1", "value1", "=value2", "" })] - [InlineData("==", true, new[] { "", "=" })] - [InlineData(";", true, new string[0])] - public void Parse_ValidInput_ReturnsExpectedPairs(string input, bool allowsEmptyKey, string[] expectedPairs) + [InlineData("key1=value1;key2=value2", new[] { "key1", "value1", "key2", "value2" })] + [InlineData("key1=value1;;key2=value2", new[] { "key1", "value1", "key2", "value2" })] + [InlineData("key1=value1;;;key2=value2", new[] { "key1", "value1", "key2", "value2" })] + [InlineData("key1=value1;key2=value2;key3=value3", new[] { "key1", "value1", "key2", "value2", "key3", "value3" })] + [InlineData("key1;key2=value2", new[] { "key1", "", "key2", "value2" })] + [InlineData("key1;key2;key3=value3", new[] { "key1", "", "key2", "", "key3", "value3" })] + [InlineData("key1;;;key3;;", new[] { "key1", "", "key3", "" })] + [InlineData("", new string[0])] + [InlineData(" ", new string[0])] + [InlineData("=", new string[0])] + [InlineData("/=", new[] { "=", "" })] + [InlineData("key1=value1;/=value2=", new[] { "key1", "value1", "=value2", "" })] + [InlineData("key1=value1;=value2", new[] { "key1", "value1" })] + [InlineData("==", new string[0])] + [InlineData("=/=", new string[0])] + [InlineData("/==", new[] { "=", "" })] + [InlineData(";", new string[0])] + public void Parse_ValidInput_ReturnsExpectedPairs(string input, string[] expectedPairs) { - var result = KeyValuePairListEncoding.Parse(input, allowsEmptyKey, ';').SelectMany(pair => new[] { pair.Name, pair.Value }).ToArray(); + var result = KeyValuePairListEncoding.Parse(input, ';').SelectMany(pair => new[] { pair.Name, pair.Value }).ToArray(); Assert.Equal(expectedPairs, result); } @@ -35,6 +34,7 @@ public void Parse_ValidInput_ReturnsExpectedPairs(string input, bool allowsEmpty [InlineData(new[] { "key1", "value1", "key2", "value2" }, "key1=value1;key2=value2")] [InlineData(new[] { "key1", "value1", "key2", "value2", "key3", "value3" }, "key1=value1;key2=value2;key3=value3")] [InlineData(new[] { "key1", "", "key2", "value2" }, "key1;key2=value2")] + [InlineData(new[] { "key1=", "", "key2=", "value2=" }, "key1=;key2/==value2/=")] [InlineData(new[] { "key1", "", "key2", "", "key3", "value3" }, "key1;key2;key3=value3")] [InlineData(new string[0], "")] public void Format_ValidPairs_ReturnsExpectedString(string[] pairs, string expectedString) From 5588830f19885543393e9d80fd809943cac4da61 Mon Sep 17 00:00:00 2001 From: Adam Ratzman Date: Fri, 27 Dec 2024 10:45:07 -0500 Subject: [PATCH 7/7] fix test expectation, use moq feature --- .../Mocks/ConfiguredProjectFactory.cs | 9 +++++---- .../VS/Properties/KeyValuePairListEncodingTests.cs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs index 89af090ab4f..6289aa4d780 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs @@ -6,12 +6,15 @@ internal static class ConfiguredProjectFactory { public static ConfiguredProject Create(IProjectCapabilitiesScope? capabilities = null, ProjectConfiguration? projectConfiguration = null, ConfiguredProjectServices? services = null, UnconfiguredProject? unconfiguredProject = null) { - var mock = new Mock(); + var mock2 = new Mock(); + mock2.Setup(c => c.EnsureProjectEvaluatedAsync()).Returns(Task.CompletedTask); + + var mock = mock2.As(); mock.Setup(c => c.Capabilities).Returns(capabilities!); mock.Setup(c => c.ProjectConfiguration).Returns(projectConfiguration!); mock.Setup(c => c.Services).Returns(services!); - mock.Setup(c => c.EnsureProjectEvaluatedAsync()).Returns(Task.CompletedTask); mock.SetupGet(c => c.UnconfiguredProject).Returns(unconfiguredProject ?? UnconfiguredProjectFactory.Create()); + return mock.Object; } @@ -33,6 +36,4 @@ public static ConfiguredProject ImplementUnconfiguredProject(UnconfiguredProject return mock.Object; } - - internal interface ITestConfiguredProjectImpl : ConfiguredProject, ConfiguredProject2; } diff --git a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs index 6f766269f2d..92b3fade014 100644 --- a/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs +++ b/tests/Microsoft.VisualStudio.ProjectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs @@ -34,7 +34,7 @@ public void Parse_ValidInput_ReturnsExpectedPairs(string input, string[] expecte [InlineData(new[] { "key1", "value1", "key2", "value2" }, "key1=value1;key2=value2")] [InlineData(new[] { "key1", "value1", "key2", "value2", "key3", "value3" }, "key1=value1;key2=value2;key3=value3")] [InlineData(new[] { "key1", "", "key2", "value2" }, "key1;key2=value2")] - [InlineData(new[] { "key1=", "", "key2=", "value2=" }, "key1=;key2/==value2/=")] + [InlineData(new[] { "key1=", "", "key2=", "value2=" }, "key1/=;key2/==value2/=")] [InlineData(new[] { "key1", "", "key2", "", "key3", "value3" }, "key1;key2;key3=value3")] [InlineData(new string[0], "")] public void Format_ValidPairs_ReturnsExpectedString(string[] pairs, string expectedString)