From ad5a6fcf6e3703e0e4fb023ca5c325ad669cef04 Mon Sep 17 00:00:00 2001 From: Jan Krivanek Date: Fri, 15 Jul 2022 18:06:45 +0200 Subject: [PATCH] Fix Cpp2 evaluator for multichoice parameters --- .../Expressions/Cpp/Scope.cs | 26 +--- .../Expressions/ScopeBuilder.cs | 2 +- .../Shared/SharedEvaluatorDefinition.cs | 14 +- .../Expressions/Token.cs | 4 +- .../PublicAPI.Shipped.txt | 2 - .../PublicAPI.Unshipped.txt | 2 + .../MultiValueParameter.cs | 76 +++++++++ .../PublicAPI.Unshipped.txt | 4 + .../MacroTests/EvaluateMacroTests.cs | 30 ++++ .../RunnableProjectGeneratorTests.cs | 147 ++++++++++++++++++ 10 files changed, 277 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.TemplateEngine.Core/Expressions/Cpp/Scope.cs b/src/Microsoft.TemplateEngine.Core/Expressions/Cpp/Scope.cs index 86ae3ed2a7..f4dc22e2cb 100644 --- a/src/Microsoft.TemplateEngine.Core/Expressions/Cpp/Scope.cs +++ b/src/Microsoft.TemplateEngine.Core/Expressions/Cpp/Scope.cs @@ -112,34 +112,12 @@ private static bool LenientEquals(object x, object y) return string.Equals(sx, sy, StringComparison.OrdinalIgnoreCase); } + if (MultiValueParameter.TryPerformMultiValueEqual(x, y, out bool result)) { - if (x is MultiValueParameter mv && y is string sv) - { - return MultivalueEquals(mv, sv); - } - } - - { - if (y is MultiValueParameter mv && x is string sv) - { - return MultivalueEquals(mv, sv); - } + return result; } return Equals(x, y); } - - private static bool MultivalueEquals(MultiValueParameter mv, string comparand) - { - foreach (string s in mv.Values) - { - if (string.Equals(s, comparand, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } } } diff --git a/src/Microsoft.TemplateEngine.Core/Expressions/ScopeBuilder.cs b/src/Microsoft.TemplateEngine.Core/Expressions/ScopeBuilder.cs index 821cbe059f..ac859e4256 100644 --- a/src/Microsoft.TemplateEngine.Core/Expressions/ScopeBuilder.cs +++ b/src/Microsoft.TemplateEngine.Core/Expressions/ScopeBuilder.cs @@ -220,7 +220,7 @@ public IEvaluable Build(ref int bufferLength, ref int bufferPosition, Action t = new Token(_literal, value); TokenScope scope = new TokenScope(isolator.Active, t); diff --git a/src/Microsoft.TemplateEngine.Core/Expressions/Shared/SharedEvaluatorDefinition.cs b/src/Microsoft.TemplateEngine.Core/Expressions/Shared/SharedEvaluatorDefinition.cs index 83b508f48e..9beada9aed 100644 --- a/src/Microsoft.TemplateEngine.Core/Expressions/Shared/SharedEvaluatorDefinition.cs +++ b/src/Microsoft.TemplateEngine.Core/Expressions/Shared/SharedEvaluatorDefinition.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Microsoft.TemplateEngine.Core.Contracts; using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.Utils; namespace Microsoft.TemplateEngine.Core.Expressions.Shared { @@ -79,6 +80,7 @@ protected static int Compare(object left, object right) return AttemptNumericComparison(left, right) ?? AttemptBooleanComparison(left, right) ?? AttemptVersionComparison(left, right) + ?? AttemptMultiValueComparison(left, right) ?? AttemptLexographicComparison(left, right) ?? AttemptComparableComparison(left, right) ?? 0; @@ -114,7 +116,17 @@ protected static int Compare(object left, object right) return ls.CompareTo(rs); } - private static int? AttemptLexographicComparison(object left, object right) + private static int? AttemptMultiValueComparison(object left, object right) + { + if (MultiValueParameter.TryPerformMultiValueEqual(left, right, out bool result)) + { + return result ? 0 : -1; + } + + return null; + } + + private static int? AttemptLexographicComparison(object left, object right) { string ls = left as string; string rs = right as string; diff --git a/src/Microsoft.TemplateEngine.Core/Expressions/Token.cs b/src/Microsoft.TemplateEngine.Core/Expressions/Token.cs index faab96dca0..a87b286b4c 100644 --- a/src/Microsoft.TemplateEngine.Core/Expressions/Token.cs +++ b/src/Microsoft.TemplateEngine.Core/Expressions/Token.cs @@ -5,7 +5,7 @@ namespace Microsoft.TemplateEngine.Core.Expressions { public class Token { - public Token(TToken family, string value) + public Token(TToken family, object value) { Family = family; Value = value; @@ -13,6 +13,6 @@ public Token(TToken family, string value) public TToken Family { get; } - public string Value { get; } + public object Value { get; } } } diff --git a/src/Microsoft.TemplateEngine.Core/PublicAPI.Shipped.txt b/src/Microsoft.TemplateEngine.Core/PublicAPI.Shipped.txt index 8c2bc07bae..a5d83c0bb3 100644 --- a/src/Microsoft.TemplateEngine.Core/PublicAPI.Shipped.txt +++ b/src/Microsoft.TemplateEngine.Core/PublicAPI.Shipped.txt @@ -331,8 +331,6 @@ override Microsoft.TemplateEngine.Core.TokenConfig.GetHashCode() -> int ~Microsoft.TemplateEngine.Core.Expressions.ScopeBuilder.Build(ref int bufferLength, ref int bufferPosition, System.Action> onFault) -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable ~Microsoft.TemplateEngine.Core.Expressions.ScopeBuilder.ScopeBuilder(Microsoft.TemplateEngine.Core.Contracts.IProcessorState processor, Microsoft.TemplateEngine.Core.Contracts.ITokenTrie tokens, Microsoft.TemplateEngine.Core.Expressions.IOperatorMap operatorMap, bool dereferenceInLiterals) -> void ~Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition -~Microsoft.TemplateEngine.Core.Expressions.Token.Token(TToken family, string value) -> void -~Microsoft.TemplateEngine.Core.Expressions.Token.Value.get -> string ~Microsoft.TemplateEngine.Core.Expressions.TokenScope.Evaluate() -> object ~Microsoft.TemplateEngine.Core.Expressions.TokenScope.Parent.get -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable ~Microsoft.TemplateEngine.Core.Expressions.TokenScope.Parent.set -> void diff --git a/src/Microsoft.TemplateEngine.Core/PublicAPI.Unshipped.txt b/src/Microsoft.TemplateEngine.Core/PublicAPI.Unshipped.txt index 1dbfd8430d..2c208e99df 100644 --- a/src/Microsoft.TemplateEngine.Core/PublicAPI.Unshipped.txt +++ b/src/Microsoft.TemplateEngine.Core/PublicAPI.Unshipped.txt @@ -21,4 +21,6 @@ static Microsoft.TemplateEngine.Core.Util.EngineConfig.DefaultWhitespaces.set -> virtual Microsoft.TemplateEngine.Core.Util.Orchestrator.RunSpecLoader(System.IO.Stream! runSpec) -> Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec! virtual Microsoft.TemplateEngine.Core.Util.Orchestrator.TryGetBufferSize(Microsoft.TemplateEngine.Abstractions.Mount.IFile! sourceFile, out int bufferSize) -> bool virtual Microsoft.TemplateEngine.Core.Util.Orchestrator.TryGetFlushThreshold(Microsoft.TemplateEngine.Abstractions.Mount.IFile! sourceFile, out int threshold) -> bool +~Microsoft.TemplateEngine.Core.Expressions.Token.Token(TToken family, object value) -> void +~Microsoft.TemplateEngine.Core.Expressions.Token.Value.get -> object ~static Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.EvaluateFromString(Microsoft.Extensions.Logging.ILogger logger, string text, Microsoft.TemplateEngine.Core.Contracts.IVariableCollection variables) -> bool \ No newline at end of file diff --git a/src/Microsoft.TemplateEngine.Utils/MultiValueParameter.cs b/src/Microsoft.TemplateEngine.Utils/MultiValueParameter.cs index cfdb12a28c..2b59179ec3 100644 --- a/src/Microsoft.TemplateEngine.Utils/MultiValueParameter.cs +++ b/src/Microsoft.TemplateEngine.Utils/MultiValueParameter.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; namespace Microsoft.TemplateEngine.Utils @@ -38,7 +39,82 @@ public MultiValueParameter(IReadOnlyList values) /// public IReadOnlyList Values { get; private init; } + public static bool TryPerformMultiValueEqual(object x, object y, out bool result) + { + bool isxMv = x is MultiValueParameter; + bool isyMv = y is MultiValueParameter; + + if (!isxMv && !isyMv) + { + result = false; + return false; + } + + { + if (x is MultiValueParameter mv && y is string sv) + { + result = MultivalueEquals(mv, sv); + return true; + } + } + + { + if (y is MultiValueParameter mv && x is string sv) + { + result = MultivalueEquals(mv, sv); + return true; + } + } + + result = Equals(x, y); + return true; + } + /// public override string ToString() => string.Join(MultiValueSeparator.ToString(), Values); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((MultiValueParameter)obj); + } + + /// + public override int GetHashCode() => Values.OrderBy(v => v).ToCsvString().GetHashCode(); + + protected bool Equals(MultiValueParameter other) + { + var set1 = new HashSet(Values); + var set2 = new HashSet(other.Values); + return set1.SetEquals(set2); + } + + private static bool MultivalueEquals(MultiValueParameter mv, string comparand) + { + foreach (string s in mv.Values) + { + if (string.Equals(s, comparand, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } } } diff --git a/src/Microsoft.TemplateEngine.Utils/PublicAPI.Unshipped.txt b/src/Microsoft.TemplateEngine.Utils/PublicAPI.Unshipped.txt index 2a6c0ebd0d..27d5e8520c 100644 --- a/src/Microsoft.TemplateEngine.Utils/PublicAPI.Unshipped.txt +++ b/src/Microsoft.TemplateEngine.Utils/PublicAPI.Unshipped.txt @@ -33,6 +33,7 @@ Microsoft.TemplateEngine.Utils.InMemoryFileSystem.PathRelativeTo(string! target, Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.Dispose() -> void Microsoft.TemplateEngine.Utils.EnumerableExtensions Microsoft.TemplateEngine.Utils.MultiValueParameter +Microsoft.TemplateEngine.Utils.MultiValueParameter.Equals(Microsoft.TemplateEngine.Utils.MultiValueParameter! other) -> bool Microsoft.TemplateEngine.Utils.MultiValueParameter.MultiValueParameter(System.Collections.Generic.IReadOnlyList! values) -> void Microsoft.TemplateEngine.Utils.MultiValueParameter.Values.get -> System.Collections.Generic.IReadOnlyList! Microsoft.TemplateEngine.Utils.PhysicalFileSystem.PathRelativeTo(string! target, string! relativeTo) -> string! @@ -43,6 +44,8 @@ Microsoft.TemplateEngine.Utils.TemplateParameter.AllowMultipleValues.get -> bool Microsoft.TemplateEngine.Utils.TemplateParameter.TemplateParameter(string! name, string! type, string! datatype, Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority priority = Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority.Required, bool isName = false, string? defaultValue = null, string? defaultIfOptionWithoutValue = null, string? description = null, string? displayName = null, bool allowMultipleValues = false, System.Collections.Generic.IReadOnlyDictionary? choices = null) -> void Microsoft.TemplateEngine.Utils.Timing.Timing(Microsoft.Extensions.Logging.ILogger! logger, string! label) -> void override Microsoft.TemplateEngine.Utils.ExactVersionSpecification.ToString() -> string! +override Microsoft.TemplateEngine.Utils.MultiValueParameter.Equals(object? obj) -> bool +override Microsoft.TemplateEngine.Utils.MultiValueParameter.GetHashCode() -> int override Microsoft.TemplateEngine.Utils.MultiValueParameter.ToString() -> string! static Microsoft.TemplateEngine.Utils.ArrayExtensions.CombineArrays(params T[]![]! arrayList) -> T[]! static Microsoft.TemplateEngine.Utils.DictionaryExtensions.CloneIfDifferentComparer(this System.Collections.Generic.IReadOnlyDictionary! source, System.StringComparer! comparer) -> System.Collections.Generic.IReadOnlyDictionary! @@ -63,6 +66,7 @@ static Microsoft.TemplateEngine.Utils.FileSystemInfoExtensions.PathRelativeTo(th static Microsoft.TemplateEngine.Utils.Glob.Parse(string! pattern, bool canBeNameOnlyMatch = true) -> Microsoft.TemplateEngine.Utils.Glob! static Microsoft.TemplateEngine.Utils.ListExtensions.GroupBy(this System.Collections.Generic.IEnumerable! elements, System.Func! grouper, System.Func! hasGroupKey, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable!>! static Microsoft.TemplateEngine.Utils.MultiValueParameter.MultiValueSeparators.get -> char[]! +static Microsoft.TemplateEngine.Utils.MultiValueParameter.TryPerformMultiValueEqual(object! x, object! y, out bool result) -> bool static Microsoft.TemplateEngine.Utils.ParserExtensions.ConvertToDoubleCurrentOrInvariant(object! value) -> double static Microsoft.TemplateEngine.Utils.ParserExtensions.DoubleTryParseŠ”urrentOrInvariant(string? stringValue, out double doubleValue) -> bool static Microsoft.TemplateEngine.Utils.RuntimeValueUtil.TryGetRuntimeValue(this Microsoft.TemplateEngine.Abstractions.IParameterSet! parameters, Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, string! name, out object? value, bool skipEnvironmentVariableSearch = false) -> bool diff --git a/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/EvaluateMacroTests.cs b/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/EvaluateMacroTests.cs index a56922a1a6..b816f35d0e 100644 --- a/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/EvaluateMacroTests.cs +++ b/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/EvaluateMacroTests.cs @@ -9,6 +9,7 @@ using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros.Config; using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; using Xunit; using static Microsoft.TemplateEngine.Orchestrator.RunnableProjects.RunnableProjectGenerator; @@ -38,5 +39,34 @@ public void TestEvaluateConfig(string predicate, bool expectedResult) macro.EvaluateConfig(_engineEnvironmentSettings, variables, macroConfig); Assert.Equal(variables[variableName], expectedResult); } + + [Theory(DisplayName = nameof(TestEvaluateConfig))] + [InlineData("(Param == A)", "C++", "A|B", true)] + [InlineData("(Param == C)", "C++", "A|B", false)] + [InlineData("((Param == A) || (Param == B))", "C++", "A|B", true)] + [InlineData("((Param == A) && (Param == B))", "C++", "A|B", true)] + [InlineData("((Param == A) && (Param != B))", "C++", "A|B", false)] + [InlineData("((Param == A) && (Param == C))", "C++", "A|B", false)] + [InlineData("(Param == A)", "C++2", "A|B", true)] + [InlineData("(Param == C)", "C++2", "A|B", false)] + [InlineData("((Param == A) || (Param == B))", "C++2", "A|B", true)] + [InlineData("((Param == A) && (Param == B))", "C++2", "A|B", true)] + [InlineData("((Param == A) && (Param != B))", "C++2", "A|B", false)] + [InlineData("((Param == A) && (Param == C))", "C++2", "A|B", false)] + public void TestEvaluateMultichoice(string condition, string evaluator, string multichoiceValues, bool expectedResult) + { + string variableName = "myPredicate"; + EvaluateMacroConfig macroConfig = new EvaluateMacroConfig(variableName, null, condition, evaluator); + + IVariableCollection variables = new VariableCollection(); + variables["A"] = "A"; + variables["B"] = "B"; + variables["C"] = "C"; + variables["Param"] = new MultiValueParameter(multichoiceValues.TokenizeMultiValueParameter()); + + EvaluateMacro macro = new EvaluateMacro(); + macro.EvaluateConfig(_engineEnvironmentSettings, variables, macroConfig); + Assert.Equal(variables[variableName], expectedResult); + } } } diff --git a/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/RunnableProjectGeneratorTests.cs b/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/RunnableProjectGeneratorTests.cs index 1de6ef2d35..d18b1e842f 100644 --- a/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/RunnableProjectGeneratorTests.cs +++ b/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/RunnableProjectGeneratorTests.cs @@ -322,6 +322,153 @@ public async void CreateAsyncTest_MultiChoiceParamReplacingAndCondition() Assert.Equal(expectedSnippet, resultContent); } + [Fact] + public async void CreateAsyncTest_MultiChoiceParamAndConditionMacro() + { + // + // Template content preparation + // + + string templateConfig = @" +{ + ""$schema"": ""https://json.schemastore.org/template.json"", + ""author"": ""Test Asset"", + ""classifications"": [ + ""Test Asset"" + ], + ""name"": ""MultiSelect.Template"", + ""generatorVersions"": ""[1.0.0.0-*)"", + ""tags"": { + ""type"": ""project"", + ""language"": ""C#"" + }, + ""groupIdentity"": ""MultiSelect.Template"", + ""precedence"": ""100"", + ""identity"": ""MultiSelect.Template"", + ""shortName"": ""MultiSelect.Template"", + ""sourceName"": ""bar"", + ""symbols"": { + ""Platform"": { + ""type"": ""parameter"", + ""description"": ""The target platform for the project."", + ""datatype"": ""choice"", + ""allowMultipleValues"": true, + ""enableQuotelessLiterals"": true, + ""choices"": [ + { + ""choice"": ""Windows"", + ""description"": ""Windows Desktop"" + }, + { + ""choice"": ""WindowsPhone"", + ""description"": ""Windows Phone"" + }, + { + ""choice"": ""MacOS"", + ""description"": ""Macintosh computers"" + }, + { + ""choice"": ""iOS"", + ""description"": ""iOS mobile"" + }, + { + ""choice"": ""android"", + ""description"": ""android mobile"" + }, + { + ""choice"": ""nix"", + ""description"": ""Linux distributions"" + } + ], + ""defaultValue"": ""MacOS|iOS"" + }, + ""IsMobile"": { + ""type"": ""computed"", + ""value"": ""((Platform == android || Platform == iOS || Platform == WindowsPhone) && Platform != Windows && Platform != MacOS && Platform != nix)"" + }, + ""IsAndroidOnly"": { + ""type"": ""computed"", + ""value"": ""(Platform == android && Platform != iOS && Platform != WindowsPhone && Platform != Windows && Platform != MacOS && Platform != nix)"" + }, + ""joinedRename"": { + ""type"": ""generated"", + ""generator"": ""join"", + ""replaces"": ""SupportedPlatforms"", + ""parameters"": { + ""symbols"": [ + { + ""type"": ""ref"", + ""value"": ""Platform"" + } + ], + ""separator"": "", "", + ""removeEmptyValues"": true + } + } + } +} +"; + + string sourceSnippet = @" +//#if IsAndroidOnly +This renders for android only +//#elseif IsMobile +This renders for rest of mobile platforms +//#else +This renders for desktop platforms +//#endif +Console.WriteLine(""Hello, World!""); + +// Plats: SupportedPlatforms +"; + + string expectedSnippet = @" +This renders for rest of mobile platforms +Console.WriteLine(""Hello, World!""); + +// Plats: android, iOS +"; + + IDictionary templateSourceFiles = new Dictionary(); + // template.json + templateSourceFiles.Add(TestFileSystemHelper.DefaultConfigRelativePath, templateConfig); + + //content + templateSourceFiles.Add("sourcFile", sourceSnippet); + + // + // Dependencies preparation and mounting + // + + IEngineEnvironmentSettings environment = _environmentSettingsHelper.CreateEnvironment(); + string sourceBasePath = FileSystemHelpers.GetNewVirtualizedPath(environment); + string targetDir = FileSystemHelpers.GetNewVirtualizedPath(environment); + + TestFileSystemHelper.WriteTemplateSource(environment, sourceBasePath, templateSourceFiles); + IMountPoint? sourceMountPoint = TestFileSystemHelper.CreateMountPoint(environment, sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + SimpleConfigModel configModel = SimpleConfigModel.FromJObject(JObject.Parse(templateConfig)); + IRunnableProjectConfig runnableConfig = new RunnableProjectConfig(environment, rpg, configModel, sourceMountPoint.FileInfo(TestFileSystemHelper.DefaultConfigRelativePath)); + IParameterSet parameters = new ParameterSet(runnableConfig); + ITemplateParameter choiceParameter; + Assert.True(parameters.TryGetParameterDefinition("Platform", out choiceParameter), "ChoiceParam expected to be extracted from template config"); + parameters.ResolvedValues[choiceParameter] = new MultiValueParameter(new[] { "android", "iOS" }); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await rpg.CreateAsync(environment, runnableConfig, sourceDir, parameters, targetDir, CancellationToken.None); + + // + // Veryfying the outputs + // + + string resultContent = environment.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourcFile")); + Assert.Equal(expectedSnippet, resultContent); + } + [Fact] public async void CreateAsyncTest_MultiChoiceParamJoining() {