diff --git a/documentation/TemplateEngine/Available-Symbols-Generators.md b/documentation/TemplateEngine/Available-Symbols-Generators.md new file mode 100644 index 000000000000..1d47d8234d7b --- /dev/null +++ b/documentation/TemplateEngine/Available-Symbols-Generators.md @@ -0,0 +1,657 @@ +Inside the `template.json` file, you can define custom symbols that will be used inside the template files. +The supported symbol types are: +- Parameter - the value is typically provided by the user when creating the template. If not provided, the value is taken from host configuration, otherwise default value is used. +- Derived - defines transformation of another symbol. The value of this symbol is derived from the value of another symbol by applying the defined form. +- Computed - the boolean value is evaluated during the processing of the template based on the other symbol values. +- Generated - the value is computed by a built-in symbol value generator. +There are no restrictions on symbol order: e.g. generated/computed/derived symbols can be used in any type of other symbols. The only rule is in avoiding circular dependencies such as: +```json +"symbols": { + "switchCheck": { + "type": "generated", + "generator": "switch", + "datatype": "string", + "parameters": { + "evaluator": "C++", + "cases": [ + { + "condition": "(switchCheck2 == 'regions')", + "value": "regions" + } + ] + } + }, + "switchCheck2": { + "type": "generated", + "generator": "switch", + "datatype": "string", + "parameters": { + "evaluator": "C++", + "cases": [ + { + "condition": "(switchCheck == 'regions')", + "value": "regions" + } + ] + } + } +``` +This article covers available generators for generated symbols. + +To use a generated symbol inside your `template.json` file: +1. Add `"type": "generated"` to the symbol definition +1. Use the `"generator": ...` parameter to select the generator to use. +This is a sample of definition of a generated symbol, the `port` generator, that generates a random number for an http port. + +```json +"IISExpressPortGenerated": { + "type": "generated", + "generator": "port", + "parameters": { + "fallback":"5000" + } +}, +``` + +Most of the generators need to be configured via parameters that let you select the source of the data and select among the options available. Below is a sample of a symbol that uses the `now` generator to replace a fixed year indication present in the source files with the current year. + +```json +"copyrightYear": { + "type": "generated", + "generator": "now", + "replaces": "1975", + "parameters": { + "format": "yyyy" + } +}, +``` + +Available built-in generators for computing generated symbols values are: + +| Name | Description | +|----------|---------------| +| [casing](#casing) | Enables changing the casing of a string. | +| [coalesce](#coalesce) | Behaves like the C# `??` operator. | +| [constant](#constant) | Constant value | +| [port](#port) | Generate a port number that can be used by web projects. | +| [guid](#guid) | Create a new guid. | +| [now](#now) | Get the current date/time. | +| [random](#random) | Generate random int. | +| [regex](#regex) | Process a regular expression. | +| [regexMatch](#regexmatch) | Checks if the value matches the regex pattern. | +| [switch](#switch) | Behaves like a C# `switch` statement. | +| [join](#join) | Concatenates multiple symbols or constants. | + +## Casing +Changes the case of the text of the source value to all upper-case or all lower-case. It does not affect spaces (i.e. does not do any sort of Camel Casing). + +#### Parameters +| Name |Data Type| Description |Mandatory| +|----------|------|---------------|---| +|`source`|`string`| The name of symbol to use as the source of data.| yes | +|`toLower`|`bool`| applies lower case if `true`, upper case otherwise| no | + +### Samples + +In this sample three symbols are defined: + - `ownerName` is a parameter which can be set on the command line using `dotnet new` It has a default value of "John Doe", that will be used if the no value is received from the host. The value will be used to replace "John Smith (a)". + - `nameUpper` and `nameLower` are the symbols that generate the upperCase and lowerCase version of `ownerName` that are used to replace any instance of "John Smith (U)" and "John Smith (l)". + +```json +"symbols":{ + "ownerName":{ + "type": "parameter", + "datatype":"text", + "replaces": "John Smith (a)", + "defaultValue": "John Doe" + }, + + "nameUpper":{ + "type": "generated", + "generator": "casing", + "parameters": { + "source":"ownerName", + "toLower": false + }, + "replaces":"John Smith (U)" + }, + + "nameLower":{ + "type": "generated", + "generator": "casing", + "parameters": { + "source":"ownerName", + "toLower": true + }, + "replaces":"John Smith (l)" + } +} +``` + +### Related + +[`Implementation class`](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CaseChangeMacro.cs) +[`Sample`](https://github.com/dotnet/dotnet-template-samples/tree/master/11-change-string-casing) + + +## Coalesce +Behaves like the C# `??` operator. Note: the empty string value and default value of value type is treated as `null`. +The typical use of this generator is to check if the parameter was provided by user, otherwise set fallback generated value. + +#### Parameters + +|Name|Data Type|Description|Mandatory| +|---|---|---|---| +|`sourceVariableName`|`string`|the symbol name which is a primary source of data (left operand of `coalesce`)|yes| +|`fallbackVariableName`|`string`|the symbol name which is an alternate source of data(right operand of `coalesce`)|yes| +|`defaultValue`|`string`|The default value. In case it is specified, and primary source is equal to this value, the fallback value will be used.|no| + +### Samples + +In this sample three symbols are defined: + - `MessageYear` - is a parameter set by the user when calling `dotnet new`. + - `ThisYear` - use the now generator to calculate the current year. + - `YearReplacer` - ensures that any occurrence of "1234" is replaced. If `MessageYear` was passed in by the user that value will be used. Otherwise `ThisYear` will be used. + +```json + "symbols":{ + "MessageYear":{ + "type": "parameter", + "datatype":"int" + }, + "ThisYear":{ + "type": "generated", + "generator": "now", + "parameters": { + "format": "yyyy" + } + }, + "YearReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "MessageYear", + "fallbackVariableName": "ThisYear" + }, + "replaces": "1234" + } + } +``` + +### Related +[`Implementation class`](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CoalesceMacro.cs) + +## Constant + +Uses constant value. + +#### Parameters + +|Name|Data Type|Description|Mandatory| +|----------|------|---------------|---| +|`value`|`string`|constant value|yes| + + +### Samples + +`myConstant` is a symbol that replaces "1234" with "5001" + +```json +"symbols":{ + "myConstant": { + "type": "generated", + "generator": "constant", + "parameters": { + "value":"5001" + }, + "replaces":"1234" + } +} +``` + +### Related + +[`Implementation class`](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ConstantMacro.cs) +[`Sample`](https://github.com/dotnet/dotnet-template-samples/tree/master/13-constant-value) + + +## Port +Gets an available port number on the machine. +During evaluation looks for a valid free port number trying to create a socket, and in case of problems, returns the value defined in the `fallback` parameter. + +#### Parameters + +|Name|Data Type|Description|Mandatory| +|----------|------|---------------|---| +|`high`|`integer`|defined the high bound of range to select port from. The maximum value is `65535`. If greater value is specified, `65535` is used instead. |no, default: `65535`| +|`low`|`integer`|defined the low bound of range to select port from. The minimum value is `1024`. If less value is specified, `1024` is used instead.|no, default: `1024`| +|`fallback`|`integer`|fallback value|no, default: `0`| + +Note: if `low` > `high`, the default values for `low` and `high` are used: 1024 - 65535. + +The following ports are reserved: +- 1719 - H323 (RAS) +- 1720 - H323 (Q931) +- 1723 - H323 (H245) +- 2049 - NFS +- 3659 - apple-sasl / PasswordServer [Apple addition] +- 4045 - lockd +- 4190 - ManageSieve [Apple addition] +- 5060 - SIP +- 5061 - SIPS +- 6000 - X11 +- 6566 - SANE +- 6665 - Alternate IRC [Apple addition] +- 6666 - Alternate IRC [Apple addition] +- 6667 - Standard IRC [Apple addition] +- 6668 - Alternate IRC [Apple addition] +- 6669 - Alternate IRC [Apple addition] +- 6679 - Alternate IRC SSL [Apple addition] +- 6697 - IRC+SSL [Apple addition] +- 10080 - amanda + +### Samples +In this sample `KestrelPortGenerated` is a symbol that return the number of an available port or 5000. + +```json + "KestrelPortGenerated": { + "type": "generated", + "generator": "port" + "parameters": { + "fallback":"5000" + } + }, +``` + +### Related +[`Implementation class`](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GeneratePortNumberMacro.cs) + + +## Guid + +*Note:* [Guids section in `template.json`](Reference-for-template.json.md#guids) can be used to achieve same goals with easier configuration + +Creates a formatted guid for a replacement. To configure the output format of the macro you can use the **defaultFormat** parameter that accepts a single value from **{'n', 'd', 'b', 'p', 'x'}** for lowercase output or **{'N', 'D', 'B', 'P', 'X'}** for uppercase output. The formats are defined in [`Guid.ToString()` method documentation](https://msdn.microsoft.com/en-us/library/97af8hh4(v=vs.110).aspx) +#### Parameters +|Name|Data Type|Description|Mandatory| +|----------|------|---------------|---| +|`defaultFormat`|`string`|format descriptor|no, default: `D`| + +### Samples +This sample creates different symbols showing the different formatting available for the generated guid. + +```json +"symbols":{ + "id01":{ + "type": "generated", + "generator": "guid", + "replaces": "myid01", + "parameters": { + "defaultFormat":"N" + } + }, + "id02":{ + "type": "generated", + "generator": "guid", + "replaces": "myid02", + "parameters": { + "defaultFormat":"D" + } + }, + "id03":{ + "type": "generated", + "generator": "guid", + "replaces": "myid03", + "parameters": { + "defaultFormat":"B" + } + }, + "id04":{ + "type": "generated", + "generator": "guid", + "replaces": "myid04", + "parameters": { + "defaultFormat":"P" + } + }, + "id05":{ + "type": "generated", + "generator": "guid", + "replaces": "myid05", + "parameters": { + "defaultFormat":"X" + } + } +} +``` + +### Related +[`Implementation class`](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GuidMacro.cs) +[`Guid Format Documentation`](https://msdn.microsoft.com/en-us/library/97af8hh4(v=vs.110).aspx) +[Guids section in `template.json`](Reference-for-template.json.md#guids) + + +## Now + +Creates a symbol from the current date/time. + +#### Parameters +|Name|Data Type|Description|Mandatory| +|----------|------|---------------|---| +|`format`|`string`|[`DateTime.ToString()`](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings) format|no| +|`utc`|`bool`|UTC time if `true`, local time otherwise|no| + +### Samples +In this sample a symbol is created showing the current data, and replacing any instance of "01/01/1999" + +```json +"symbols":{ + "createdDate": { + "type": "generated", + "generator": "now", + "parameters": { + "format": "MM/dd/yyyy" + }, + "replaces":"01/01/1999" + } +} +``` + +### Related +[`Implementation class`](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/NowMacro.cs) +[`DateTime.ToString documentation`](https://msdn.microsoft.com/en-us/library/zdtaw1bw(v=vs.110).aspx) +[`Sample`](https://github.com/dotnet/dotnet-template-samples/tree/master/10-symbol-from-date) + +## Random + +Creates a random integer value in a specified range. + +#### Parameters +|Name|Data Type|Description|Mandatory| +|----------|------|---------------|---| +|`low`|`integer`|lower inclusive bound|yes| +|`high`|`integer`|upper exclusive bound|no, default:`int.MaxValue`| + +### Samples +This sample shows a symbol that generates a value from `0` to `10000` excluded, and replace any instance of `4321` + +```json +"symbols":{ + "myRandomNumber":{ + "type": "generated", + "generator": "random", + "parameters": { + "low": 0, + "high": 10000 + }, + "replaces": "4321" + } +} +``` + +### Related +[`Implementation class`](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RandomMacro.cs) +[`Sample`](https://github.com/dotnet/dotnet-template-samples/tree/master/12-random-number) + + +## Regex +Defines a list of data manipulation steps based on regex expressions. + +#### Parameters +|Name|Data Type|Description|Mandatory| +|----------|------|---------------|---| +|`source`|`string`|the symbol to transform|yes| +|`steps`|`array`|replacement steps|yes| + +`steps` element definition: +|Name|Data Type|Description|Mandatory| +|----------|------|---------------|---| +|`regex`|`string`, regex pattern|selection pattern|yes| +|`replacement`|`string`|the replacement value for matched pattern|yes| + +### Samples + +```json +"symbols": { + "regexExample": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "replaces": "A different message", //The value to replace in the output + "parameters": { + "source": "message", //The name of the symbol whose value should be operated on + "steps": [ + { + "regex": "^test", //The regular expression whose matches will be replaced with '[Replaced]` + "replacement": "[Replaced]" //The value to replace matches of the expression '^test' with + }, + { + "regex": "test$", + "replacement": "[/Replaced]" + } + ] + } + } +} +``` + +### Related +[`Implementation class`](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMacro.cs) +[`RegEx.Replace Documentation`](https://msdn.microsoft.com/en-us/library/xwewhkd1(v=vs.110).aspx) + +## RegexMatch +Tries to match regex pattern against value of source symbol and returns `true` if matched, otherwise `false`. + +#### Parameters +|Name|Data Type|Description|Mandatory| +|----------|------|---------------|---| +|`source`|`string`|the symbol to attempt to match value|yes| +|`pattern`|`string`, regex pattern|the regex match pattern|yes| + +### Samples + +```json +"symbols": { + "isMatch": { + "type": "generated", + "generator": "regexMatch", + "dataType": "bool", + "replaces": "test.value1", + "parameters": { + "source": "name", + "pattern": "^hello$" + } + } +} +``` + +### Related +[`Implementation class`](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMatchMacro.cs) +[`Regex.IsMatch Documentation`](https://docs.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch) + +## Switch + +Defines a set of conditions to be evaluated, and the value to return if the condition is met. The first condition to evaluate to true is used. To include a default case, add a condition that always evaluates to true as the last entry in `cases`. + +#### Parameters +|Name|Data Type|Description|Mandatory| +|----------|------|---------------|---| +|`cases`|`array`|choices to evaluate|yes| +|`evaluator`|`enum`: `C++2`, `C++`, `MSBuild`, `VB`|expression evaluation engine|no, default: `C++2`| + +`cases` definition +|Name|Data Type|Description|Mandatory| +|----------|------|---------------|---| +|`condition`|`string`|the condition to evaluate, keep empty for default clause.|no| +|`value`|`string`|the value to return, if `condition` evaluates to `true`|yes| + +### Samples + +This sample shows how to change the replacement value based on evaluating conditions using other symbols: + +```json +"symbols": { + "test": { + "type": "parameter", + "datatype": "string" + }, + "example": { + "type": "generated", + "generator": "switch", + "replaces": "abc", + "parameters": { + "evaluator": "C++", + "datatype": "string", + "cases": [ + { + "condition": "(test == '123')", + "value": "456" + }, + { + "condition": "(test == '789')", + "value": "012" + } + ] + } + } +} +``` +In this case, if the user enters the value `123` as the value of the parameter `test`, `abc` in the content will be replaced with `456`, if the user enters `789`, `abc` is replaced with `012` instead. + +### Related +[`Implementation class`](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/SwitchMacro.cs) + +## Join + +Concatenates multiple symbols or constants with the defined separator into a new symbol. + +#### Parameters +|Name|Data Type|Description|Mandatory| +|----------|---------|----------|-----| +|`symbols` |`array` |defines the values to concatenate|yes| +|`separator` |`string` |the value used as the separator between the values to be concatenated, notice that you can use `/` as folder separator also on Windows, since File API will convert it into `\` | no | +|`removeEmptyValues` |`bool`|indicates whether the empty values should be skipped or honored. By default this switch is off - leading to multiple consecutive separators in output string in case that same input values are null or empty| no | + +`symbols` definition +|Name|Data Type|Description|Mandatory| +|------|---------|---------------|---| +|`type` |`enum`: `ref`, `const` |`ref` indicates that the value is referenced from another symbol
`const` - the value is a string constant|no, default: `const`| +|`value` |`string` |either a name of another symbol or string constant|yes, should be not empty or whitespace when `type` is `ref`| + +### Samples + +This sample shows how to change the replacement value based on evaluating conditions using other symbols: + +```json +"symbols": { + "company": { + "type": "parameter", + "dataType": "string", + "defaultValue": "Microsoft" + }, + "product": { + "type": "parameter", + "dataType": "string", + "defaultValue": "Visual Studio" + }, + "joinedRename": { + "type": "generated", + "generator": "join", + "fileRename": "Api", + "parameters": { + "symbols": [ + { + "type": "const", + "value": "Source" + }, + { + "type": "const", + "value": "Api" + }, + { + "type": "ref", + "value": "company" + }, + { + "type": "ref", + "value": "product" + } + ], + "separator": "/", + "removeEmptyValues": true + } + } +} +``` +This sample will rename folder called `Api` into `Source/Api/Microsoft/Visual Studio`. Notice that File API will automatically change `/` into `\` on Windows. + +Joining [multi-choice symbol](Reference-for-template.json.md#multichoice-symbols-specifics) values: + +`template.json`: +```json +"symbols": { + "Platform": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "allowMultipleValues": 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" + }, + "joinedRename": { + "type": "generated", + "generator": "join", + "replaces": "SupportedPlatforms", + "parameters": { + "symbols": [ + { + "type": "ref", + "value": "Platform" + } + ], + "separator": ", ", + "removeEmptyValues": true, + } + } +} +``` + +`Program.cs`: +```C# +// This file is generated for platform: SupportedPlatforms +``` + +This sample will expand and join values of `Platform` argument and replace `SupportedPlatforms` string with `MacOS, iOS`: + +`Program.cs`: +```C# +// This file is generated for platform: MacOS, iOS +``` + +### Related +[`Implementation class`](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/JoinMacro.cs) diff --git a/documentation/TemplateEngine/Available-templates-for-dotnet-new.md b/documentation/TemplateEngine/Available-templates-for-dotnet-new.md new file mode 100644 index 000000000000..042aad9077c6 --- /dev/null +++ b/documentation/TemplateEngine/Available-templates-for-dotnet-new.md @@ -0,0 +1,95 @@ +To search for the templates available on NuGet.org, use [`dotnet new search`](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-new-search). +``` + dotnet new search web + dotnet new search azure --type project + dotnet new search azure --author Microsoft +``` + +To ensure that the template package appears in `dotnet new search` result, set [the NuGet package type](https://docs.microsoft.com/en-us/nuget/create-packages/set-package-type) to `Template`. + +**Note:** In order to show templates preinstalled with SDK and installed manually run the [`dotnet new list`](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-new-list). + +``` + dotnet new list + dotnet new list --author Microsoft + dotnet new list web + dotnet new list web --type project +``` + +Below is a list of selected NuGet.org templates which are available for use with `dotnet new`: + +# C# Templates + + +| Name | Quick Install | +|----------|:--------------| +| [.NET Boxed](https://github.com/Dotnet-Boxed/Templates) | `dotnet new install "Boxed.Templates"`| +| [Auth0 Templates](https://github.com/auth0/auth0-dotnet-templates) | `dotnet new install Auth0.Templates` | +| [AWS Lambda .NET Core Templates](https://github.com/aws/aws-lambda-dotnet/tree/master/Blueprints) | `dotnet new install "Amazon.Lambda.Templates"`| +| [Avalonia UI Templates](https://github.com/AvaloniaUI/Avalonia) - Avalonia is a framework for creating cross platform UI | `dotnet new install "Avalonia.Templates"`| +| [Blazor](http://blazor.net) - Full stack web development with C# and WebAssembly | `dotnet new install "Microsoft.AspNetCore.Blazor.Templates::3.0.0-*"`| +| [Cake.Frosting](https://github.com/cake-build/cake) | `dotnet new install "Cake.Frosting.Template"` | +| [Carter](https://github.com/CarterCommunity/Carter) - Carter is a library that allows Nancy-esque routing for use with ASP.Net Core. | `dotnet new install "CarterTemplate"`| +| [CleanBlazor](https://github.com/fvilches17/CleanBlazor) - Minimal Blazor projects. Standard Blazor project templates minus any boilerplate assets (e.g. Bootstrap, Counter.razor, etc.) | `dotnet new install "FriscoVInc.DotNet.Templates.CleanBlazor"` | +| [cloudscribe](https://www.cloudscribe.com/docs/introduction) | `dotnet new install "cloudscribe.templates"` | +| [DotVVM](https://github.com/riganti/dotvvm) - Open source MVVM framework for line of business web applications | `dotnet new install "DotVVM.Templates"` | +| [Eto.Forms](https://github.com/picoe/Eto) | `dotnet new install "Eto.Forms.Templates"` | +| [GCC.Build](https://github.com/roozbehid/dotnet-vcxproj) - C/C++/CPP/VCXPROJ Build Template using GCC/G++/EMCC or your favorate compiler | `dotnet new install GCC.Build.Template` | +| [Geco](https://github.com/iQuarc/Geco) - C# 6 Interpolated strings based Code Generator | `dotnet new install "iQuarc.Geco.CSharp"` | +| [GtkSharp](https://github.com/GtkSharp/GtkSharp) | `dotnet new install "GtkSharp.Template.CSharp"` | +| [IdentityServer4.Templates](https://github.com/IdentityServer/IdentityServer4.Templates) | `dotnet new install "identityserver4.templates"` | +| [Kentico Cloud Boilerplate](https://github.com/Kentico/cloud-boilerplate-net) | `dotnet new install "KenticoCloud.CloudBoilerplateNet"` | +| [MonoGame (.NET Core)](https://github.com/MonoGame/MonoGame) | `dotnet new install "MonoGame.Templates.CSharp"` | +| [MSBuild extension](https://github.com/tintoy/msbuild-extension-template) | `dotnet new install "MSBuildExtensionTemplate"` | +| [MvxScaffolding Templates](https://github.com/Plac3hold3r/MvxScaffolding) - MvvmCross Xamarin native and Xamarin Forms templates. | `dotnet new install "MvxScaffolding.Templates"` | +| [NancyFX Template](https://github.com/jchannon/NancyTemplate) | not on nuget.org | +| [NSpec Templates](https://github.com/nspec/DotNetNewNSpec) | `dotnet new install "dotnet-new-nspec"` | +| [NUnit 3 Test Project Template](https://github.com/nunit/dotnet-new-nunit) | `dotnet new install "NUnit3.DotNetNew.Template"` | +| [Paulovich.Caju](https://github.com/ivanpaulovich/dotnet-new-caju) - .NET applications with Event Sourcing, Hexagonal or Clean Architectures styles | `dotnet new install "Paulovich.Caju"` | +| [Paulovich.Manga](https://github.com/ivanpaulovich/manga-clean-architecture) - Clean Architecture for .NET Applications! | `dotnet new install "Paulovich.Manga"` | +| [Particular Templates](https://docs.particular.net/nservicebus/dotnet-templates) - Templates targeting NServiceBus and other tools and libraries from [Particular Software](https://particular.net/) | `dotnet new install "ParticularTemplates"` | +| [Pioneer Console Boilerplate](https://github.com/PioneerCode/pioneer-console-boilerplate) - Boilerplated .NET Core console application that includes dependency injection, logging and configuration. | `dotnet new install "Pioneer.Console.Boilerplate"` | +| [PowerShell Core](https://github.com/tintoy/ps-core-module-template) | `dotnet new install "FiftyProtons.Templates.PSCore"` | +| [Prism Forms QuickStarts](https://github.com/dansiegel/Prism-Templates) - Empty & QuickStart project Templates for Prism for Xamarin Forms. *Requires dotnet cli 2.0 pre3+* | `dotnet new install "Prism.Forms.QuickstartTemplates"` | +| [Raspberry Pi 3](https://github.com/jeremylindsayni/RaspberryPiTemplate) - C# template for .NET Core 2 IoT applications. | `dotnet new install "RaspberryPi.Template"` | +| [ServiceStack](https://github.com/NetCoreApps/templates) | `dotnet new install "ServiceStack.Core.Templates"` | +| [SpecFlow.Templates.DotNet](https://github.com/SpecFlowOSS/SpecFlow) - A project template for creating executable specifications with SpecFlow. You can choose from different .NET frameworks and test frameworks. |`dotnet new install "SpecFlow.Templates.DotNet"` | +| [Template templates](https://github.com/tintoy/dotnet-template-templates) - Templates to create new project and item templates. Requires `new3`. | `dotnet new install "FiftyProtons.Templates.DotNetNew"` | +| [Zahasoft Templates](https://github.com/zahasoft/skele) | `dotnet new install "Zahasoft.Skele"` | +| [ASP.NET Core Web API (extended)](https://github.com/popov1024/httpapi-template-sharp) | `dotnet new install "Popov1024.HttpApi.Template.CSharp"` | +| [ASP.NET Core Web API for AKS](https://github.com/robbell/dotnet-aks-api-template) - A template for creating a fully-featured, 12 Factor, ASP.NET Core Web API for AKS | `dotnet new install "RobBell.AksApi.Template"` | +| [HoNoSoFt.DotNet.Web.Spa.ProjectTemplates (VueJs + Picnic CSS)](https://github.com/Nordes/HoNoSoFt.DotNet.Web.Spa.ProjectTemplates) | `dotnet new install "HoNoSoFt.DotNet.Web.Spa.ProjectTemplates"` | +| [xUnit Test Template](https://github.com/gatewayprogrammingschool/xUnit.Template) - Adds a xUnit test file to an existing test project. | `dotnet new install GatewayProgrammingSchool.xUnit.CSharp`| +[RocketMod Plugin Templates](https://github.com/RocketMod/Rocket.Templates) RocketMod is a plugin framework for .NET based games. This template allows to quickly get started with a new RocketMod Plugin.| `dotnet new install "Rocket.Templates"` | +| [EISK Web Api](https://github.com/eisk/eisk.webapi) - ASP.NET Core templates with simple use cases to build scalable web api with architectural best practices (DDD, Onion Architecture etc). | `dotnet new install "eisk.webapi"` | +|[OpenMod Plugin Templates](https://github.com/openmod/openmod/tree/master/templates) - OpenMod is .NET plugin framework. These templates allow user to quickly get started with a new OpenMod Plugin.| `dotnet new install "OpenMod.Templates"` | + +# F# Templates + +| Name | Quick Install | +|----------|:--------------| +| [ASP.NET Core WebAPI F# Template](https://github.com/MNie/FSharpNetCoreWebApiTemplate) | `dotnet new install "WebAPI.FSharp.Template"` | +| [Bolero: F# in WebAssembly](https://fsbolero.io/)| `dotnet new install Bolero.Templates`| +| [Eto.Forms](https://github.com/picoe/Eto) | `dotnet new install "Eto.Forms.Templates"` | +| [Expecto Template](https://github.com/MNie/Expecto.Template) | `dotnet new install "Expecto.Template"`| +| [F# TypeProvider Template](https://github.com/fsprojects/FSharp.TypeProviders.SDK#the-f-type-provider-sdk)| `dotnet new install FSharp.TypeProviders.Templates`| +| [Fable-elmish](https://github.com/fable-compiler/fable-elmish) | `dotnet new install "Fable.Template.Elmish.React"` | +| [Fable, F# \|> Babel](http://fable.io) | `dotnet new install "Fable.Template"` | +| [Fable Library](https://github.com/TheAngryByrd/Fable.Template.Library) - F# Template for creating and publishing Fable libraries | `dotnet new install "Fable.Template.Library"` | +| [Fabulous for Xamarin.Forms](https://github.com/fsprojects/Fabulous/tree/master/Fabulous.XamarinForms)| `dotnet new install Fabulous.XamarinForms.Templates`| +| [Freya](https://freya.io) | `dotnet new install "Freya.Template"` | +| [Giraffe Template](https://github.com/giraffe-fsharp/giraffe-template) | `dotnet new install "giraffe-template"` | +| [GtkSharp](https://github.com/GtkSharp/GtkSharp) | `dotnet new install "GtkSharp.Template.FSharp"` | +| [Interstellar](https://github.com/fsprojects/Interstellar) | `dotnet new install "Interstellar.Template"` | +| [MiniScaffold](https://github.com/TheAngryByrd/MiniScaffold) - F# Template for creating and publishing libraries targeting .NET Full (net45) and Core (netstandard1.6) | `dotnet new install "MiniScaffold"` | +| [NancyFx](https://github.com/MNie/NancyFxCore)| `dotnet new install "NancyFx.Core.Template"`| +| [SAFE Template](https://safe-stack.github.io/)| `dotnet new install "SAFE.Template"`| +| [vbfox's F# Templates](https://github.com/vbfox/FSharpTemplates)| `dotnet new install "BlackFox.DotnetNew.FSharpTemplates"`| +| [WebSharper](https://github.com/dotnet-websharper/core)| `dotnet new install "WebSharper.Templates"` + +# VBNet Templates + +| Name | Quick Install | +|----------|:--------------| +| [GtkSharp](https://github.com/GtkSharp/GtkSharp) | `dotnet new install "GtkSharp.Template.VBNet"` | +| [InteXX Assorted Templates](https://github.com/InteXX/Templates) | `dotnet new install "Intexx.Templates"` | diff --git a/documentation/TemplateEngine/Binding-and-project-context-evaluation.md b/documentation/TemplateEngine/Binding-and-project-context-evaluation.md new file mode 100644 index 000000000000..8ba818f91dc3 --- /dev/null +++ b/documentation/TemplateEngine/Binding-and-project-context-evaluation.md @@ -0,0 +1,102 @@ +# Binding and project context evaluation + +## Overview + +Since .NET SDK 7.0.100 template engine supports binding of symbols to various external sources, including MSBuild properties of the current project. +The feature is available for both `dotnet new` and Visual Studio. + +## `bind` symbols + +The symbol binds value from external sources. +By default, the following sources are available: +- host parameters - parameters defined at certain host. For .NET SDK the following parameters are defined: `HostIdentifier: dotnetcli`, `GlobalJsonExists: true/false`, `WorkingDirectory: `. Binding syntax is `host:`, example: `host:HostIdentifier`. +- environment variables - allows to bind environment variables. Binding syntax is `env:`, example: `env:MYENVVAR`. + +It is also possible to bind the parameter without the prefix as a fallback behavior: `HostIdentifier`, `MYENVVAR`. + +The priority of the sources are following: +- host parameters: 100 +- environment variables: 0 + +The higher value indicates higher priority. + + +|Name|Description|Mandatory| +|---|---|---| +|`type`|`bind`|yes| +|`binding`| Mandatory. The name of the source and parameter in the source to take the value from. The syntax follows: `:`.|yes| +|`replaces`|The text to be replaced by the symbol value in the template files content.|no| +|`fileRename`|The portion of template filenames to be replaced by the symbol value.|no| +|`defaultValue`|The value assigned to the symbol if no value was provided from external source(s). Recommended to be used when `replaces` and/or `fileRename` is used. In case default value is not specified and no value was provided from external source(s) the replacement won't be performed. |no| +|`dataType`|The value assigned to the symbol if no value was provided from external source(s). Allowed values are: "bool", "float", "int", "hex", "text", "string". If not specified, the value type will be inferred. In case the type of values might be ambiguous, consider specifying the desired datatype for processing. In case the conversion of value to the type fails, the symbol will be skipped from further processing. For more information about supported data types and their restrictions, refer to [data type description](Reference-for-template.json.md#parameter-symbol) for parameter symbols. |no| + +##### Example + +```json +"symbols": { + "HostIdentifier": { + "type": "bind", + "binding": "host:HostIdentifier" + }, + "WorkingDirectory": { + "type": "bind", + "binding": "host:WorkingDirectory" + } +} +``` + +### Binding to MSBuild properties + +It is possible to bind symbols to MSBuild properties of the current project. The prefix to be used: `msbuild` and it is mandatory. +Commonly used with item templates to get the information about the project it is added to. + +`dotnet new` attempts to find the closest project file using following rules: +- The project in current directory or `--output` directory (matching `*.*proj` extension). +- If not found, the parent of above and so on. +- The path to the project can be explicitly specified using `--project` instantiation option. This path takes precedence - so it can be used in case of ambiguity. + +Once project is located, its MSBuild properties are evaluated. The project should be restored, otherwise evaluation fails. +Only .NET [SDK-style projects](https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview) are supported. + +It is recommended to configure `defaultValue` for `bind` symbol that will be used in case evaluation fails. +If applicable, it is also recommended to use [project capability constraint](https://github.com/dotnet/templating/wiki/Constraints#Project-capabilities) to define the projects that the template can be added to. + +Example - binds `DefaultNamespace` symbol to `RootNamespace` of the project: +```json +"symbols": { + "DefaultNamespace": { + "type": "bind", + "binding": "msbuild:RootNamespace", + "replaces": "%NAMESPACE%", + "defaultValue": "TestNamespace" + } +}, +"constraints": { + "csharp-only": { + "type": "project-capability", + "args": "CSharp + TestContainer" // only allowed in C# test project + } +} +``` + + +## Visual Studio specifics + +Visual Studio supports binding to host parameters, environment variables and MSBuild properties. +In addition to that, there is additional `context` source supporting: +- `context:createsolutiondirectory` - indicates whether a solution directory is to be created as a result of project creation (Place solution and project in same directory is UNCHECKED in NPD). +- `context:isexclusive` - indicates whether the template instantiation is a result of a new project being created (true) vs result of adding to an existing solution (false). +- `context:solutionname` - the name of the solution, which may be different from the project name. + +Visual Studio also provides a way to bind to "namespace" via host parameters source: +```json + "type": "bind" + "binding": "namespace" +``` + +or + +```json + "type": "bind" + "binding": "host:namespace" +``` diff --git a/documentation/TemplateEngine/Blog-posts.md b/documentation/TemplateEngine/Blog-posts.md new file mode 100644 index 000000000000..1815ab277d38 --- /dev/null +++ b/documentation/TemplateEngine/Blog-posts.md @@ -0,0 +1,8 @@ + - [**How to create your own templates for `dotnet new`**](https://aka.ms/dotnetnew-create-templates) by @sayedihashimi + - [Announcing .NET Core (includes how to use `dotnet new`)](https://blogs.msdn.microsoft.com/dotnet/2017/03/07/announcing-net-core-tools-1-0/) by @sayedihashimi + - [Trying out "dotnet new" template updates and csproj with VS2017](https://www.hanselman.com/blog/TryingOutDotnetNewTemplateUpdatesAndCsprojWithVS2017.aspx) by @shanselman + - [Custom Project Templates Using dotnet new](http://rehansaeed.com/custom-project-templates-using-dotnet-new/) by @RehanSaeed + - [dotnet new Feature Selection](http://rehansaeed.com/dotnet-new-feature-selection/) by @RehanSaeed + - [Building a Solution with dotnet cli nuget templates](https://motowilliams.com/2017-02-21-building-a-solution-with-dotnet-cli-nuget-templates/) by @motowilliams +- [Building And Debugging The dotnet new Templating Engine In Visual Studio](http://pioneercode.com/post/building-and-debugging-the-dot-net-new-templating-engine-in-visual-studio) by @chad-ramos +- [How To Create A dotnet new Project Template In .NET Core](http://pioneercode.com/post/how-to-create-a-dot-net-new-project-template-in-dot-net-core) - @chad-ramos \ No newline at end of file diff --git a/documentation/TemplateEngine/Conditional-processing-and-comment-syntax.md b/documentation/TemplateEngine/Conditional-processing-and-comment-syntax.md new file mode 100644 index 000000000000..7d5bdab847af --- /dev/null +++ b/documentation/TemplateEngine/Conditional-processing-and-comment-syntax.md @@ -0,0 +1,716 @@ +# Table of contents + +- [Introduction](#introduction) +- [Known Issues](#known-issues) +- [Language Source Files](#language-source-files) + - [Samples](#samples) + - [Ignore conditions expressions in language files](#ignore-conditions-expressions-in-language-files) + - [Related](#related) +- [JSON Files](#json-files) + - [Samples](#samples) + - [Related](#related) +- [XML Files](#xml-files) + - [Samples](#samples) + - [Related](#related) +- [MSBuild Files](#msbuild-files) + - [Samples](#samples) + - [Ignore conditions expressions in MSBuild files](#ignore-conditions-expressions-in-msbuild-files) + - [Related](#related) +- [Single hash line comments](#single-hash-line-comments) + - [Samples](#samples) + - [Related](#related) +- [CSS Files](#css-files) + - [Samples](#samples) + - [Related](#related) +- [Command Files](#command-files) + - [Samples](#samples) + - [Related](#related) +- [Razor Views](#razor-views) + - [Samples](#samples) + - [Related](#related) +- [Haml Files](#haml-files) + - [Samples](#samples) + - [Related](#related) +- [Jsx Files](#jsx-files) + - [Samples](#samples) + - [Related](#related) +- [Other File Types](#other-file-types) + - [Samples](#samples) + - [Related](#related) + + +## Introduction + +To add conditional, or dynamic, content you can add Template Engine expressions in your source files. The conditional expression lets you include or exclude part of the file according to a specified condition, and to do this you can use the familiar conditional expressions like **#if**, **#else**, **#elseif**, **#endif**. + +To learn more about conditional expressions evaluation go to [Conditions](Conditions.md) description. + + +| Name | Description | +|----------|---------------| +|[Language files](#language-source-files)| Common Dotnet language source files.| +|[JSON files](#json-files) | Common Json type files. | +|[XML files](#xml-files) | Common Xml and *tml type files. | +|[MSBuild files](#msbuild-files)| MSBuild project files.| +|[Single hash line comments](#single-hash-line-comments)| Common file types that use single hash line comment syntax.| +|[Css files](#css-files)| Css Files.| +|[Command files](#command-files)| Windows command Files.| +|[Razor Views](#razor-views)| Razor View files.| +|[Haml Files](#haml-files)| Haml files.| +|[JSX Files](#jsx-files)| Jsx and Tsx files.| +|[Other File](#other-file-types)| Default rules for file type not in this list.| + +## Known Issues + +| Name | Description | Incorrect Use | Correct Use | +|----------|---------------|-----------------|----------------| +|[Conditional statement overlaps with Replacement functionality](https://github.com/dotnet/templating/issues/6536)| There is a problem with using different types of conditional comments in replacement statements. In order to workaround it avoid using comments (e.g. "//") as a part of the candidate string for replacing.| "http://localhost:"| "localhost:"| + +#### File Extensions +`.cs`, `.fs`,`.cpp`, `.h`, `.hpp`, `.cake`. + +In these file types you can use a preprocessor directive. + +### Samples + +In this sample, according to the value of the `IndividualB2CAuth` and `OrganizationalAuth` the appropriate service is added. + +``` +#if (IndividualB2CAuth) + services.AddAzureAdB2CBearerAuthentication(); +#elseif (OrganizationalAuth) + services.AddAzureAdBearerAuthentication(); +#endif + +``` + +#### C# Sample + +In this sample, using the parameter symbol `addMethod` we include the definition of a new method, and its use inside the main function. + +```csharp +namespace MyProject.Con +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); +#if( addMethod ) + HelloWordAgain(); +#endif + } + +#if( addMethod ) + static void HelloWordAgain() { + Console.WriteLine("Hello World Again!"); + } +#endif + } +} +``` + +#### C++ Sample + +In this sample, using the parameter symbol `addMethod` we include the definition of a new class, and its use inside the main function. + +```cpp +#include "stdafx.h" +#include +#include + +using namespace std; +#if( addMethod ) + +class HelloClass +{ +public: + HelloClass() { + } + ~HelloClass() + { + } + + void HelloWordAgain() { + cout << "Hello World Again!"; + } +}; +#endif + +int main() +{ + cout << "Hello World!"; + +#if( addMethod ) + HelloClass *hello = new HelloClass(); + hello->HelloWordAgain(); +#endif + + return 0; +} +``` + +#### F# Sample + +In this sample, using the parameter symbol `addMethod` we include the definition of a new method, and its use inside the main function. + +```fsharp +open System + +#if( addMethod ) + +let helloWorldAgain = + printfn "HelloAgain" +#endif + +[] +let main argv = + printfn "Hello World from F#!" +#if( addMethod ) + helloWorldAgain +#endif + 0 // return an integer exit code + +``` + +#### File Extensions + +`.vb`. + +For Visual Basic file the expressions must be preceded by a `'` comment and conditional expressions are **#If**, **#ElseIf**, **#Else**, **#End If**. + +#### Visual Basic Sample + +In this sample, using the parameter symbol `addMethod` we include the definition of a new method, and its use inside the main function. + +```vb +Module Module1 + + Sub Main() + Console.WriteLine("Hello World!") +'#If( addMethod ) + HelloWorldAgain() +'#End If + End Sub + +'#If( addMethod ) + Sub HelloWorldAgain() + Console.WriteLine("Hello World Again!") + End Sub +'#End If +End Module +``` + +#### File Extensions + +`.js`,`.ts`. + +With these file types the expressions must be preceded by a `//` comment . + +#### JavaScript Sample + +In this sample, using the parameter symbol `addMethod` we include the definition of a new method, and its use inside the main function. + +```javascript +(function () { + +//#if( addMethod ) + function helloWorldAgain() { + console.log("Hello World Again!"); + } +//#endif + + console.log("Hello World!"); +//#if( addMethod ) + helloWorldAgain(); +//#endif + +})(); +``` + +#### TypeScript Sample + +In this sample, using the parameter symbol `addMethod` we include the definition of a new method, and its use inside the constructor. + +```typescript +class Student { + + constructor() { + console.log("Hello World!"); +//#if( addMethod ) + this.helloWorldAgain(); +//#endif + } + +//#if( addMethod ) + public helloWorldAgain(): void { + console.log("Hello World Again!"); + } +//#endif +} +``` + +### Ignore conditions expressions in language files +Template engine attempts to process all conditional statements in language files. In case conditions should not be processed by template engine, this should be explicitly specified. + +There are two ways to do it: +- disable processing for the whole file +- disable processing for the part of the file + +If the file should never be processed by template engine, it can be specified as `copyOnly` in the `sources` section of the `template.json`. For example: + +```json +"sources": [ + { + "modifiers": [ + { + "copyOnly": [ "Directory.Build.props" ] + } + ] + } +], +``` + +If template engine should not process only part of the file, but other parts should be processed, the conditional processing can be turned off for the section that should not be processed by using directives: + +`//-:cnd:noEmit` + +`//+:cnd:noEmit` + +The part `//` is the prefix to use for turning operations on and off in language files. The name `cnd` is the name of the operation to turn on/off. The `-` and `+` near the beginning of these lines indicate that the operation should be turned off, and on, respectively. The `noEmit` part tells templating to not process this line. + +For example: + +``` +#if DEBUG +Comet.Reload.Init(); +#endif +//-:cnd:noEmit +#if DEBUG +Xamarin.Calabash.Start(); +#endif +//+:cnd:noEmit +``` +When invoking the template with the above content, the output looks like this: +``` +#if DEBUG +Xamarin.Calabash.Start(); +#endif +``` +The first expression is not emitted because it is processed, and the condition evaluated to `false`. The second expression is copied as-is because the conditional processing is turned off for that part of the file. + +### Related +[C# Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cs) +[C++ Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cpp) +[F# Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.fs) +[VB Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.vb) +[JavaScript Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.js) +[TypeScript Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.ts) + +## JSON Files + +For .json files, the comment block starts with `//` or `////` to the end of the line. After this marker you can add the conditional expressions. +The choice between `//` or `////` is very important because it lets you choose between different actions to be executed when the condition is met: in the first case the content in the expression is simply rendered into the output files as is, while in the second a different action can be executed, by default uncommenting removing an eventual leading `//`. + +#### File Extensions + +`.json`, `.jsonld`, `.hjson`, `.json5`, `.geojson`, `.topojson`, `.bowerrc`, `.npmrc`, `.job`, `.postcssrc`, `.babelrc`, `.csslintrc`, `.eslintrc`, `.jade-lintrc`, `.pug-lintrc`, `.jshintrc`, `.stylelintrc`, `.yarnrc`. + +### Samples + +In this sample, we see the difference between the two ways to define conditional expression. +If the initial `#if` condition is preceded by `//` so, if `param1 == true`, rows below are copied as is. +second condition `#elseif`, is preceded by `////`, so if `param1 == false` and `param2 == true`, rows below will be copied after the leading `//` has been removed, resulting in + +```jsonc +// comment related to the 'elseif' content +content for when param2 is true and param1 is false +``` + +the latest condition, `#else`, is preceded by `////`, so the comments will removed, resulting in + +```jsonc +// comment related to the 'else' content +content for when both param1 & param2 are false +``` +Changing from `////#else` to `//#else` the result will be + +```jsonc +//// comment related to the 'else' content +// content for when both param1 & param2 are false +``` + +```jsonc +//#if (param1) + // comment related to the 'if' content + default content // also appropriate if param1 is true +////#elseif (param2) + //// comment related to the 'elseif' content + //content for when param2 is true and param1 is false +////#else + //// comment related to the 'else' content + // content for when both param1 & param2 are false +//#endif +``` + +### Related +[Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.json) + +## XML Files + +#### File Extensions + +`.*htm`, `.*html`, `.jsp`, `.asp`, `.aspx`, `.nuspec`, `.xslt`, `.xsd`, `.vsixmanifest`, `.vsct`, `.storyboard`, `.axml`, `.plist`, `.xib`, `.strings`, `.xml`, `.xaml`, `.axaml`, `.md`, `.appxmanifest`. + +#### Well known XML file names + +`app.config`, `web.config`, `web.\*.config`, `packages.config`, `nuget.config`. + +the comment block starts with ``. Inside this block you can add your conditional expressions. + +### Samples + +In this sample, conditional expression is inside the comment `` in the same line. According to the value of the `IndividualLocalAuth` and `UseLocalDB` symbol, an element is added. + +```xml + + true + +``` + +In this sample, conditional expression is inside the comment `` in a block. + +```xml + +``` + +### Related +[Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.xml) + +## MSBuild Files + +#### File Extensions + +`.*proj`, `.proj.user`, `.msbuild`, `.targets`, `.props` + +MSBuild files, in addition to the `#if`, `#else`, `#elseif`, `#endif` inside an xml type comment, a **Condition** expression could be added to an element. + +### Samples + +In this sample, one **** element will be added according to the value of the **TargetFrameworkOverride** symbol. This syntax is more clear and lets you use a single conditional expression to rule the inclusion of a whole block of content inside the template + +```xml +netcoreapp2.0 +TargetFrameworkOverride +``` + +In this sample, we can see that if the **TargetFrameworkOverride** symbol is defined all the package references are added to the project file. + +```xml + + + + + +``` + +In this snippet of MSBuild file, we see usage of conditional expression embedded inside a xml comment. + +```xml + + + + + +``` + +### Ignore conditions expressions in MSBuild files + + Template engine attempts to process all conditional statements in MSBuild files. In case conditions should not be processed by template engine, this should be explicitly specified. + +There are two ways to do it: +- disable processing for the whole file +- disable processing for the part of the file + +If the file should never be processed by template engine, it can be specified as `copyOnly` in the `sources` section of the `template.json`. For example: + +```json + "sources": [ + { + "modifiers": [ + { + "copyOnly": [ "Directory.Build.props" ] + } + ] + } + ], +``` + +If template engine should not process only part of the file, but other parts should be processed, the conditional processing can be turned off for the section that should not be processed by using directives: + +`` + +`` + +The part ` + Bar + + + + + Bar + + +``` +When invoking the template with the above content in `Directory.Build.props`, the output looks like this: +``` + + + Bar + + + + + +``` +The first ` AzureAdB2COptions +#endif *@ +``` + +### Related + +[Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cshtml) + +## Haml Files + +#### File Extensions +`.haml` + +The comment block starts with `-#` to the end of the line. After this marker you can add the conditional expressions. + +### Samples + +In this sample, according to the value of the `addParagraph` symbol, a paragraph is added. + +```haml +-##if addParagraph + %p A new paragraph is added. +-##endif +``` + +### Related +[Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.haml) + +## Jsx Files + +#### File Extensions + +`.jsx `,`.tsx` + +The comment block starts with `{/*` and ends with `*/}`. Inside this block you can add your conditional expressions. + +### Samples + +In this sample, according to the value of the `addParagraph` symbol, a paragraph is added. + +```jsx +const myElement = ( +
+ {/*#if addParagraph +

A new paragraph is added

+ #endif*/} +

I am a paragraph.

+
+ ); +``` + +### Related +[Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.jsx) + +## Other File Types + +The file types that do not have special configuration predefined in template engine or `template.json` configuration will follow default settings. +The comment block starts with `//` to the end of the line. After this marker you can add the conditional expressions using `#if`, `#elseif`, `#endif`, `#else` directives. + +### Samples + +In this sample, according to the value of the boolean `param1` symbol, `option1` is added to the file. +```jsx +//#if (param1) +option1 +//#endif +option2 +``` + +### Related +[Sample](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.othertype) diff --git a/documentation/TemplateEngine/Conditions.md b/documentation/TemplateEngine/Conditions.md new file mode 100644 index 000000000000..5e919652d824 --- /dev/null +++ b/documentation/TemplateEngine/Conditions.md @@ -0,0 +1,256 @@ +# Conditions + +## Table of contents + +* [Overview](#overview) + * [Generated Conditions](#generated-conditions) + * [Example](#example) +* [Choice symbols](#choice-symbols) + * [Quoteless literals](#quoteless-literals) + * [Multichoice symbols](#multi-choice-symbols) + * [Using Computed Conditions to work with Multichoice Symbols](#using-computed-conditions-to-work-with-multi-choice-symbols) +* [Conditional parameters](#conditional-parameters) + * [Evaluation](#evaluation) + * [Performing evaluation externally](#performing-evaluation-externally) + +## Overview + +Conditions are used to drive [dynamic content generating or replacing](Conditional-processing-and-comment-syntax.md). + +Conditions use C++ style of [conditional preprocessor expressions](https://docs.microsoft.com/en-us/cpp/preprocessor/hash-if-hash-elif-hash-else-and-hash-endif-directives-c-cpp?view=msvc-170). Expressions are composed from constant literals (strings, numbers, `true`, `false`), [operators](https://github.com/dotnet/templating/blob/main/src/Microsoft.TemplateEngine.Core/Expressions/Cpp/Operator.cs), [symbols](https://github.com/dotnet/templating/blob/main/docs/Available-Symbols-Generators.md), brackets and whitespaces. Only single line expressions are supported. Boolean and numerical expressions are supported (nonzero value is interpreted as `true`) + +[Sample conditions in source code](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.CStyleEvaluator.cs) + +### Generated Conditions +Unlike C++ preprocessor conditions, template engine allows ability for using conditional expressions that are based on results of other expressions. Specifically [Evaluate](Available-Symbols-Generators.md#evaluate) and [Computed](Reference-for-template.json.md#computed-symbol) symbols can be leveraged for this purpose. + +### Example +(other related sample in [GeneratorTest.json](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/GeneratorTest.json#L82-L84)): + +`template.json`: +```json +"symbols":{ + "langVersion": { + "type": "parameter", + "datatype": "text", + "description": "Sets the LangVersion property in the created project file", + "defaultValue": "", + "replaces": "$(ProjectLanguageVersion)", + "displayName": "Language version" + }, + "csharp10orLater": { + "type": "generated", + "generator": "regexMatch", + "datatype": "bool", + "parameters": { + "pattern": "^(|10\\.0|10|preview|latest|default|latestMajor)$", + "source": "langVersion" + } + }, + "csharpFeature_ImplicitUsings": { + "type": "computed", + "value": "csharp10orLater == \"true\"" + }, +} +``` + +`Program.cs`: +```C# +#if (!csharpFeature_ImplicitUsings) +using System; +#endif +``` + +## Choice symbols + +### Quoteless literals + +[Choice Symbol](Reference-for-template.json.md#examples) can have one of N predefined values. Those predefined values can be referenced in the conditions as quoted literals. Unquoted literals are as well supported as opt-in feature via [`enableQuotelessLiterals`](Reference-for-template.json.md#enableQuotelessLiterals). Following 2 expressions are equivalent when opted in: + +`#if (PLATFORM == "Windows")` + +`#if (PLATFORM == Windows)` + +This allows for easier authoring of nested generated conditions. + +### Multi-choice symbols + +Information about multi-choice symbols can be found in [Reference for `template.json`](Reference-for-template.json.md#multichoice-symbols-specifics) + +Comparison to multi-choice symbol results in operation checking of a presence of any value of a multi-choice parameter - meaning `==` operator behaves as `contains()` operation. Example (sourced from [integration test](https://github.com/dotnet/templating/tree/main/test/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultiValueChoice)): + +`template.json`: +```json + "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" + } +} +``` + +`Program.cs`: +```C# +#if (Platform == MacOS) +// MacOS choice flag specified here +#endif +``` + +In above example if `Platform` has it's default value (`MacOS` and `iOS`) or if those 2 values are passed to the engine (e.g. via command line: `dotnet new MyTemplate --Platform MacOS --Platform iOS`), the condition in `Program.cs` file will be evaluated as true. + +Order of operands doesn't matter - `PLATFORM == Windows` evaluates identical as `Windows == PLATFORM`. Comparing 2 multi-choice symbols leads to standard equality check + +### Using Computed Conditions to work with multi-choice symbols + +Cases that needs evaluation of different type of condition over multi-choice symbols than 'contains' (e.g. exclusive equality or membership in subset of possible values) can be achieved with slightly more involved condition - so we recommend definition of aliases via computed conditions. + +#### Example: + +Lets consider following multi-choice symbol: + +`template.json`: +```json + "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": "WindowsPhone|iOS|android" + } +} +``` + +Then Checking whether platform is a mobile platform can be performed with following condition: `(PLATFORM == android || PLATFORM == iOS || PLATFORM == WindowsPhone) && PLATFORM != Windows && PLATFORM != MacOS && PLATFORM != nix` + +Checking for one and only one platform needs similarly involved condition: `PLATFORM == android && PLATFORM != iOS && PLATFORM != WindowsPhone && PLATFORM != Windows && PLATFORM != MacOS` + +This is given by the fact that we do not support exclusive equality operator (in the future, if needed, we can introduce dedicated operator for that - e.g. `===`). + +To simplify templates and make them more readable - following computed conditions can be defined: + +`template.json`: +```json + "symbols": { + "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" + }, +} +``` + +Usage can then look as following: + +`Program.cs` +```C# +#if IsAndroidOnly +// This renders for android only +#elseif IsMobile +// This renders for rest of mobile platforms +#else +// This renders for desktop platforms +#endif +``` + +## Conditional Parameters + +[Parameter symbols in template](Reference-for-template.json.md#parameter-symbol) can be specified together with optional conditions: +* [`IsEnabled Condition`](Reference-for-template.json.md#isEnabled) - Used to determine when (or if) this symbol should be used. If this condition is specified and evaluates to `false` (or a `false` constant is passed), then this parameter is treated as if it does not exist. This applies to the use of [conditional processing of sources](Conditional-processing-and-comment-syntax.md) and [replacements](Reference-for-template.json.md#replaces). [Verification of mandatory parameters](Reference-for-template.json.md#isRequired) does not consider disabled parameters (even if marked as required). +* [`IsRequired Condition`](Reference-for-template.json.md#isRequired) - defines if parameter is required or optional. + +### Evaluation + +**Input** - currently only other parameter symbols from the template configuration are supported within the parameter conditions. Any other variables are not bound and replaced (they are considered part of literal string). + +**Evaluation order** - Dependencies between parameters are detected and evaluation is performed in order that guarantees that all dependencies are evaluated prior their dependant (see [Topological Sorting](https://en.wikipedia.org/wiki/Topological_sorting) for details). + + In case of cyclic dependency the evaluation proceeds only if current input values of parameters do not lead to nondeterministic result (and the cycle is indicated in warning log message). That means order of evaluation or number of reevaluations should not have impact on the result of evaluation. Otherwise an error is reported, indicating the cycle. + + Example `template.json` with cyclic dependency: +```json + "symbols": { + "A": { + "type": "parameter", + "datatype": "bool", + "isEnabled": "B != false", + "defaultValue": false + }, + "B": { + "type": "parameter", + "datatype": "bool", + "isEnabled": "A != true", + "defaultValue": true + } +} +``` + +The following input parameter values can (and will) be evaluated deterministically: `--A false --B true` + +The following input parameter values cannot be evaluated deterministically (and will lead to error): `--A true --B false` + +**Applying user, host and default values** - All user-provided, host-provided and default values are applied before the conditions are evaluated. After the evaluation default values are reapplied to parameters that were evaluated as optional and that do not have user-/host-provided values. After this an evaluation of presence of required values takes place. + +### Performing evaluation externally + +Users of Edge API can supply evaluation results of parameters conditions when instantiating template via `TemplateCreator`. More details are in [Inside the Template Engine](api/Inside-the-Template-Engine.md#supplying-parameters-conditions-results) document. diff --git a/documentation/TemplateEngine/Constraints.md b/documentation/TemplateEngine/Constraints.md new file mode 100644 index 000000000000..b710d99e3e3d --- /dev/null +++ b/documentation/TemplateEngine/Constraints.md @@ -0,0 +1,246 @@ +# Constraints + +| Constraint | `type` | +|:-----|----------| +| [Operating system](#operating-system) | `os` | +| [Running template engine host](#template-engine-host) | `host` | +| [Installed workloads](#installed-workloads) | `workload` | +| [Current SDK version](#current-sdk-version) | `sdk-version` | +| [Project capabilities](#project-capabilities) | `project-capability` | + +The feature is available since .NET SDK 7.0.100. +The template may define constraints, all of which must be met in order for the template to be usable. In case constraints are not met, the template will be installed, however will not be visible nor used by default. + + +# Base configuration +The constraints are defined under `constraints` top-level property in `template.json`. `constraints` contains objects (constraint definition). Each constraint should have a unique name, and the following properties: +- `type`: (string) - constraint type (mandatory) +- `args`: (string, array, object) - constraint arguments - depend on actual constraint implementation. May be optional. + +Example `template.json`: +```json +} + // other template elements + + "constraints": { + "windows-only": { // Custom name - not validated + "type": "os", // Type of the constraint - used to match to proper Constraint component to evaluate the constraint + "args": "Windows" // Arguments passed to the evaluating constraint component + } + } +} +``` + +## Operating system + +Restrict the template instantiation to certain operating system. + +**Configuration:** + + - `type`: `os` + - `args`: (string, array) - list of supported operating systems. Possible values are: `Windows`, `Linux`, `OSX`. + +**Supported in:** + - all hosts (by default). 3rd party host may explicitly disable the constraint. + + +### Examples + +```json +"constraints": { + "linux-only": { + "type": "os", + "args": "Linux" + }, +} +``` +```json +"constraints": { + "linux-and-osx": { + "type": "os", + "args": [ "Linux", "OSX" ] + }, +} +``` + +## Template engine host +Restrict template to be run only in certain application and its version. +The following host identifiers are available: +- `dotnetcli` - .NET SDK +- `vs` - Visual Studio +- `vs-mac` - Visual Studio for Mac +- `ide` - may refer to both Visual Studio and Visual Studio for Mac +- `dotnetcli-preview` - `dotnet new3` command (used for debugging testing) + +3rd party applications may define other host identifiers. + +**Configuration:** + +- `type`: `host` +- `args` (array). Mandatory. Array elements can have following properties: + - `hostname`: (string) - the host identifier (see above) + - `version`: (string, optional) - supported version, or version range. If not specified, all the versions are supported. + +**Supported in**: + - all hosts (by default). 3rd party host may explicitly disable the constraint. + +The version and version range syntax is explained [here](https://docs.microsoft.com/en-us/nuget/concepts/package-versioning). + +The parsing is done in following order: +- exact version syntax (`1.0.0`) +- floating version syntax (`1.*`) +- version range syntax (`(1.0,)`) + +### Examples + +Supported on .NET SDK 6. +```json +"constraints": { + "sdk-only": { + "type": "host", + "args": [ + { + "hostname": "dotnetcli", + "version": "6.0.*" + } + ] + }, +} +``` + +Supported from .NET SDK 6. +```json +"constraints": { + "sdk-only": { + "type": "host", + "args": [ + { + "hostname": "dotnetcli", + "version": "[6.0,)" + } + ] + }, +} +``` + +Supported in .NET SDK and Visual Studio +```json +"constraints": { + "sdk-only": { + "type": "host", + "args": [ + { + "hostname": "dotnetcli" + }, + { + "hostname": "vs" + }, + ] + }, +} +``` + +## Installed Workloads +Defines applicability of a template in the dotnet runtime with specific [workloads](https://github.com/dotnet/designs/blob/main/accepted/2020/workloads/workloads.md) installed. +All the installed (queryable via `dotnet workload list`) as well as [extended](https://github.com/dotnet/designs/blob/main/accepted/2020/workloads/workload-manifest.md#workload-composition) workloads are inspected during evaluating of this constraint. + +**Configuration:** + +- `type`: `workload` +- `args` (string, array). Mandatory. List of names of supported workloads (running host need to have at least one of the requested workloads installed). + + To see the list of installed workloads run [`dotnet workload list`](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-list). To see the list of available workloads run [`dotnet workload search`](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-search) + +**Supported in**: + - .NET SDK CLI (`dotnet new`) + - Visual Studio 17.4 + +### Examples + +```json +"constraints": { + "android-runtime": { + "type": "workload", + "args": [ "microsoft-net-runtime-android", "microsoft-net-runtime-android-aot" ] + }, +} +``` +```json +"constraints": { + "web-assembly": { + "type": "workload", + "args": "wasm-tools" + }, +} +``` + +## Current SDK Version +Defines .NET SDK version(s) the template can be used on. + +Only the currently active SDK (queryable via `dotnet --version`, changeable by the [`global.json`](https://docs.microsoft.com/en-us/dotnet/core/tools/global-json)) is being considered. Other available SDKs (queryable via `dotnet --list-sdks`) are checked for possible match and result is reported in the evaluation output with possible remedy steps - the form of reporting is dependent on the templating host. + +**Configuration:** + +- `type`: `sdk-version` +- `args` (string, array). List of versions supported by the template. Syntax and match evaluation of versions are identical as in the [Template engine host](#template-engine-host) constraint. + +**Supported in**: + - .NET SDK CLI (`dotnet new`) + - Visual Studio 17.4 + +### Examples + +```json +"constraints": { + "LTS ": { + "type": "sdk-version", + "args": [ "6.0.*", "3.1.*" ] + }, +} +``` +```json +"current-with-previews": { + "web-assembly": { + "type": "sdk-version", + "args": "7.*.*-*" + }, +} +``` +## Project capabilities +Defines [project capabilities](https://github.com/microsoft/VSProjectSystem/blob/master/doc/overview/about_project_capabilities.md) that the template requires. +Commonly used with item templates to define the certain projects it is applicable to. +`dotnet new` attempts to find the closest project file using following rules: +- The project in current directory or `--output` directory (matching `*.*proj` extension). +- If not found, the parent of above and so on. +- The path to the project can be explicitly specified using `--project` instantiation option. This path takes precedence - so it can be used in case of ambiguity. + +Once project is located, its project capabilities are evaluated. The project should be restored, otherwise evaluation fails. +Only .NET [SDK-style projects](https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview) are supported. + +**Configuration:** + +- `type`: `project-capability` +- `args`: (string) project capability [expression](https://docs.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.shell.interop.vsprojectcapabilityexpressionmatcher) that should be satisfied by the project. + +**Supported in**: + - .NET SDK CLI (`dotnet new`) + - Visual Studio 17.4 Preview 3 + +### Examples + +```json +"constraints": { + "CSharp": { + "type": "project-capability", + "args": "CSharp", + }, +} +``` +```json +"constraints": { + "CSharpTest": { + "type": "project-capability", + "args": "CSharp & TestContainer", + }, +} +``` \ No newline at end of file diff --git a/documentation/TemplateEngine/Exit-Codes.md b/documentation/TemplateEngine/Exit-Codes.md new file mode 100644 index 000000000000..2705494dad94 --- /dev/null +++ b/documentation/TemplateEngine/Exit-Codes.md @@ -0,0 +1,268 @@ +# `dotnet new` exit codes and their meaning + +Exit codes are chosen to conform to existing standards or standardization attempts and well known exit code. See [Related resources](#related) for more details + +| Exit Code | Reason | +|:-----|----------| +| 0 | Success | +| [70](#70) | Unexpected internal software issue. | +| [73](#73) | Can't create output file. | +| [100](#100) | Instantiation Failed - Processing issues. | +| [101](#101) | Invalid template or template package. | +| [102](#102) | Missing required option(s) and/or argument(s) for the command. | +| [103](#103) | The template or the template package was not found. | +| [104](#104) | PostAction operation was cancelled. | +| [105](#105) | Instantiation Failed - Post action failed. | +| [106](#106) | Template/Package management operation Failed. | +| [107 - 113](#107) | Reserved. | +| [127](#127) | Unrecognized option(s) and/or argument(s) for a command. | +| [130](#130) | Command terminated by user. | + + +To enable verbose logging in order to troubleshoot issue(s), set the `DOTNET_CLI_CONTEXT_VERBOSE` environment variable to `true` + +_PowerShell:_ +```PowerShell +$env:DOTNET_CLI_CONTEXT_VERBOSE = 'true' +``` + +_Cmd:_ +```cmd +set DOTNET_CLI_CONTEXT_VERBOSE=true +``` + +## 70 - Unexpected internal software issue + +Unexpected result or issue. [File a bug](https://github.com/dotnet/templating/issues/new?title=Unexpected%20Internal%20Software%20Issue%20(EX_SOFTWARE)) if you encounter this exit code. + +This is a semi-standardized exit code (see [EX_SOFTWARE in /usr/include/sysexits.h](https://github.com/openbsd/src/blob/master/include/sysexits.h#L107)) + + +## 73 - Can't create output file. + +The operation was cancelled due to detection of an attempt to perform destructive changes to existing files. This can happen if you are attempting to instantiate template into the same folder where it was previously instantiated under same target name (specified via `--name` option or defaults to the target directory name) + +_Example:_ +```console +> dotnet new console + +The template "Console App" was created successfully. + +Processing post-creation actions... +Running 'dotnet restore' on C:\tmp\tmp.csproj... + Determining projects to restore... + Restored C:\tmp\tmp.csproj (in 47 ms). +Restore succeeded. + +> dotnet new console + +Creating this template will make changes to existing files: + Overwrite ./tmp.csproj + Overwrite ./Program.cs + +Rerun the command and pass --force to accept and create. + +For details on current exit code please visit https://aka.ms/templating-exit-codes#73 +``` + +Destructive changes can be forced by passing `--force` option. + +This is a semi-standardized exit code (see [EX_CANTCREAT in /usr/include/sysexits.h](https://github.com/openbsd/src/blob/master/include/sysexits.h#L110)) + + +## 100 - Instantiation Failed - Processing issues + +The template instantiation failed due to error(s). Caused by environment (failure to read/write template(s) or cache). + +## 101 - Invalid template or template package + +_Reserved for future usage - described behavior is yet not implemented. [Feature is tracked](https://github.com/dotnet/templating/issues/4801)_ + +Caused by erroneous template(s) (incomplete conditions, symbols or macros etc.). Exact error reason will be output to stderr. + +_Examples:_ + +Missing mandatory properties in template.json +```json +{ + "author": "John Doe", + "name": "name", +} +``` + + +## 102 - Missing required option(s) and/or argument(s) for the command + +_Reserved for future usage - described behavior is only partially implemented. Some cases that should fall under this exit code are now leading to code [127](#127) [Issue is tracked](https://github.com/dotnet/templating/issues/4806)_ + +The exit code is used when one or more required options or/and arguments used for the command were not passed. Applicable to `search` command with not enough information as well. + +Applicable as well if template option [marked as required](Reference-for-template.json.md#isrequired) was not passed during the template instantiation. + +_Examples:_ +```console +> dotnet new my-template +Mandatory option '--MyMandatoryParam' is missing for the template 'My Template'. + +For details on current exit code please visit https://aka.ms/templating-exit-codes#102 +``` + +```console +> dotnet new search +Search failed: not enough information specified for search. +To search for templates, specify partial template name or use one of the supported filters: '--author', '--baseline', '--language', '--type', '--tag', '--package'. +Examples: + dotnet new search web + dotnet new search --author Microsoft + dotnet new search web --language C# + +For details on current exit code please visit https://aka.ms/templating-exit-codes#102 +``` + + +## 103 - The template or the template package was not found + +Applicable to instantiation, listing, remote sources searching and installation. + +_Examples:_ +```console +> dotnet new xyz +No templates found matching: 'xyz'. + +To list installed templates, run: + dotnet new list +To search for the templates on NuGet.org, run: + dotnet new search xyz + +For details on current exit code please visit https://aka.ms/templating-exit-codes#103 +``` + +```console +> dotnet new list xyz +No templates found matching: 'xyz'. + +To search for the templates on NuGet.org, run: + dotnet new search xyz + +For details on current exit code please visit https://aka.ms/templating-exit-codes#103 +``` + +```console +> dotnet new search xyz +Searching for the templates... +Matches from template source: NuGet.org +No templates found matching: 'xyz'. + +For details on current exit code please visit https://aka.ms/templating-exit-codes#103 +``` + +```console +> dotnet new install foobarbaz +The following template packages will be installed: + foobarbaz + +foobarbaz could not be installed, no NuGet feeds are configured or they are invalid. + +For details on current exit code please visit https://aka.ms/templating-exit-codes#103 +``` + +## 104 - Post action operation was cancelled + +Applicable to a case when user aborts run-script post action. + + +## 105 - Instantiation Failed - Post action failed + +Applicable to a case when post action fails - unless it is configured to [continue on errors](Post-Action-Registry.md#continueOnError). + +## 106 - Template/Package management operation failed + +The exit code is used for errors during templates installation, uninstallation or updates. +Failure to download packages, read/write templates or cache, erroneous or corrupted template, or an attempt to install same package multiple times. + +_Example:_ +```console +>dotnet nuget disable source nuget.org +Package source with Name: nuget.org disabled successfully. + +> dotnet new install webapi2 +The following template packages will be installed: + webapi2 + +Error: No NuGet sources are defined or enabled. +webapi2 could not be installed, the package does not exist. + +For details on current exit code please visit https://aka.ms/templating-exit-codes#106 +``` + +## 107 - 113 + +Reserved for future use. + +[File a bug](https://github.com/dotnet/templating/issues/new?title=Unexpected%20Exit%20Code) if you encounter any of these exit codes. + + +## 127 - Unrecognized option(s) and/or argument(s) for a command + +The exit code is used when one or more options or/and arguments used in the command not recognized or invalid. + +Usually a mismatch in type of the specified template option or unrecognized choice value. + +_Examples:_ + +```console +> dotnet new console --framework xyz +Error: Invalid option(s): +--framework xyz + 'xyz' is not a valid value for --framework. The possible values are: + net6.0 - Target net6.0 + net7.0 - Target net7.0 + +For details on current exit code please visit https://aka.ms/templating-exit-codes#127 +``` + +```console +dotnet new update --smth +Unrecognized command or argument '--smth' + + + +Description: +Checks the currently installed template packages for update, and install the updates. + + + +Usage: +dotnet new update [options] + + + +Options: +--interactive Allows the command to stop and wait for user input or action (for +example to complete authentication). +--add-source, --nuget-source Specifies a NuGet source to use during install. +--check-only, --dry-run Only check for updates and display the template packages to be updated +without applying update. +-?, -h, --help Show command line help. + +For details on current exit code please visit https://aka.ms/templating-exit-codes#127 +``` + +This is a semi-standardized exit code (see [127 - "command not found" in 'The Linux Documentation Project'](https://tldp.org/LDP/abs/html/exitcodes.html)) + + +## 130 - Command terminated by user. + +_Reserved for future usage - described behavior is yet not implemented. [Feature is tracked](https://github.com/dotnet/templating/issues/4799)_ + +The exit code is used if command is terminated after user non-forceful termination request (e.g. `Ctrl-C`, `Ctrl-Break`). + +This is a semi-standardized exit code (see [130 - Script terminated by Control-C in 'The Linux Documentation Project'](https://tldp.org/LDP/abs/html/exitcodes.html)) + +
+
+
+ +### Related Resources +* [`BSD sysexit.h`](https://github.com/openbsd/src/blob/master/include/sysexits.h) +* [`Special exit codes - The Linux Documentation Project`](https://tldp.org/LDP/abs/html/exitcodes.html) diff --git a/documentation/TemplateEngine/Home.md b/documentation/TemplateEngine/Home.md new file mode 100644 index 000000000000..cbd3ffe0d379 --- /dev/null +++ b/documentation/TemplateEngine/Home.md @@ -0,0 +1,20 @@ +# Environments using Template Engine +- `dotnet new` +- Visual Studio +- Visual Studio for Mac + +# Community templates +- See [available templates for dotnet new](https://github.com/dotnet/templating/wiki/Available-templates-for-dotnet-new) + +# Template authoring +- [How to create templates](https://docs.microsoft.com/dotnet/core/tools/custom-templates) +- [Samples repository](https://github.com/dotnet/dotnet-template-samples) +- [Reference for `template.json`](Reference-for-template.json.md) +- [Post actions](Post-Action-Registry.md) +- [Symbols Generators](Available-Symbols-Generators.md) +- [Value Forms](Value-Forms.md) +- [Conditional content generation](Conditional-processing-and-comment-syntax.md) +- [Template constraints](Constraints.md) +- [Binding and project context evaluation](Binding-and-project-context-evaluation.md) +- [Authoring tools: localization](authoring-tools/Localization.md) +- [Authoring tools: testing the templates](authoring-tools/Templates-Testing-Tooling.md) \ No newline at end of file diff --git a/documentation/TemplateEngine/Naming-and-default-value-forms.md b/documentation/TemplateEngine/Naming-and-default-value-forms.md new file mode 100644 index 000000000000..a9e9b4605054 --- /dev/null +++ b/documentation/TemplateEngine/Naming-and-default-value-forms.md @@ -0,0 +1,85 @@ +# `sourceName` default value forms + +`sourceName` defines the name in the source tree to replace with the name the user specifies. The value to be replaced with can be given using the `-n` `--name` options while running a template or Name field in IDE. The template engine will look for any occurrence of the name present in the config file and replace it in file names and file contents. If no name is specified by the host, the current directory is used. The value of the `sourceName` is available in built-in `name` symbol and can be used as the source for creating other symbols and condition expressions. Therefore, it is not allowed to define `name` symbol in `symbols` in template.json configuration. + +When selecting a `sourceName` for a template you're authoring, keep in mind the default value forms applied to this symbol: +- `identity` - the value as entered by user. +- `namespace` - the value transformed in a way to be a correct .NET namespace. [Details](https://github.com/dotnet/templating/blob/b0b1283f8c96be35f1b65d4b0c1ec0534d86fc2f/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueForms/DefaultSafeNamespaceValueFormFactory.cs#L17-L59) +- `class name` - the value transformed in a way to be a correct .NET class name. [Details](https://github.com/dotnet/templating/blob/b0b1283f8c96be35f1b65d4b0c1ec0534d86fc2f/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ValueForms/DefaultSafeNameValueFormFactory.cs#L15-L21) +- `lower case namespace` - same as `namespace`, but lower case. +- `lower case class name` - same as `class name`, but lower case. + +`sourceName` is available in template configuration as `name` variable. + +A good choice for `sourceName` is the value that produces distinct values under the below transformations, for example `Template.1`: +Form | Source | Transformed value +-------|------|-------- +`identity` | `Template.1` | `Template.1` +`namespace` | `Template.1` | `Template._1` +`class name` | `Template.1` | `Template__1` +`lower case namespace` | `Template.1` | `template._1` +`lower case class name`| `Template.1` | `template__1` + +In this case, you can use the transformed value to force using the form, for example: +```csharp +namespace Template._1; //uses `namespace` form + +public class Template__1 //uses `class` form +{ + var str = "My template name is Template.1"; //uses `identity` form +} +``` + +In this case, if user use `My-App` as the name, the generated content will be as follows: +Form | Source | Transformed value +-------|------|-------- +`identity` | ` My-App` | `My-App` +`namespace` | ` My-App` | `My_App` +`class name` | `My-App` | `My_App` +`lower case namespace` | `My-App` | `my_app` +`lower case class name`| `My-App` | `my_app` + +```csharp +namespace My_App; //uses `namespace` form + +public class My_App //uses `class` form +{ + var str = "My template name is My-App"; //uses `identity` form +} +``` + +An example of wrong `sourceName` is `template1`, in this case all form transformations result in `template1`. +Referring to previous example: +```csharp +namespace template1; //intent to use `namespace` form + +public class template1 //intent to use `class` form +{ + var str = "My template name is template1"; //intent to use `identity` form +} +``` + +In this case, if the user use `My-App` as the name, the generated content may be as follows: +```csharp +namespace My-App; //intent to use `namespace` form, but `identity` was used instead. It is not guaranteed which of `My-App`, `My_App`, `my_app` will be used here. + +public class My-App //intent to use `class` form, but `identity` was used instead +{ + var str = "My template name is My_App"; //intent to use `identity` form, but `namespace` was used instead +} +``` +As the result, this code won't compile as namespace and class are not using correct names. + +Besides using the forms in template content, it is possible to access certain form of `sourceName` via the following variables: +- `identity`: `name{-VALUE-FORMS-}identity`, equivalent to `name`. +- `namespace`: `name{-VALUE-FORMS-}safe_namespace` +- `class name`: `name{-VALUE-FORMS-}safe_name` +- `lower case namespace`: `name{-VALUE-FORMS-}lower_safe_namespace` +- `lower case class name`: `name{-VALUE-FORMS-}lower_safe_name` + +You can use them in conditions or other expressions of template configuration. Example: +```xml +Company.ConsoleApplication1 +``` + +For more details on value forms, refer to [the article](Value-Forms.md). \ No newline at end of file diff --git a/documentation/TemplateEngine/Post-Action-Registry.md b/documentation/TemplateEngine/Post-Action-Registry.md new file mode 100644 index 000000000000..370739e931d6 --- /dev/null +++ b/documentation/TemplateEngine/Post-Action-Registry.md @@ -0,0 +1,493 @@ +# Existing Post Actions + +| Description | ActionID | +|:-----|----------| +| [Restore NuGet packages](#restore-nuget-packages) | `210D431B-A78B-4D2F-B762-4ED3E3EA9025` | +| [Run script](#run-script) | `3A7C4B45-1F5D-4A30-959A-51B88E82B5D2` | +| [Open a file in the editor](#open-a-file-in-the-editor) | `84C0DA21-51C8-4541-9940-6CA19AF04EE6` | +| [Add a reference to a project file](#add-a-reference-to-a-project-file) | `B17581D1-C5C9-4489-8F0A-004BE667B814` | +| [Add projects to a solution file](#add-projects-to-a-solution-file) | `D396686C-DE0E-4DE6-906D-291CD29FC5DE` | +| [Change file permissions (Unix/OS X)](#change-file-permissions) | `CB9A6CF3-4F5C-4860-B9D2-03A574959774` | +| [Display manual instructions](#display-manual-instructions) | `AC1156F7-BB77-4DB8-B28F-24EEBCCA1E5C` | +| [Add a property to an existing JSON file](#add-a-property-to-an-existing-json-file) | `695A3659-EB40-4FF5-A6A6-C9C4E629FCB0` | + +# Base configuration +Each post action has set of standard properties as well as custom properties defined by certain post action. +The standard properties are listed below. + - **Configuration** : + - `actionId` (string): Action ID. + - `condition` (string) (optional): A C++ style boolean expression defining if post action should be run. This expression may use any symbols that have been defined. + - `description` (string) (optional): A human-readable description of the action. + - `configFile` (string) (optional): Additional configuration for the associated post action. The structure & content will vary based on the post action. + - `continueOnError` (bool) (optional): If this action fails, the value of continueOnError indicates whether to process the next action, or stop processing the post actions. Should be set to true when subsequent actions rely on the result of the current action. The default value is false. + - `manualInstructions` (array) (optional): An ordered list of possible instructions to display if the action cannot be performed. Each element in the list must contain a key named "text", whose value contains the instructions. Each element may also optionally provide a key named "condition" - a boolean expression. The first instruction with blank condition is considered a default. If true conditions are present, the last one of them will be considered valid, all other ignored. It is recommended not to have more than one true condition at the time. + - `applyFileRenamesToArgs` (array) (optional): A list of arguments names from 'args' to which the file renames configured in symbols should be applied. By default, the file renames are not applied. Available since .NET SDK 8.0.100. + - `applyFileRenamesToManualInstructions` (boolean) (optional): If set to true, the file renames configured in symbols should be applied to manual instructions. By default, the file renames are not applied. Available since .NET SDK 8.0.100. + +# Restore NuGet packages + +Used to restore NuGet packages after project create. + + - **Action ID** : `210D431B-A78B-4D2F-B762-4ED3E3EA9025` + - **Specific Configuration** : + - `args`: + - `files` (string|array) (optional): + - `string`: A semicolon delimited list of files that should be restored. If specified, the primary outputs will be ignored for processing. If not specified, matching primary outputs are restored. + - `array`: An array of files that should be restored. If specified, the primary outputs will be ignored for processing. If not specified, matching primary outputs are restored. + + Note: the file path specified in `files` is used as glob pattern matching relative source path that starts with `./`. If none of the patterns is matched, matching primary outputs are restored. + + Given that relative source paths are: + - ./src/Client/Client.csproj + - ./src/Client/Client.Library.csproj + - ./src/Client/Client.Test.csproj + + The following patterns can be used. + |Description|Glob Pattern| + |-|-| + |Exact path matching single project|`./src/Client/Client.Test.csproj`| + |Wildcard `*` matching multiple projects|`./src/Client/Client.*.csproj`| + |Globstar `**` recursively matching multiple layers of directories|`**/Client.Library.csproj;**/Client.csproj`| + |File name without parent path matching the project with the same name|`Client.Library.csproj`| + + - **Supported in**: + - `dotnet new3` + - `dotnet new` (2.0.0 or higher) + - **Ignored in**: + - `Visual Studio` - Visual Studio restores all projects automatically, so post action will be be ignored. + +Note: when using `files` argument it should contain the path to the file in source template definition, and ignore all the path and filename changes that can happen when instantiating template. For more details, see [the article](Using-Primary-Outputs-for-Post-Actions.md). + +### Example + +Restores the project mentioned in primary outputs: + +``` +"primaryOutputs": [ + { + "path": "MyTestProject.csproj" + } +], +"postActions": [{ + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [{ + "text": "Run 'dotnet restore'" + }], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true +}] +``` + +Restores the files mentioned in `files` argument. The primary outputs will be ignored. + +``` +"primaryOutputs": [ + { + "path": "Primary/Output/PrimaryOutput.csproj" // will not be restored + } +], +"postActions": [{ + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [{ + "text": "Run 'dotnet restore'" + }], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true, + "args": { + "files": ["./Client/Client.csproj", "./Server/Server.csproj"] + } +}] +``` + +If none of files mentioned in `files` argument is matched, the primary outputs will be restored. + +``` +"primaryOutputs": [ + { + "path": "Primary/Output/PrimaryOutput.csproj" // will be restored + } +], +"postActions": [{ + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [{ + "text": "Run 'dotnet restore'" + }], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true, + "args": { + "files": ["Client/Client.csproj"] // This will not match any project because relative source path starts with "./" + } +}] +``` + +# Run script + +Used to run a script after create. + + - **Action ID** : `3A7C4B45-1F5D-4A30-959A-51B88E82B5D2` + - **Specific Configuration** : There are three required properties that must be specified. + - `args` + - `executable` (string): The executable to launch. + - `args` (string): The arguments to pass to the executable. + - `redirectStandardOutput` (bool) (optional): Whether or not to redirect stdout for the process (prevents output from being displayed if true). The default value is true. + - `redirectStandardError` (bool) (optional): Defines whether or not the stderr should be redirected. If the output is redirected, it prevents it from being displayed. The default value is true. Available since .NET SDK 6.0.100. + - `manualInstructions` (required) + - **Supported in**: + - `dotnet new3` + - `dotnet new` (2.0.0 or higher) + +The working directory for the launched executable is set to the root of the output template content. + +### Example + +``` +"postActions": [{ + "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", + "args": { + "executable": "setup.cmd", + "args": "", + "redirectStandardOutput": false, + "redirectStandardError": false + }, + "manualInstructions": [{ + "text": "Run 'setup.cmd'" + }], + "continueOnError": false, + "description ": "setups the project by calling setup.cmd" +}] +``` + +# Open a file in the editor + +Opens a file in the editor. For command line cases this post action will be ignored. + + - **Action ID** : `84C0DA21-51C8-4541-9940-6CA19AF04EE6` + - **Specific Configuration** : + - `files` (string): A semicolon delimited list of indexes to the primary outputs. + Note: If primary outputs are conditional, multiple post actions with the same + conditions as the primary outputs might be necessary. + - **Supported in**: + - since `Visual Studio 2017.3 Preview 1` + +### Example + +``` +"primaryOutputs": [ + { "path": "Company.ClassLibrary1.csproj" }, + { + "condition": "(HostIdentifier != \"dotnetcli\")", + "path": "Class1.cs" + } +], +"postActions": [ + { + "condition": "(HostIdentifier != \"dotnetcli\")", + "description": "Opens Class1.cs in the editor", + "manualInstructions": [ ], + "actionId": "84C0DA21-51C8-4541-9940-6CA19AF04EE6", + "args": { + "files": "1" + }, + "continueOnError": true + } +] +``` + +# Add a reference to a project file + - **Action ID** : `B17581D1-C5C9-4489-8F0A-004BE667B814` + - **Specific Configuration** : + - `args` + - `targetFiles` (string|array) (optional): + - `string`: A semicolon delimited list of files that should be processed. If not specified, the project file in output directory or its closest parent directory will be used. + - `array`: An array of files that should be processed. If not specified, the project file in output directory or its closest parent directory will be used. + - `referenceType` (string): Either "package" or "project". + - `reference` (string): The package ID or relative path of the project to add the reference to. + - `projectFileExtensions` (string) (optional): A semicolon delimited list of literal file extensions to use when searching for the project to add the reference to. If not specified, `*.*proj` mask is used when searching. + - `version` (string) (optional) (package referenceType only): The version of the package to install. + - **Supported in**: + - `dotnet new3` + - `dotnet new` (2.0.0 or higher) + +Note: when using `targetFiles` argument it should contain the path to the file in source template definition, and ignore all the path and filename changes that can happen when instantiating template. For more details, see [the article](Using-Primary-Outputs-for-Post-Actions.md). + +### Example + +Adds a reference `Microsoft.NET.Sdk.Functions` to the project file. + +``` +"postActions": [{ + "Description": "Adding Reference to Microsoft.NET.Sdk.Functions NuGet package", + "ActionId": "B17581D1-C5C9-4489-8F0A-004BE667B814", + "ContinueOnError": "false", + "ManualInstructions": [{ + "Text": "Manually add the reference to Microsoft.NET.Sdk.Functions to your project file" + }], + "args": { + "referenceType": "package", + "reference": "Microsoft.NET.Sdk.Functions", + "version": "1.0.0", + "projectFileExtensions": ".csproj" + } +}] +``` + +Includes a reference to `SomeDependency` into `MyProjectFile`. The referenced project file is in the `SomeDependency` folder. + +``` +"postActions": [{ + "Description": "Adding a reference to another project", + "ActionId": "B17581D1-C5C9-4489-8F0A-004BE667B814", + "ContinueOnError": "false", + "ManualInstructions": [{ + "Text": "Manually add the reference to SomeDependency to MyProjectFile" + }], + "args": { + "targetFiles": ["MyProjectFile.csproj"] + "referenceType": "project", + "reference": "SomeDependency/SomeDependency.csproj" + } +}] +``` + +## Adding references to existing projects + +It is possible to add references to existing projects in your working directory. Since the name of the existing project is likely not a constant value for all template instantiations, a symbol can be used to pass the name of the existing project. + +The example below demonstrates how to add the existing project ```src/AlreadyExisting/AlreadyExisting.csproj``` as a reference to the template source project ```Project1/Project1.csproj```. + +``` +{ + "symbols": { + "existingProject": { + ... + "type": "parameter", + "datatype": "string", + "defaultValue": "ExistingProject/ExistingProject.csproj", + "fileRename": "ExistingProjectPath" // Must be same as targetFile + } + }, + "postActions": [ + { + "Description": "Add ProjectReference to ExistingProject/ExistingProject.csproj", + "applyFileRenamesToArgs": [ + "targetFiles" // Must be specified + ], + "args": { + "targetFiles": [ + "ExistingProjectPath" // Must be same as fileRename + ], + "referenceType": "project", + "reference": "Project1/Project1.csproj" + } + }] +} +``` + +The template above: +- Configures the ```existingProject``` parameter *symbol* with a ```fileRename``` configuration. +- Instructs the *'add reference to a project file'* post action to apply ```fileRename``` to the ```targetFiles``` argument. +- Uses the value passed to the ```existingProject``` *symbol* to replace the value of the matching ```targetFiles```. + +This template can be instantiated using: + +```dotnet new [templateName] --existingProject src/AlreadyExisting/AlreadyExisting.csproj``` + + +# Add project(s) to a solution file + + - **Action ID** : `D396686C-DE0E-4DE6-906D-291CD29FC5DE` + - **Specific Configuration** : + - `args`: + - `projectFiles` (string|array) (optional): + - `string`: A semicolon delimited list of files that should be added to solution. If not specified, primary outputs will be used instead. + - `array`: An array of files that should be added to solution. If not specified, primary outputs will be used instead. + - `primaryOutputIndexes` (string) (optional): A semicolon delimited list of indexes to the primary outputs. If not specified, all primary outputs will be added. Note: If primary outputs are conditional, multiple post actions with the same conditions as the primary outputs might be necessary. + - `solutionFolder` (string) (optional) (supported in 5.0.200 or higher): the destination solution folder path to add the projects to. + - `inRoot` (boolean) (optional) (supported in 7.0.200 or higher): whether to place the projects in the root of the solution, rather than create a solution folder. Cannot be used with `solutionFolder`. + - **Supported in**: + - `dotnet new3` + - `dotnet new` (2.0.0 or higher) + - **Ignored in**: + - `Visual Studio` - the user indicates where to add project explicitly, so post action defined in the template will be ignored. + +Note: when using `projectFiles` argument it should contain the path to the file in source template definition, and ignore all the path and filename changes that can happen when instantiating template. For more details, see [the article](Using-Primary-Outputs-for-Post-Actions.md). + + +### Example + +Adds `MyTestProject.csproj` to solution in output directory or its closest parent directory. + +``` +"primaryOutputs": [ + { + "path": "MyTestProject.csproj" + } +], +"postActions": [{ + "description": "Add projects to solution", + "manualInstructions": [ { "text": "Add generated project to solution manually." } ], + "args": { + "solutionFolder": "src" + }, + "actionId": "D396686C-DE0E-4DE6-906D-291CD29FC5DE", + "continueOnError": true +}] +``` + +Adds `MyTestProject.csproj` to solution in output directory or its closest parent directory (using `projectFiles` argument). + +``` +"postActions": [{ + "description": "Add projects to solution", + "manualInstructions": [ { "text": "Add generated project to solution manually." } ], + "args": { + "solutionFolder": "src", + "projectFiles": ["MyTestProject.csproj"] + }, + "actionId": "D396686C-DE0E-4DE6-906D-291CD29FC5DE", + "continueOnError": true +}] +``` + +Adds `MyTestProject.csproj` in the root of the solution. + +```json +"primaryOutputs": [{ + "path": "MyTestProject.csproj" + } +], +"postActions": [{ + "description": "Add projects to solution", + "manualInstructions": [{ + "text": "Add generated project to solution manually." + } + ], + "args": { + "inRoot": true + }, + "actionId": "D396686C-DE0E-4DE6-906D-291CD29FC5DE", + "continueOnError": true + } +] +``` + +# Change file permissions + +Unix / OS X only (runs the Unix `chmod` command). + + - **Action ID** : `CB9A6CF3-4F5C-4860-B9D2-03A574959774` + - **Specific Configuration** : + - `args`: The permissions to set (see examples). Usually this will contain a glob like `{ "+x": "*.sh" }` or a list of filenames like `{ "+x": ["script1", "script2"] }`. + - **Supported in**: + - `dotnet new3` + - `dotnet new` (2.0.0 or higher) + +### Example + +``` +"postActions": [{ + "condition": "(OS != \"Windows_NT\")", + "description": "Make scripts executable", + "manualInstructions": [{ + "text": "Run 'chmod +x *.sh'" + }], + "actionId": "cb9a6cf3-4f5c-4860-b9d2-03a574959774", + "args": { + "+x": "*.sh" + }, + "continueOnError": true +}] +``` + +or + +``` +"postActions": [{ + "condition": "(OS != \"Windows_NT\")", + "description": "Make scripts executable", + "manualInstructions": [ { "text": "Run 'chmod +x *.sh somethingelse'" } ], + "actionId": "cb9a6cf3-4f5c-4860-b9d2-03a574959774", + "args": { + "+x": [ + "*.sh", + "somethingelse" + ] + }, + "continueOnError": true +}] +``` + +# Display manual instructions + +Prints out the manual instructions after instantiating template in format: +``` +Description: +Manual instructions: +Actual command: +``` + +Command is printed only if defined in post action arguments. + + - **Action ID** : `AC1156F7-BB77-4DB8-B28F-24EEBCCA1E5C` + - **Specific Configuration** : + - `args`: + - `executable` (string) (optional): command to run + - `args` (string) (optional): arguments to use + - **Supported in**: + - `dotnet new3` + - `dotnet new` (2.0.0 or higher) + +### Example + +``` +"postActions": [{ + "description": "Manual actions required", + "manualInstructions": [{ + "text": "Run the following command" + }], + "actionId": "AC1156F7-BB77-4DB8-B28F-24EEBCCA1E5C", + "args": { + "executable": "setup.cmd", + "args": "" + }, + "continueOnError": true +}] +``` + +# Add a property to an existing JSON file + +Adds a new JSON property in an existing JSON file. + +- **Action ID** : 695A3659-EB40-4FF5-A6A6-C9C4E629FCB0 +- **Specific Configuration** : + - `args`: + - `jsonFileName (string)`: The path to the JSON file that must be modified. + - `parentPropertyPath (string)` (optional): Specifies an existing property in the JSON file for which the new property must be a child property. The complete path must be specified, using a colon (:) as a separator character, for instance, `Person:Address`. If parentPropertyPath is not defined, the new property will be added to the root of the JSON document. + - `newJsonPropertyName (string)`: The name that must be given to the new property. + - `newJsonPropertyValue (string)`: The value that must be assigned to the new property. This must be a valid JSON. + - Starting in .NET 10 SDK, the following are also supported: + - `detectRepositoryRoot (bool)`: Whether or not a detection logic to find repo root is enabled. When the JSON file is not found, it will be created in the detected repo root if this option is true. Also when searching for an existing JSON file, the search will stop at the repo root and won't consider any parent directories. The default is `false`. + - `includeAllDirectoriesInSearch (bool)`: Whether or not sub-directories are searched for a matching json file (i.e, use `SearchOption.AllDirectories`). The default is `true`. + - `includeAllParentDirectories (bool)`: Whether or not all parent directories (up to repo root, if `detectRepositoryRoot` is true) are searched for a matching JSON file. When `false`, only one level up is searched. The default is `false`. +- **Supported in** : + - dotnet new3 + - dotnet new (2.0.0 or higher) + +## Example +``` +"postActions": [{ ` + `"description": "Adds a new JSON property in an existing JSON file.",` + `"manualInstructions": [ { "text": "Add a new property 'LogLevel' with value 'Information' to '.\deployment.json' under existing property 'moduleConfiguration:edgeAgent:properties.desired'" } ],` + `"actionId": "695A3659-EB40-4FF5-A6A6-C9C4E629FCB0",` + `"args": {` + `"jsonFileName": ".\deployment.json",` + `"parentPropertyPath": "moduleConfiguration:edgeAgent:properties.desired",` + `"newJsonPropertyName": "LogLevel",` + `"newJsonPropertyValue": "Information"` + `},` + `"continueOnError": true` +`}] +``` diff --git a/documentation/TemplateEngine/Reference-for-template.json.md b/documentation/TemplateEngine/Reference-for-template.json.md new file mode 100644 index 000000000000..9bcdba08135a --- /dev/null +++ b/documentation/TemplateEngine/Reference-for-template.json.md @@ -0,0 +1,692 @@ +## Overview + +This page describes the available properties of `template.json` configuration file. + +The templates defined using `template.json`are still the "runnable projects" A "runnable project" is a project that can be +executed as a normal project can. Instead of updating your source files to be tokenized you define replacements, and other processing, mostly in an external file, +the `template.json` file. This requires that a few changes have to be done on the fly during the preparation of the package, and in the `template.json` we define all the operations needed to create a well working package. + +**Benefits of "runnable project" templates** +* Code format is not modified, therefore existing editors work great for template content +* You can still build, run, debug and test your template just like any other project +* No injection of special templating tokens into the project is required (though possible) + +**Typical layout on disk** +``` + .template.config/ + template.json + MyTemplate/ + MyTemplate.csproj + Program.cs +``` + +The templates can be packaged to a NuGet package for further distribution. +A tutorial on how to create the template package can be found [here](https://learn.microsoft.com/en-us/dotnet/core/tutorials/cli-templates-create-template-package). + + +|Category|Description| +|------|------| +|[Template Definition](#template-definition)|Metadata of the template| +|[Content Manipulation](#content-manipulation)|Configuration of content changes in source files| +|[Output Management](#output-management)|Configuration of the final output| + +### Template Definition +|Name|Description|Mandatory| +|---|---|---| +|`identity`|A unique name for this template|yes| +|`author`|The author of the template|no| +|`classifications`| Zero or more characteristics of the template which are shown in the tabular output of `dotnet new list` and `dotnet new search` as "Tags". It is possible to filter the templates based on them using `--tag` option. In Visual Studio those items are shown in New Project Dialog in the available list of templates together with template name, description and language. Common classifications are: `Library`, `Test`, `Web` etc. |no| +|`name`|The name for the template. This is displayed as the template name when using `dotnet new` and Visual Studio.|yes| +|`groupIdentity`|The ID of the group this template belongs to. This allows multiple templates to be displayed as one, with the decision for which one to use based on the template options. See more information on templates grouping in [this article](./Using-Group-Identity.md). |no| +|`tags`|You can use tags to improve the metadata of your project. Well-known tags are: `language` and `type`. To specify the template language, use the tag `language`. To specify the template type, use the tag `type`. Supported types are: `project`, `item`, `solution`.|no| +|`shortName`|A name for selecting the template in CLI environment. This is the name shown as `Short Name` in `dotnet new` list of templates, and is the name to use to run this template. It is possible to specify multiple short names for the single template, and then any of them can be used to instantiate the template. The first element of the array is considered as the primary and first choice in scenarios where only single value is allowed (for example in tab completion of `dotnet new`). Be mindful when selecting the short name and using synonyms to avoid conflicts with other templates - the template short name should be unique. In case multiple template with the same short name are installed at the same time, the user won't be able to use any of them until one of the packages is uninstalled making the short name unique again. This doesn't apply for the templates that are part of the same group (have same `groupIdentity`). |yes| +|`postActions`|Enables actions to be performed after the project is created. See [the article](Post-Action-Registry.md) on post actions for more details.|no| +|`constraints`|[Template constraints](#template-constraints)||no| + +#### Template constraints + +Available since .NET SDK 7.0.100. +The template may define the constraints all of which must be met in order for the template to be used. +In case constraints are not met, the template will be installed, however will not be visible by default. +For the details on available constraints, refer to [the article](Constraints.md). +The constraints are defined under `constraints` property (top level in template.json). `constraints` contains objects (constraint definition). Each constraint should have a unique name, `type` and optional arguments (`args`). Argument syntax depends on the constraint implementation. + +```json +"constraints": { + "linux-only": { + "type": "os", + "args": "Linux" + }, + "sdk-only": { + "type": "host", + "args": [ + { + "hostname": "dotnetcli", + "version": "[6.0.100, )" + } + ] + } +} +``` + +### Content Manipulation +|Name|Description|Default|Mandatory| +|---|---|---|---| +|`sources`|The set of mappings in the template content to user directories. It's defined as any array of [Source](#source-definition) |If not specified, an implicit source is created with `"source": "./"` and `"target": "./"`, all other properties in the source are set to their defaults|no| +|`guids`|[Guids](#guids)||no| +|`symbols`|[Symbols](#symbols)||no| +|`forms`|[Value forms](Value-Forms.md)||no| + +#### Source Definition +|Name|Description|Default|Mandatory| +|---|---|---|---| +|`source`|The path in the template content (relative to the directory containing the .template.config folder) that should be processed|`./`|no| +|`target`|The path (relative to the directory the user has specified) that content should be written to|`./`|no| +|`include`|The set of globing patterns indicating the content to process in the path referred to by `source`|[ "**/*" ]|no| +|`exclude` |The set of globing patterns indicating the content that was included by sources.include that should not be processed|`[ "**/[Bb]in/**", "**/[Oo]bj/**", ".template.config/**/*", "**/*.filelist", "**/*.user", "**/*.lock.json" ]`|no| +|`copyOnly` |The set of globing patterns indicating the content that was included by `include`, that hasn't been excluded by sources.exclude that should be placed in the user's directory without modification|`[ "**/node_modules/**/*" ]`|no| +|`rename`|The set of explicit renames to perform. Each key is a path to a file in the source, each value is a path to the target location - only the values will be evaluated with the information the user supplies||no| +|`condition`|A boolean condition to indicate if the sources configuration should be included or ignored. If the condition evaluates to `true` or is not provided, the sources config will be used for creating the template. If it evaluates to `false`, the sources config will be ignored||no| +|`modifiers`|A list of additional source information which gets added to the top-level source information, based on evaluation the corresponding `source.modifiers.condition`. Is defined as an array of [Modifier](#modifier-definition)||no| + +#### Modifier Definition +|Name|Description|Mandatory| +|---|---|---| +|`condition`|A boolean condition to indicate if the `sources.modifiers` instance should be included or ignored. If the condition evaluates to `true` or is not provided, the `sources.modifiers` instance will be used for creating the template. If it evaluates to false, the `sources.modifiers` config will be ignored.|yes| +|`include`|Include configuration specific to this `sources.modifiers` instance, contingent on the corresponding `sources.modifiers.condition`. See `sources.include` for more info.|no| +|`exclude`|Exclude configuration specific to this `sources.modifiers` instance, contingent on the corresponding `sources.modifiers.condition`. See `sources.exclude` for more info|no| +|`copyOnly`|CopyOnly configuration specific to this `sources.modifiers` instance, contingent on the corresponding `sources.modifiers.condition`. See `sources.copyOnly` for more info|no| + +### Guids +An optional list of guids which appear in the template source and should be replaced in the template output. For each guid listed, a replacement guid is generated, and replaces all occurrences of the source guid in the output. Matching of the guids in the source template works independently on the format and casing of the guids in the `template.json` file and source files. Format and casing from template source is preserved in the output (casing and format of the guid in `Guids` section of `template.json` doesn't influence matching or output generation). + +##### Example +Sample template.json snippet: +```json + "guids": [ + "98048C9C-BF28-46BA-A98E-63767EE5E3A8", + "c7ab42cf938548c08b8784349ab5e04b" + ], +``` + +Sample template file content: +```console +[n]: 98048c9cbf2846baa98e63767ee5e3a8 +[d]: 98048c9c-bf28-46ba-a98e-63767ee5e3a8 +[b]: {98048c9c-bf28-46ba-a98e-63767ee5e3a8} +[p]: (98048c9c-bf28-46ba-a98e-63767ee5e3a8) +[x]: {0x98048c9c,0xbf28,0x46ba,{0xa9,0x8e,0x63,0x76,0x7e,0xe5,0xe3,0xa8}} +[N]: 98048C9CBF2846BAA98E63767EE5E3A8 +[D]: 98048C9C-BF28-46BA-A98E-63767EE5E3A8 +[B]: {98048C9C-BF28-46BA-A98E-63767EE5E3A8} +[P]: (98048C9C-BF28-46BA-A98E-63767EE5E3A8) +[X]: {0X98048C9C,0XBF28,0X46BA,{0XA9,0X8E,0X63,0X76,0X7E,0XE5,0XE3,0XA8}} + +[n]: c7ab42cf938548c08b8784349ab5e04b +[d]: c7ab42cf-9385-48c0-8b87-84349ab5e04b +[b]: {c7ab42cf-9385-48c0-8b87-84349ab5e04b} +[p]: (c7ab42cf-9385-48c0-8b87-84349ab5e04b) +[x]: {0xc7ab42cf,0x9385,0x48c0,{0x8b,0x87,0x84,0x34,0x9a,0xb5,0xe0,0x4b}} +[N]: C7AB42CF938548C08B8784349AB5E04B +[D]: C7AB42CF-9385-48C0-8B87-84349AB5E04B +[B]: {C7AB42CF-9385-48C0-8B87-84349AB5E04B} +[P]: (C7AB42CF-9385-48C0-8B87-84349AB5E04B) +[X]: {0XC7AB42CF,0X9385,0X48C0,{0X8B,0X87,0X84,0X34,0X9A,0XB5,0XE0,0X4B}} +``` + +Output content after template instantiation: +```console +[n]: a6d920ff125841318f5e07df108f5a4a +[d]: a6d920ff-1258-4131-8f5e-07df108f5a4a +[b]: {a6d920ff-1258-4131-8f5e-07df108f5a4a} +[p]: (a6d920ff-1258-4131-8f5e-07df108f5a4a) +[x]: {0xa6d920ff,0x1258,0x4131,{0x8f,0x5e,0x07,0xdf,0x10,0x8f,0x5a,0x4a}} +[N]: A6D920FF125841318F5E07DF108F5A4A +[D]: A6D920FF-1258-4131-8F5E-07DF108F5A4A +[B]: {A6D920FF-1258-4131-8F5E-07DF108F5A4A} +[P]: (A6D920FF-1258-4131-8F5E-07DF108F5A4A) +[X]: {0XA6D920FF,0X1258,0X4131,{0X8F,0X5E,0X07,0XDF,0X10,0X8F,0X5A,0X4A}} + +[n]: 2879773c9a5241f78fa5ac318214f54d +[d]: 2879773c-9a52-41f7-8fa5-ac318214f54d +[b]: {2879773c-9a52-41f7-8fa5-ac318214f54d} +[p]: (2879773c-9a52-41f7-8fa5-ac318214f54d) +[x]: {0x2879773c,0x9a52,0x41f7,{0x8f,0xa5,0xac,0x31,0x82,0x14,0xf5,0x4d}} +[N]: 2879773C9A5241F78FA5AC318214F54D +[D]: 2879773C-9A52-41F7-8FA5-AC318214F54D +[B]: {2879773C-9A52-41F7-8FA5-AC318214F54D} +[P]: (2879773C-9A52-41F7-8FA5-AC318214F54D) +[X]: {0X2879773C,0X9A52,0X41F7,{0X8F,0XA5,0XAC,0X31,0X82,0X14,0XF5,0X4D}} +``` + +### Symbols +The symbols section defines variables and their values, the values may be the defined in terms of other symbols. When a defined symbol name is encountered anywhere in the template definition, it is replaced by the value defined in this configuration. The symbols configuration is a collection of key-value pairs. The keys are the symbol names, and the value contains key-value-pair configuration information on how to assign the symbol a value. +The supported symbol types are: +- Parameter +- Derived +- Computed +- Generated +- Bind + +There are 3 places from which a symbol can acquire its value: +- Template configuration (this). +- Host provided values, which override template configuration values. For example, the host may provide the operating system kind as "Linux", overriding a template default value of "Windows". +- User provided values, which override host & template values. The way these are provided to the template generation broker is specific to the broker. This may be via additional configuration files, command line parameters, inputs to a UI, etc. + +#### Parameter symbol + +A symbol for which the config provides literal and/or default values. + +|Name|Description|Mandatory| +|---|---|---| +|`type`|`parameter`| yes | +|`dataType`| Supported values:
- `bool`: boolean type, possible values: `true`/`false`.
- `choice`: enumeration, possible values are defined in `choices` property.
- `float`: double-precision floating format number. Accepts any value that can be parsed by `double.TryParse()`.
- `int`/`integer`: 64-bit signed integer. Accepts any value that can be parsed by `long.TryParse()`.
- `hex`: hex number. Accepts any value that can be parsed by `long.TryParse(value.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out long convertedHex)`.
- `text`/`string`: string type.
- ``: treated as string.| no, default - `string` | +|`defaultValue`|The value assigned to the symbol if no parameter is provided by the user or host. The default is *not applied* if [`isRequired`](#isRequired) configuration is set to `true` for a parameter (or is set to condition that evaluates to `true`), as that is an indication that user specified value is required. | no | +|`defaultIfOptionWithoutValue`|The value assigned to the symbol if explicit `null` parameter value is provided by the user or host.|no | +|`replaces`|The text to be replaced by the symbol value in the template files content| no | +|`fileRename`|The portion of template filenames to be replaced by the symbol value.| no | +|`description`|Human readable text describing the meaning of the symbol. This has no effect on template generation.| no | +|`isRequired`|Optional. Indicates if the user supplied value is required or not. Might be a fixed boolean value or a condition string that evaluates based on passed parameters - more details in [Conditions documentation](Conditions.md#conditional-parameters).
If set to `true` (or condition that evaluates to `true`) and user has not specified the value on input, validation error occurs - even if [`defaultValue`](#defaultValue) is present.| no | +|`isEnabled`| Optional condition indicating whether parameter should be processed. Might be a fixed boolean value or a condition string that evaluates based on passed parameters - more details in [Conditions documentation](Conditions.md#conditional-parameters).| no | +|`choices`|Applicable only when `datatype=choice.`
List of available choices. Contains array of the elements:
- `choice`: possible value of the symbol.
- `description`: human readable text describing the meaning of the choice. This has no effect on template generation.
If not provided, there are no valid choices for the symbol, so it can never be assigned a value.| yes, for `dataType` = `choice` | +|`allowMultipleValues`|Applicable only when `datatype=choice`.
Enables ability to specify multiple values for single symbol.| no | +|`enableQuotelessLiterals`|Applicable only when `datatype=choice`.
Enables ability to specify choice literals in conditions without quotation.|no | +|`onlyIf`| Defines the conditions that should be fulfilled in order that replacement happens. |no | +|`forms`|Defines the set of transforms that can be referenced by symbol definitions. Forms allow the specification of a "replaces"/"replacement" pair to also apply to other ways the "replaces" value may have been specified in the source by specifying a transform from the original value of "replaces" in configuration to the one that may be found in the source. [Details](Value-Forms.md)|no | + +##### Examples +Boolean optional parameter with default value `false`: +```json + "symbols": { + "TestProject": { + "type": "parameter", + "dataType": "bool", + "defaultValue": "false" + } + }, +``` + +Choice optional parameter with 3 possible values: +```json + "symbols": { + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "enableQuotelessLiterals": true, + "choices": [ + { + "choice": "netcoreapp3.1", + "description": "Target netcoreapp3.1" + }, + { + "choice": "netstandard2.1", + "description": "Target netstandard2.1" + }, + { + "choice": "netstandard2.0", + "description": "Target netstandard2.0" + } + ], + "replaces": "netstandard2.0", + "defaultValue": "netstandard2.0" + } +} +``` + +String optional parameter, replaces TargetFrameworkOverride: +```json + "symbols": { + "TargetFrameworkOverride": { + "type": "parameter", + "description": "Overrides the target framework", + "replaces": "TargetFrameworkOverride", + "datatype": "string", + "defaultValue": "" + } +} +``` + +#### Multi-choice symbols specifics +Multi-choice symbols have similar behavior and usage scenarios as [C# Flag enums](https://docs.microsoft.com/en-us/dotnet/api/system.flagsattribute) - they express a range of possible values (not a single value - unlike the plain [choice symbol](#choice-sample)) + +##### Example definition of multi-choice symbol: + +```json +"symbols": +{ + "Platform": { + "type": "parameter", + "description": "The target platform for the project.", + "datatype": "choice", + "allowMultipleValues": true, // multi-choice indicator + "choices": [ + { + "choice": "Windows", + "description": "Windows Desktop" + }, + { + "choice": "MacOS", + "description": "Macintosh computers" + }, + { + "choice": "iOS", + "description": "iOS mobile" + }, + { + "choice": "android", + "description": "android mobile" + }, + { + "choice": "nix", + "description": "Linux distributions" + } + ], + "defaultValue": "MacOS|iOS" + } +} +``` + +There are some specifics in behavior of multi-choice symbols that are worth noting: + +* Condition evaluation - closer described in [Conditions document](Conditions.md#multichoice-literals). +* [`Switch` symbol](Available-Symbols-Generators.md#switch) evaluation - conditions are evaluated by identical evaluator as preprocessing conditions (previous bullet point). +* Argument passing and tab completion on CLI - User can specify multiple options via repeating the argument switch for each option: + `dotnet new MyTemplate --MyParameter value1 --MyParameter value2` + + or by passing multiple tokens to single option switch: + + `dotnet new MyTemplate --MyParameter value1 value2` + + Tab completion works identically as for standard choice symbol + +* Default values specification and API usage - Currently multiple values can be specified within single string separated by `|` or `,` characters. Escaping of those characters within values is currently not supported. +* Using multi-choice value as a string in template content - this can be achieved via leveraging the [`join` symbol](Available-Symbols-Generators.md#multichoice-join-sample). For simplicity it is as well possible to specify replacement for choice symbol with multiple values - in such case the values will be rendered into single string separated by `|` sign. + + + +#### Derived symbol + +A symbol that defines transformation of another symbol. The value of this symbol is derived from the value of another symbol by the application of form defined in `valueTransform`. + +|Name|Description|Mandatory| +|---|---|---| +|`type`|`derived`| yes | +|`valueSource`|The name of the other symbol which value will be used to derive this value.| yes | +|`valueTransform`|The name of the value form to apply to the source value.| yes | +|`replaces`|The text to be replaced by the symbol value in the template files content| no | +|`fileRename`|The portion of template filenames to be replaced by the symbol value.| no | +|`description`|Human readable text describing the meaning of the symbol. This has no effect on template generation.| no | + +##### Examples +Renames `Application1` file to value of `name` symbol after last dot +```json +{ +... + "symbols": { + "app1Rename": { + "type": "derived", + "valueSource": "name", + "valueTransform": "ValueAfterLastDot", + "fileRename": "Application1", + "description": "A value derived from the 'name' param, used to rename Application1.cs" + } + } +... + "forms": { + "ValueAfterLastDot": { + "identifier": "replace", + "pattern": "^.*\\.(?=[^\\.]+$)", // regex to match everything up to and including the final "." + "replacement": "" // replace it with empty string + } + } +... +} +``` + +#### Generated symbol + +A symbol which value gets computed by a built-in symbol value generator. [Details](Available-Symbols-Generators.md) + +|Name|Description|Mandatory| +|---|---|---| +|`type`|`generated`| yes | +|`generator`|Generator to use: See [this article](Available-Symbols-Generators.md) for more details.| yes | +|`parameters`|The parameters for generator. See [description](Available-Symbols-Generators.md) for each generator for details.| depends on generator | +|`replaces`|The text to be replaced by the symbol value in the template files content| no | +|`fileRename`|(supported in 5.0.200 or higher) The portion of template filenames to be replaced by the symbol value.| no | +|`dataType`| Supported values:
- `bool`: boolean type, possible values: `true`/`false`.
- `float`: double-precision floating format number. Accepts any value that can be parsed by `double.TryParse()`.
- `int`/`integer`: 64-bit signed integer. Accepts any value that can be parsed by `long.TryParse()`.
- `hex`: hex number. Accepts any value that can be parsed by `long.TryParse(value.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out long convertedHex)`.
- `text`/`string`: string type.
- ``: treated as string.| no, default: `string` | + +##### Example + +- `myconstant`: replaces `1234` with `5001` +- `ownername`: replaces `John Smith (a)` with the value of the `ownername` parameter +- `nameUpper`: + - replaces `John Smith (U)` with the value of the `ownername` parameter in upper case in template files content + - replaces `author_uc` with the value of the `ownername` parameter in upper case in filenames +- `nameLower`: + - replaces `John Smith (l)` with the value of the `ownername` parameter in lower case in template files content + - replaces `author_lc` with the value of the `ownername` parameter in lower case in filenames + +```json +"symbols":{ + "myconstant": { + "type": "generated", + "generator": "constant", + "parameters": { + "value":"5001" + }, + "replaces":"1234" + }, + "ownername":{ + "type": "parameter", + "datatype":"text", + "replaces": "John Smith (a)", + "defaultValue": "John Doe" + }, + "nameUpper":{ + "type": "generated", + "generator": "casing", + "parameters": { + "source":"ownername", + "toLower": false + }, + "replaces":"John Smith (U)", + "fileRename": "author_uc" + }, + "nameLower":{ + "type": "generated", + "generator": "casing", + "parameters": { + "source":"ownername", + "toLower": true + }, + "replaces":"John Smith (l)", + "fileRename": "author_lc" + } +} +``` + +#### Computed symbol + +A symbol for which the config provides a Boolean predicate whose evaluation result is the computed symbol result. + +|Name|Description|Mandatory| +|---|---|---| +|`type`|`computed`| yes | +|`value`| Boolean expression to be computed.| yes | +|`evaluator`|Language to be used for evaluation of expression:
- `C++2`
- `C++`
- `MSBUILD`
- `VB`| no, default: `C++2` | + +Please note that math operations are not supported in expressions at this point. + +##### Example + +Values of `OrganizationalAuth`, `WindowsAuth`, `MultiOrgAuth`, `SingleOrgAuth`, `IndividualAuth`, `NoAuth`, `RequiresHttps` symbols are computed based on `auth` symbol. + +```json + "symbols": { + "auth": { + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "None", + "description": "No authentication" + }, + { + "choice": "Individual", + "description": "Individual authentication" + }, + { + "choice": "SingleOrg", + "description": "Organizational authentication for a single tenant" + }, + { + "choice": "MultiOrg", + "description": "Organizational authentication for multiple tenants" + }, + { + "choice": "Windows", + "description": "Windows authentication" + } + ], + "defaultValue": "Individual", + "description": "The type of authentication to use" + }, + "OrganizationalAuth": { + "type": "computed", + "value": "(auth == \"SingleOrg\" || auth == \"MultiOrg\")" + }, + "WindowsAuth": { + "type": "computed", + "value": "(auth == \"Windows\")" + }, + "MultiOrgAuth": { + "type": "computed", + "value": "(auth == \"MultiOrg\")" + }, + "SingleOrgAuth": { + "type": "computed", + "value": "(auth == \"SingleOrg\")" + }, + "IndividualAuth": { + "type": "computed", + "value": "(auth == \"Individual\")" + }, + "NoAuth": { + "type": "computed", + "value": "(!(IndividualAuth || MultiOrgAuth || SingleOrgAuth || WindowsAuth))" + }, + "RequiresHttps": { + "type": "computed", + "value": "(OrganizationalAuth)" + } + } +``` + +#### Bind symbol +See [the article](Binding-and-project-context-evaluation.md#bind-symbols). + + +### Output Management +|Name|Description|Default|Mandatory| +|---|---|---|---| +|`sourceName`| The text in the source content to replace with the name the user specifies. The value to be replaced with can be given using the `-n` `--name` options while running a template. The template engine will look for any occurrence of the `sourceName` present in the config file and replace it in file names and file contents. If no name is specified by the host, the current directory is used. The value of the `sourceName` is available in built-in `name` symbol and can be used as the source for creating other symbols and condition expressions. Due to implicit forms applied to `sourceName` it is important to select the replacement that doesn't cause the conflicts. See more details about `sourceName` in [the article](Naming-and-default-value-forms.md)||no| +|`preferDefaultName`| Boolean value that decides which name will be used if no `--name` is specified during creation. If `false` it will use the fallback name (output folder name), if `true` it will use the template's `defaultName`. If no `defaultName` and no fallback name is specified and `preferDefaultName` is set to `true`, the template creation will not succeed with a `TemplateIssueDetected` exception. | If not specified, `false` is used. | no +|`preferNameDirectory`| Boolean value, indicates whether to create a directory for the template if name is specified but an output directory is not set (instead of creating the content directly in the current directory).|If not specified, `false` is used.|no| +|`placeholderFilename`|A filename that will be completely ignored, used to indicate that its containing directory should be copied. This allows creation of empty directory in the created template, by having a corresponding source directory containing just the placeholder file. By default, empty directories are ignored.|If not specified, a default value of `"-.-"` is used.|no| +|`primaryOutputs`|A list of template files for further processing by the host (including post-actions). The path should contain the relative path to the file prior to the symbol based renaming that may happen during template generation. It is defined as an array of [Primary Outputs](#primary-output-definition)||no| + +#### Primary Output Definition +Primary outputs define the list of template files for further processing, usually post actions. +|Name|Description|Mandatory| +|---|---|---| +|`path`|should contain the relative path to the file after the template is instantiated.|yes| +|`condition`|if the condition evaluates to `true`, the corresponding path will be added to primary outputs, if `false`, the path is ignored. If no condition is provided for a path, the condition defaults to `true`.|no| + +For more information on primary outputs, refer to [the article](Using-Primary-Outputs-for-Post-Actions.md). + +### Global custom operations and special custom operations +This advanced configuration allows the template author to define custom actions for the template creation process. Global custom operations are scoped globally unless overridden by a more restrictive custom configuration. +Special custom operations are scoped to the files matched by their glob pattern. + +Customizations allowed include: +* Flag definitions +* Modification operations + +There can only be one instance of the (global) `customOperations` section, as follows: +```JSON + "customOperations": { + // configuration details + } +``` + +But since there can be many instances of the SpecialCustomOperations, each applicable to their glob patterns, the `specialCustomOperations` configuration section is defined as follows: + +```JSON + "specialCustomOperations": { + "": { + // configuration details + }, + "": { + // configuration details + }, + } +``` + +The configuration details options are identical in both situations, they only differ in the scopes they're applied in. The below sections are labelled `customOperations.`, but are equally applicable to `specialCustomOperations..` + +|Name|Description|Mandatory| +|---|---|---| +|`flagPrefix`|Overrides the default prefix to indicate a flag option in a file being processed.|no| +|`operations`|An array that defines custom operations for processing data in the files within the scope of the configuration. These operations are applied in addition to other operations setup by default, except in the case of the conditional operation. If a custom conditional operation is defined, the default conditional operation(s) are ignored (in this scope).|no| +|`operations.type`|Indicates the type of operation being configured. The operations currently available to configure are:
- `balancedNesting`
- `conditional`
- `flag`
- `include`
- `region`
- `replacement`|yes| +|`operations.condition`|A boolean expression whose result determines whether or not to use this custom operation. If this is not provided, the operation is used.|no| +|`operations.configuration`|The details of the operation configuration. Each type of operation has its own configuration options, as detailed below.|yes| + +SpecialCustomOperations can be used for adding conditions for handling files with extensions not listed in [Conditional processing and comment syntax](../docs/Conditional-processing-and-comment-syntax.md#introduction). + +```JSON +{ + "SpecialCustomOperations": { + "**.axml": { + "operations": [ + { + "type": "conditional", + "configuration": { + "endif": [ "#endif", " + true + + + + + + annotations + + + + + + + + + + + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/FileSystemInfoKind.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/FileSystemInfoKind.cs new file mode 100644 index 000000000000..ac2779b3f49f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/FileSystemInfoKind.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Mount +{ + /// + /// Defines the kind of a entry. + /// + public enum FileSystemInfoKind + { + /// + /// Entry is a file. + /// + File, + + /// + /// Entry is a directory. + /// + Directory + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IDirectory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IDirectory.cs new file mode 100644 index 000000000000..3dcb743209d3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IDirectory.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Mount +{ + /// + /// Defines directory entry. + /// + public interface IDirectory : IFileSystemInfo + { + /// + /// Enumerates the entries in the directory. + /// + /// Matching pattern for the entries, see for more details. + /// Matching pattern for the entries, see for more details. + /// The enumerator to entries in the directory. + IEnumerable EnumerateFileSystemInfos(string pattern, SearchOption searchOption); + + /// + /// Enumerates the entries in the directory. + /// + /// Matching pattern for the entries, see for more details. + /// Matching pattern for the entries, see for more details. + /// The enumerator to entries in the directory. + IEnumerable EnumerateFiles(string pattern, SearchOption searchOption); + + /// + /// Enumerates the entries in the directory. + /// + /// Matching pattern for the entries, see for more details. + /// Matching pattern for the entries, see for more details. + /// The enumerator to entries in the directory. + IEnumerable EnumerateDirectories(string pattern, SearchOption searchOption); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IFile.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IFile.cs new file mode 100644 index 000000000000..90bb7de0ad7d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IFile.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Mount +{ + /// + /// Defines a file entry. + /// + public interface IFile : IFileSystemInfo + { + /// + /// Opens the file stream for reading. + /// + /// that can be used for reading the file. + Stream OpenRead(); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IFileSystemInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IFileSystemInfo.cs new file mode 100644 index 000000000000..be73e30cdde1 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IFileSystemInfo.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Mount +{ + /// + /// Defines a file system entry. + /// + /// + /// + public interface IFileSystemInfo + { + /// + /// Returns true if file system entry exists, false otherwise. + /// + bool Exists { get; } + + /// + /// Gets the path to file system entry inside mount point . + /// + string FullPath { get; } + + /// + /// Gets file system entry kind. + /// + FileSystemInfoKind Kind { get; } + + /// + /// Gets parent directory of file system entry. + /// + IDirectory? Parent { get; } + + /// + /// Gets file system entry name. + /// + string Name { get; } + + /// + /// Gets mount point for file system entry. + /// + IMountPoint MountPoint { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IMountPoint.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IMountPoint.cs new file mode 100644 index 000000000000..a3670f1fa8e0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IMountPoint.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Mount +{ + /// + /// Represents abstract directory. + /// This could be regular folder on file system, zip file, HTTP server or anything else that can + /// present files in file system hierarchy. + /// + public interface IMountPoint : IDisposable + { + /// + /// Returns mount point URI (as received from template package provider = not normalized). + /// + string MountPointUri { get; } + + /// + /// Returns root directory of the mount point. + /// + IDirectory Root { get; } + + /// + /// used to create this instance. + /// + IEngineEnvironmentSettings EnvironmentSettings { get; } + + /// + /// Gets the file info for the file in the mount point. + /// + /// The path to the file relative to mount point root. + /// + IFile? FileInfo(string path); + + /// + /// Gets the directory info for the directory in the mount point. + /// + /// The path to the directory relative to mount point root. + /// + IDirectory? DirectoryInfo(string path); + + /// + /// Gets the file system entry (file or directory) info for the entry in the mount point. + /// + /// The path to the file system entry relative to mount point root. + /// + IFileSystemInfo? FileSystemInfo(string path); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IMountPointFactory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IMountPointFactory.cs new file mode 100644 index 000000000000..ebc75da55f7c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IMountPointFactory.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Mount +{ + /// + /// Implement this factory and register it with . + /// Template engine calls with Uri as parameter on all factories + /// and use mount point from first successful factory. + /// + public interface IMountPointFactory : IIdentifiedComponent + { + /// + /// Tries to mount specified Uri. + /// + /// Environment to be used. + /// Mount points can be mounted inside each other. Pass in parent or null. + /// Valid that represents mount point. + /// Resulting mount point. + /// true if mount point was successfully mounted. + bool TryMount(IEngineEnvironmentSettings environmentSettings, IMountPoint? parent, string mountPointUri, out IMountPoint? mountPoint); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IMountPointManager.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IMountPointManager.cs new file mode 100644 index 000000000000..42f6f11043a7 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Mount/IMountPointManager.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Mount +{ + [Obsolete("Use Microsoft.TemplateEngine.Utils.EngineEnvironmentSettingsExtensions.TryGetMountPoint")] + public interface IMountPointManager + { + IEngineEnvironmentSettings EnvironmentSettings { get; } + + bool TryDemandMountPoint(string mountPointUri, out IMountPoint mountPoint); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/ParameterChoice.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/ParameterChoice.cs new file mode 100644 index 000000000000..c41ed22272cb --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/ParameterChoice.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions +{ + /// + /// Represents a choice that can be assigned to a parameter. + /// + public sealed class ParameterChoice + { + public ParameterChoice(string? displayName, string? description) + { + DisplayName = displayName; + Description = description; + } + + /// + /// Gets or sets the friendly name of the choice to be displayed to the user. + /// + public string? DisplayName { get; set; } + + /// + /// Gets or sets the description of the choice to be displayed to the user. + /// + public string? Description { get; set; } + + /// + /// Localizes the choice with the given localization model. + /// + public void Localize(ParameterChoiceLocalizationModel localizationModel) + { + DisplayName = localizationModel.DisplayName ?? DisplayName; + Description = localizationModel.Description ?? Description; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/ParameterChoiceLocalizationModel.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/ParameterChoiceLocalizationModel.cs new file mode 100644 index 000000000000..73cb109c218a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/ParameterChoiceLocalizationModel.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions +{ + /// + /// Represents the data necessary for the localization of a . + /// + public sealed class ParameterChoiceLocalizationModel + { + public ParameterChoiceLocalizationModel(string? displayName, string? description) + { + DisplayName = displayName; + Description = description; + } + + /// + /// Gets the friendly name of the choice to be displayed to the user. + /// + public string? DisplayName { get; private set; } + + /// + /// Gets the description of the choice to be displayed to the user. + /// + public string? Description { get; private set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/DataSource.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/DataSource.cs new file mode 100644 index 000000000000..e8ee33cf7729 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/DataSource.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Parameters +{ + public enum DataSource + { + /// + /// Value was not set. + /// + NoSource, + + /// + /// Those are values supplied by the host. This usually means value(s) was/were set by user. + /// + User, + + /// + /// Value obtained via . + /// + HostDefault, + + /// + /// Value obtained via ITemplateEngineHost.OnParameterError. + /// + [Obsolete("The value is not used anymore due to ITemplateEngineHost.OnParameterError was removed.")] + HostOnError, + + /// + /// Value from template - . + /// + Default, + + /// + /// Value from template - . + /// + DefaultIfNoValue, + + /// + /// This corresponds to Name implicit parameter value. + /// + NameParameter, + + /// + /// To be used in case host uses advanced object model to supply values to TemplateCreator or Generator and + /// wants to indicate that it used some custom logic of inferring value for parameter + /// (e.g. custom Host calculated value of parameter based on current context and supplied the value to template engine). + /// + HostOther, + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/IParameterDefinitionSet.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/IParameterDefinitionSet.cs new file mode 100644 index 000000000000..2e9c4efaf8b0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/IParameterDefinitionSet.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Parameters; + +/// +/// The set of descriptors of template parameters extracted from the template. +/// +public interface IParameterDefinitionSet : IReadOnlyList +{ + // + // Following methods are taken from the IReadOnlyDictionary + // That interface is not explicitly inherited as it would lead to ambiguous implementation of GetEnumerator() + // and hence not clean way of using in foreach. As that's more often scenario, we decided to inherit IEnumerable + // + + /// Gets an enumerable collection that contains the keys in the read-only dictionary. + /// An enumerable collection that contains the keys in the read-only dictionary. + IEnumerable Keys { get; } + + /// Gets an enumerable collection that contains the values in the read-only dictionary. + /// An enumerable collection that contains the values in the read-only dictionary. + IEnumerable Values { get; } + + /// Gets the element that has the specified key in the read-only dictionary. + /// The key to locate. + /// The element that has the specified key in the read-only dictionary. + /// key is null. + /// The property is retrieved and key is not found. + ITemplateParameter this[string key] { get; } + + /// Determines whether the read-only dictionary contains an element that has the specified key. + /// The key to locate. + /// true if the read-only dictionary contains an element that has the specified key; otherwise, false. + /// key is null. + bool ContainsKey(string key); + + /// Gets the value that is associated with the specified key. + /// The key to locate. + /// When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the value parameter. This parameter is passed uninitialized. + /// true if the object that implements the interface contains an element that has the specified key; otherwise, false. + /// key is null. + bool TryGetValue(string key, out ITemplateParameter value); + + // End of definitions pulled from IReadOnlyDictionary + + /// + /// Casts the Parameter set to IReadOnlyDictionary. + /// + /// + IReadOnlyDictionary AsReadonlyDictionary(); +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/IParameterSetData.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/IParameterSetData.cs new file mode 100644 index 000000000000..a233b22c4194 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/IParameterSetData.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Parameters; + +/// +/// Data model for bound and merged dataset to be used to substitute and evaluate active sections within templates. +/// Data are possibly merged from multiple sources (default values in definition, values supplied by host, user, etc.). +/// +public interface IParameterSetData : IReadOnlyDictionary +{ + /// + /// Descriptors for the parameters - inferred from the template. + /// + IParameterDefinitionSet ParametersDefinition { get; } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterData.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterData.cs new file mode 100644 index 000000000000..ecace37ae821 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterData.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Parameters; + +/// +/// Data model for template parameter instance data. Mainly used as input for . +/// +public class ParameterData +{ + /// + /// Creates new instance of class. + /// + /// Descriptor of the parameter inferred from the template. + /// The value of the parameter derived from the instantiation context. + /// Source of value for template instantiation in the current context. + /// Indicates whether the parameter is enabled or disabled (and hence completely ignored). + public ParameterData( + ITemplateParameter parameterDefinition, + object? value, + DataSource source, + bool isEnabled = true) + { + ParameterDefinition = parameterDefinition; + Value = value; + DataSource = source; + IsEnabled = isEnabled; + } + + /// + /// Descriptor of the parameter inferred from the template. + /// + public ITemplateParameter ParameterDefinition { get; } + + /// + /// The value of the parameter derived from the instantiation context (actual source of value indicated in ). + /// + public object? Value { get; } + + /// + /// Source of value for template instantiation in the current context. + /// + public DataSource DataSource { get; } + + /// + /// Indicates whether the parameter is enabled or disabled (and hence completely ignored). Disabling of parameter can be achieved via + /// condition or constant for IsEnabled property in Template. + /// + public bool IsEnabled { get; } + + public override string ToString() => $"{ParameterDefinition}: {Value?.ToString() ?? ""}"; +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterDefinitionSet.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterDefinitionSet.cs new file mode 100644 index 000000000000..941b8f7fea8a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterDefinitionSet.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; + +namespace Microsoft.TemplateEngine.Abstractions.Parameters +{ + /// + public class ParameterDefinitionSet : IParameterDefinitionSet + { + /// + /// Empty descriptor set. + /// + public static readonly IParameterDefinitionSet Empty = new ParameterDefinitionSet((IEnumerable?)null); + + private readonly IReadOnlyDictionary _parameters; + + /// + /// Initializes new instance of the type. + /// + /// + public ParameterDefinitionSet(IReadOnlyDictionary? parameters) => + _parameters = parameters ?? new Dictionary(); + + /// + /// Initializes new instance of the type. + /// + /// + public ParameterDefinitionSet(IEnumerable? parameters) + : this(parameters?.ToDictionary(p => p.Name, p => p)) + { } + + /// + /// Initializes new instance of the type. + /// + /// Instance to be cloned. + public ParameterDefinitionSet(IParameterDefinitionSet other) : this(other.AsReadonlyDictionary()) + { } + + /// + public IEnumerable Keys => _parameters.Keys; + + /// + public IEnumerable Values => _parameters.Values; + + /// + public int Count => _parameters.Count; + + /// + public ITemplateParameter this[string key] => _parameters[key]; + + /// + public ITemplateParameter this[int index] => _parameters.Values.ElementAt(index); + + /// + public IReadOnlyDictionary AsReadonlyDictionary() => _parameters; + + /// + public bool ContainsKey(string key) => _parameters.ContainsKey(key); + + /// + public IEnumerator GetEnumerator() => _parameters.Values.GetEnumerator(); + + /// + public bool TryGetValue(string key, out ITemplateParameter value) => _parameters.TryGetValue(key, out value); + + /// + IEnumerator IEnumerable.GetEnumerator() => _parameters.Values.GetEnumerator(); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterSetData.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterSetData.cs new file mode 100644 index 000000000000..f5b61520d78e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterSetData.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; + +namespace Microsoft.TemplateEngine.Abstractions.Parameters; + +/// +public class ParameterSetData : IParameterSetData +{ + private readonly IReadOnlyDictionary _parametersData; + + /// + /// Creates new instance of the data type. + /// + /// + /// + public ParameterSetData(IParameterDefinitionSet parameters, IReadOnlyList parameterData) + { + ParametersDefinition = new ParameterDefinitionSet(parameters.AsReadonlyDictionary()); + _parametersData = parameterData.ToDictionary(d => d.ParameterDefinition, d => d); + } + + /// + /// Creates new instance of the data type, not initialized with any actual instantiation data. + /// To be used for compatibility purposes in places where old dictionary parameter set was used. + /// + /// + public ParameterSetData(ITemplateInfo templateInfo) + : this(templateInfo, (IReadOnlyDictionary?)null) + { } + + /// + /// Creates new instance of the data type. + /// To be used for compatibility purposes in places where old dictionary parameter set was used. + /// + /// + /// + public ParameterSetData(ITemplateInfo templateInfo, IReadOnlyDictionary? inputParameters) + : this(templateInfo, inputParameters?.ToDictionary(p => p.Key, p => (object?)p.Value)) + { } + + /// + /// Creates new instance of the data type. + /// To be used for compatibility purposes in places where old dictionary parameter set was used. + /// + /// + /// + public ParameterSetData(ITemplateInfo templateInfo, IReadOnlyDictionary? inputParameters) + { + ParametersDefinition = new ParameterDefinitionSet(templateInfo.ParameterDefinitions); + _parametersData = templateInfo.ParameterDefinitions.ToDictionary(p => p, p => + { + object? value = null; + bool isSet = inputParameters != null && inputParameters.TryGetValue(p.Name, out value); + return new ParameterData(p, value, isSet ? DataSource.User : DataSource.NoSource); + }); + } + + /// + /// Empty instance. + /// + public static IParameterSetData Empty => + new ParameterSetData(new ParameterDefinitionSet((IReadOnlyDictionary?)null), []); + + /// + public IParameterDefinitionSet ParametersDefinition { get; } + + /// + public int Count => _parametersData.Count; + + /// + public IEnumerable Keys => _parametersData.Keys; + + /// + public IEnumerable Values => _parametersData.Values; + + /// + public ParameterData this[ITemplateParameter key] => _parametersData[key]; + + /// + public IEnumerator> GetEnumerator() => _parametersData.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + public bool ContainsKey(ITemplateParameter key) => _parametersData.ContainsKey(key); + + /// + public bool TryGetValue(ITemplateParameter key, out ParameterData value) => _parametersData.TryGetValue(key, out value); +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterSetDataExtensions.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterSetDataExtensions.cs new file mode 100644 index 000000000000..c2ab6463e903 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/Parameters/ParameterSetDataExtensions.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.Parameters; + +public static class ParameterSetDataExtensions +{ + /// + /// Fetches the value of parameter from the set, based on the name of the parameter. + /// + /// + /// + /// + public static ParameterData GetValue(this IParameterSetData data, string parameterName) + { + return data[data.ParametersDefinition[parameterName]]; + } + + /// + /// Attempts to fetch the value of parameter from the set, based on the name of the parameter. + /// + /// + /// + /// + /// True if parameter data was found. False otherwise. + public static bool TryGetValue(this IParameterSetData data, string parameterName, out ParameterData? parameterData) + { + parameterData = null; + return + data.ParametersDefinition.TryGetValue(parameterName, out ITemplateParameter parameter) && + data.TryGetValue(parameter, out parameterData); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PhysicalFileSystem/IFileLastWriteTimeSource.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PhysicalFileSystem/IFileLastWriteTimeSource.cs new file mode 100644 index 000000000000..e53a4ba187dc --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PhysicalFileSystem/IFileLastWriteTimeSource.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem +{ + [Obsolete("Use IPhysicalFileSystem properties instead.")] + public interface IFileLastWriteTimeSource + { + DateTime GetLastWriteTimeUtc(string file); + + void SetLastWriteTimeUtc(string file, DateTime lastWriteTimeUtc); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PhysicalFileSystem/IPhysicalFileSystem.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PhysicalFileSystem/IPhysicalFileSystem.cs new file mode 100644 index 000000000000..09a6c402fc6a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PhysicalFileSystem/IPhysicalFileSystem.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem +{ + /// + /// Abstraction of FileSystem APIs, this allows unit tests to execute without touching hard drive. + /// But also allows host to not store anything on physical file system, instead everything can be + /// kept in memory and discarded after running. + /// + public interface IPhysicalFileSystem + { + /// + /// Same behavior as . + /// + bool DirectoryExists(string directory); + + /// + /// Same behavior as . + /// + bool FileExists(string file); + + /// + /// Same behavior as . + /// + Stream CreateFile(string path); + + /// + /// Same behavior as . + /// + void CreateDirectory(string path); + + /// + /// Same behavior as . + /// + string GetCurrentDirectory(); + + /// + /// Same behavior as . + /// + IEnumerable EnumerateFileSystemEntries(string directoryName, string pattern, SearchOption searchOption); + + /// + /// Same behavior as . + /// + void FileCopy(string sourcePath, string targetPath, bool overwrite); + + /// + /// Same behavior as . + /// + void DirectoryDelete(string path, bool recursive); + + /// + /// Same behavior as . + /// + string ReadAllText(string path); + + /// + /// Opens a binary file, reads the contents of the file into a byte array, and then closes the file. + /// Same behavior as . + /// + byte[] ReadAllBytes(string path); + + /// + /// Same behavior as . + /// + void WriteAllText(string path, string value); + + /// + /// Same behavior as . + /// + IEnumerable EnumerateDirectories(string path, string pattern, SearchOption searchOption); + + /// + /// Same behavior as . + /// + IEnumerable EnumerateFiles(string path, string pattern, SearchOption searchOption); + + /// + /// Same behavior as . + /// + Stream OpenRead(string path); + + /// + /// Same behavior as . + /// + void FileDelete(string path); + + /// + /// Same behavior as . + /// + FileAttributes GetFileAttributes(string file); + + /// + /// Same behavior as . + /// + void SetFileAttributes(string file, FileAttributes attributes); + + /// + /// Same behavior as . + /// + /// + /// Creates new which monitors specified path and on any changes calls callback. + /// To stop watching dispose returned object. + /// + IDisposable WatchFileChanges(string filePath, FileSystemEventHandler fileChanged); + + /// + /// Gets the last write time for the in UTC. + /// Same behavior as . + /// + /// The file to get last write time for. + /// + DateTime GetLastWriteTimeUtc(string file); + + /// + /// Sets the last write time for the in UTC. + /// Same behavior as . + /// + /// The file to set last write time for. + /// the time to set. + void SetLastWriteTimeUtc(string file, DateTime lastWriteTimeUtc); + + /// + /// If target is a sub-path of relativeTo a relative bath from relativeTo to sub-path will be returned. + /// + /// Path to be converted to relative if possible. + /// Base of the relative path to be returned. + /// + string PathRelativeTo(string target, string relativeTo); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PrecedenceDefinition.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PrecedenceDefinition.cs new file mode 100644 index 000000000000..696d4b595290 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PrecedenceDefinition.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions; + +/// +/// Indicates parameter defined precedence. +/// +public enum PrecedenceDefinition +{ + // If enable condition is set - parameter is conditionally disabled (regardless if require condition is set or not) + // Conditionally required is if and only if the only require condition is set. + + /// + /// Parameter value is unconditionally required. + /// + Required, + + /// + /// Set if and only if only the IsRequiredCondition is set. + /// + ConditionalyRequired, + + /// + /// Parameter value is not required from user. + /// + Optional, + + /// + /// Parameter value is implicitly populated. + /// + Implicit, + + /// + /// Parameter might become disabled - value would not be needed nor used in such case. + /// + ConditionalyDisabled, + + /// + /// Parameter is disabled - it's value is not required and will not be used. + /// + Disabled, +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PublicAPI.Shipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..83281993f106 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PublicAPI.Shipped.txt @@ -0,0 +1,469 @@ +#nullable enable +const Microsoft.TemplateEngine.Abstractions.Installer.InstallerConstants.NuGetSourcesSeparator = ';' -> char +const Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.BuiltIn.Author = "Author" -> string! +const Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.BuiltIn.Baseline = "Baseline" -> string! +const Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.BuiltIn.Classification = "Classification" -> string! +const Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.BuiltIn.Language = "Language" -> string! +const Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.BuiltIn.Name = "Name" -> string! +const Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.BuiltIn.ShortName = "ShortName" -> string! +const Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.BuiltIn.Type = "Type" -> string! +Microsoft.TemplateEngine.Abstractions.ChangeKind +Microsoft.TemplateEngine.Abstractions.ChangeKind.Change = 3 -> Microsoft.TemplateEngine.Abstractions.ChangeKind +Microsoft.TemplateEngine.Abstractions.ChangeKind.Create = 0 -> Microsoft.TemplateEngine.Abstractions.ChangeKind +Microsoft.TemplateEngine.Abstractions.ChangeKind.Delete = 1 -> Microsoft.TemplateEngine.Abstractions.ChangeKind +Microsoft.TemplateEngine.Abstractions.ChangeKind.Overwrite = 2 -> Microsoft.TemplateEngine.Abstractions.ChangeKind +Microsoft.TemplateEngine.Abstractions.IAllowDefaultIfOptionWithoutValue +Microsoft.TemplateEngine.Abstractions.IBaselineInfo +Microsoft.TemplateEngine.Abstractions.ICacheParameter +Microsoft.TemplateEngine.Abstractions.ICacheParameter.DataType.get -> string? +Microsoft.TemplateEngine.Abstractions.ICacheParameter.DefaultValue.get -> string? +Microsoft.TemplateEngine.Abstractions.ICacheParameter.Description.get -> string? +Microsoft.TemplateEngine.Abstractions.ICacheParameter.DisplayName.get -> string? +Microsoft.TemplateEngine.Abstractions.ICacheTag +Microsoft.TemplateEngine.Abstractions.ICacheTag.Choices.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.ICacheTag.DefaultValue.get -> string? +Microsoft.TemplateEngine.Abstractions.ICacheTag.Description.get -> string? +Microsoft.TemplateEngine.Abstractions.ICacheTag.DisplayName.get -> string? +Microsoft.TemplateEngine.Abstractions.IComponentManager +Microsoft.TemplateEngine.Abstractions.IComponentManager.AddComponent(System.Type! interfaceType, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! instance) -> void +Microsoft.TemplateEngine.Abstractions.ICreationEffects +Microsoft.TemplateEngine.Abstractions.ICreationEffects.CreationResult.get -> Microsoft.TemplateEngine.Abstractions.ICreationResult! +Microsoft.TemplateEngine.Abstractions.ICreationEffects.FileChanges.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.ICreationEffects2 +Microsoft.TemplateEngine.Abstractions.ICreationEffects2.CreationResult.get -> Microsoft.TemplateEngine.Abstractions.ICreationResult! +Microsoft.TemplateEngine.Abstractions.ICreationEffects2.FileChanges.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.ICreationPath +Microsoft.TemplateEngine.Abstractions.ICreationPath.Path.get -> string! +Microsoft.TemplateEngine.Abstractions.ICreationResult +Microsoft.TemplateEngine.Abstractions.ICreationResult.PostActions.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.ICreationResult.PrimaryOutputs.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings +Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings.Components.get -> Microsoft.TemplateEngine.Abstractions.IComponentManager! +Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings.Environment.get -> Microsoft.TemplateEngine.Abstractions.IEnvironment! +Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings.Host.get -> Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost! +Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings.Paths.get -> Microsoft.TemplateEngine.Abstractions.IPathInfo! +Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings.SettingsLoader.get -> Microsoft.TemplateEngine.Abstractions.ISettingsLoader! +Microsoft.TemplateEngine.Abstractions.IEnvironment +Microsoft.TemplateEngine.Abstractions.IEnvironment.ConsoleBufferWidth.get -> int +Microsoft.TemplateEngine.Abstractions.IEnvironment.ExpandEnvironmentVariables(string! name) -> string! +Microsoft.TemplateEngine.Abstractions.IEnvironment.GetEnvironmentVariable(string! name) -> string? +Microsoft.TemplateEngine.Abstractions.IEnvironment.GetEnvironmentVariables() -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.IEnvironment.NewLine.get -> string! +Microsoft.TemplateEngine.Abstractions.IFileChange +Microsoft.TemplateEngine.Abstractions.IFileChange.ChangeKind.get -> Microsoft.TemplateEngine.Abstractions.ChangeKind +Microsoft.TemplateEngine.Abstractions.IFileChange.Contents.get -> byte[]! +Microsoft.TemplateEngine.Abstractions.IFileChange.TargetRelativePath.get -> string! +Microsoft.TemplateEngine.Abstractions.IFileChange2 +Microsoft.TemplateEngine.Abstractions.IFileChange2.SourceRelativePath.get -> string! +Microsoft.TemplateEngine.Abstractions.IGenerator +Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent +Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent.Id.get -> System.Guid +Microsoft.TemplateEngine.Abstractions.ILocalizationLocator +Microsoft.TemplateEngine.Abstractions.Installer.CheckUpdateResult +Microsoft.TemplateEngine.Abstractions.Installer.CheckUpdateResult.IsLatestVersion.get -> bool +Microsoft.TemplateEngine.Abstractions.Installer.IInstaller +Microsoft.TemplateEngine.Abstractions.Installer.IInstallerFactory +Microsoft.TemplateEngine.Abstractions.Installer.InstallerConstants +Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode.AlreadyInstalled = 6 -> Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode.DownloadFailed = 3 -> Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode.GenericError = 5 -> Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode.InvalidPackage = 8 -> Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode.InvalidSource = 2 -> Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode.PackageNotFound = 1 -> Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode.Success = 0 -> Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode.UnsupportedRequest = 4 -> Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode.UpdateUninstallFailed = 7 -> Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallerOperationResult +Microsoft.TemplateEngine.Abstractions.Installer.InstallerOperationResult.Error.get -> Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallerOperationResult.Success.get -> bool +Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest +Microsoft.TemplateEngine.Abstractions.Installer.InstallResult +Microsoft.TemplateEngine.Abstractions.Installer.ISerializableInstaller +Microsoft.TemplateEngine.Abstractions.Installer.TemplatePackageData +Microsoft.TemplateEngine.Abstractions.Installer.TemplatePackageData.Details.get -> System.Collections.Generic.IReadOnlyDictionary? +Microsoft.TemplateEngine.Abstractions.Installer.TemplatePackageData.InstallerId.get -> System.Guid +Microsoft.TemplateEngine.Abstractions.Installer.TemplatePackageData.LastChangeTime.get -> System.DateTime +Microsoft.TemplateEngine.Abstractions.Installer.TemplatePackageData.MountPointUri.get -> string! +Microsoft.TemplateEngine.Abstractions.Installer.TemplatePackageData.TemplatePackageData(System.Guid installerId, string! mountPointUri, System.DateTime lastChangeTime, System.Collections.Generic.IReadOnlyDictionary? details) -> void +Microsoft.TemplateEngine.Abstractions.Installer.UninstallResult +Microsoft.TemplateEngine.Abstractions.Installer.UpdateRequest +Microsoft.TemplateEngine.Abstractions.Installer.UpdateResult +Microsoft.TemplateEngine.Abstractions.IParameterSet +Microsoft.TemplateEngine.Abstractions.IParameterSymbolLocalizationModel +Microsoft.TemplateEngine.Abstractions.IParameterSymbolLocalizationModel.Choices.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.IParameterSymbolLocalizationModel.Description.get -> string? +Microsoft.TemplateEngine.Abstractions.IParameterSymbolLocalizationModel.DisplayName.get -> string? +Microsoft.TemplateEngine.Abstractions.IParameterSymbolLocalizationModel.Name.get -> string! +Microsoft.TemplateEngine.Abstractions.IPathInfo +Microsoft.TemplateEngine.Abstractions.IPathInfo.GlobalSettingsDir.get -> string! +Microsoft.TemplateEngine.Abstractions.IPathInfo.HostSettingsDir.get -> string! +Microsoft.TemplateEngine.Abstractions.IPathInfo.HostVersionSettingsDir.get -> string! +Microsoft.TemplateEngine.Abstractions.IPathInfo.UserProfileDir.get -> string! +Microsoft.TemplateEngine.Abstractions.IPostAction +Microsoft.TemplateEngine.Abstractions.IPostAction.ActionId.get -> System.Guid +Microsoft.TemplateEngine.Abstractions.IPostAction.Args.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.IPostAction.ContinueOnError.get -> bool +Microsoft.TemplateEngine.Abstractions.IPostAction.Description.get -> string? +Microsoft.TemplateEngine.Abstractions.IPostAction.ManualInstructions.get -> string? +Microsoft.TemplateEngine.Abstractions.IPrioritizedComponent +Microsoft.TemplateEngine.Abstractions.IPrioritizedComponent.Priority.get -> int +Microsoft.TemplateEngine.Abstractions.ISearchPackFilter +Microsoft.TemplateEngine.Abstractions.ISettingsLoader +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.Save() -> void +Microsoft.TemplateEngine.Abstractions.IShortNameList +Microsoft.TemplateEngine.Abstractions.ISimpleConfigModifiers +Microsoft.TemplateEngine.Abstractions.ITemplate +Microsoft.TemplateEngine.Abstractions.ITemplate.IsNameAgreementWithFolderPreferred.get -> bool +Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost +Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost.BuiltInComponents.get -> System.Collections.Generic.IReadOnlyList<(System.Type! InterfaceType, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! Instance)>! +Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost.FallbackHostTemplateConfigNames.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost.FileSystem.get -> Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem! +Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost.HostIdentifier.get -> string! +Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost.Logger.get -> Microsoft.Extensions.Logging.ILogger! +Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost.LoggerFactory.get -> Microsoft.Extensions.Logging.ILoggerFactory! +Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost.OnPotentiallyDestructiveChangesDetected(System.Collections.Generic.IReadOnlyList! changes, System.Collections.Generic.IReadOnlyList! destructiveChanges) -> bool +Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost.TryGetHostParamDefault(string! paramName, out string? value) -> bool +Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost.Version.get -> string! +Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost.VirtualizeDirectory(string! path) -> void +Microsoft.TemplateEngine.Abstractions.ITemplateInfo +Microsoft.TemplateEngine.Abstractions.ITemplateInfo.CacheParameters.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.ITemplateInfo.HasScriptRunningPostActions.get -> bool +Microsoft.TemplateEngine.Abstractions.ITemplateInfo.HasScriptRunningPostActions.set -> void +Microsoft.TemplateEngine.Abstractions.ITemplateInfo.Parameters.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.ITemplateInfo.ShortName.get -> string! +Microsoft.TemplateEngine.Abstractions.ITemplateInfo.Tags.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.ITemplateParameter +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.Choices.get -> System.Collections.Generic.IReadOnlyDictionary? +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.DataType.get -> string! +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.DefaultIfOptionWithoutValue.get -> string? +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.DefaultValue.get -> string? +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.Description.get -> string? +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.DisplayName.get -> string? +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.Documentation.get -> string? +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.IsName.get -> bool +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.Name.get -> string! +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.Priority.get -> Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.Type.get -> string! +Microsoft.TemplateEngine.Abstractions.Mount.FileSystemInfoKind +Microsoft.TemplateEngine.Abstractions.Mount.FileSystemInfoKind.Directory = 1 -> Microsoft.TemplateEngine.Abstractions.Mount.FileSystemInfoKind +Microsoft.TemplateEngine.Abstractions.Mount.FileSystemInfoKind.File = 0 -> Microsoft.TemplateEngine.Abstractions.Mount.FileSystemInfoKind +Microsoft.TemplateEngine.Abstractions.Mount.IDirectory +Microsoft.TemplateEngine.Abstractions.Mount.IFile +Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo +Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo.Exists.get -> bool +Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo.Kind.get -> Microsoft.TemplateEngine.Abstractions.Mount.FileSystemInfoKind +Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint +Microsoft.TemplateEngine.Abstractions.Mount.IMountPointFactory +Microsoft.TemplateEngine.Abstractions.Mount.IMountPointManager +Microsoft.TemplateEngine.Abstractions.ParameterChoice +Microsoft.TemplateEngine.Abstractions.ParameterChoice.Description.get -> string? +Microsoft.TemplateEngine.Abstractions.ParameterChoice.Description.set -> void +Microsoft.TemplateEngine.Abstractions.ParameterChoice.DisplayName.get -> string? +Microsoft.TemplateEngine.Abstractions.ParameterChoice.DisplayName.set -> void +Microsoft.TemplateEngine.Abstractions.ParameterChoice.Localize(Microsoft.TemplateEngine.Abstractions.ParameterChoiceLocalizationModel! localizationModel) -> void +Microsoft.TemplateEngine.Abstractions.ParameterChoice.ParameterChoice(string? displayName, string? description) -> void +Microsoft.TemplateEngine.Abstractions.ParameterChoiceLocalizationModel +Microsoft.TemplateEngine.Abstractions.ParameterChoiceLocalizationModel.Description.get -> string? +Microsoft.TemplateEngine.Abstractions.ParameterChoiceLocalizationModel.DisplayName.get -> string? +Microsoft.TemplateEngine.Abstractions.ParameterChoiceLocalizationModel.ParameterChoiceLocalizationModel(string? displayName, string? description) -> void +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IFileLastWriteTimeSource +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.CreateDirectory(string! path) -> void +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.CreateFile(string! path) -> System.IO.Stream! +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.DirectoryDelete(string! path, bool recursive) -> void +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.DirectoryExists(string! directory) -> bool +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.EnumerateDirectories(string! path, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.EnumerateFiles(string! path, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.EnumerateFileSystemEntries(string! directoryName, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.FileCopy(string! sourcePath, string! targetPath, bool overwrite) -> void +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.FileDelete(string! path) -> void +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.FileExists(string! file) -> bool +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.GetCurrentDirectory() -> string! +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.GetFileAttributes(string! file) -> System.IO.FileAttributes +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.GetLastWriteTimeUtc(string! file) -> System.DateTime +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.OpenRead(string! path) -> System.IO.Stream! +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.ReadAllBytes(string! path) -> byte[]! +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.ReadAllText(string! path) -> string! +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.SetFileAttributes(string! file, System.IO.FileAttributes attributes) -> void +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.SetLastWriteTimeUtc(string! file, System.DateTime lastWriteTimeUtc) -> void +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.WatchFileChanges(string! filePath, System.IO.FileSystemEventHandler! fileChanged) -> System.IDisposable! +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.WriteAllText(string! path, string! value) -> void +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo.AddMatchDisposition(Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo! newDisposition) -> void +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo.Info.get -> Microsoft.TemplateEngine.Abstractions.ITemplateInfo! +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo.MatchDisposition.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.BuiltIn +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.Kind.get -> Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.MatchInfo(string! name, string? value, Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind kind) -> void +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.Name.get -> string! +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.Value.get -> string? +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind.AmbiguousValue = 5 -> Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind.Exact = 0 -> Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind.InvalidName = 3 -> Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind.InvalidValue = 4 -> Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind.Mismatch = 2 -> Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind.Partial = 1 -> Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind +Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind.SingleStartsWith = 6 -> Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchKind +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider +Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackage +Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackage.LastChangeTime.get -> System.DateTime +Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProvider +Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProvider.TemplatePackagesChanged -> System.Action? +Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProviderFactory +Microsoft.TemplateEngine.Abstractions.TemplatePackage.TemplatePackage +Microsoft.TemplateEngine.Abstractions.TemplatePackage.TemplatePackage.LastChangeTime.get -> System.DateTime +Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority +Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority.Implicit = 3 -> Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority +Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority.Optional = 2 -> Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority +Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority.Required = 0 -> Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority +Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority.Suggested = 1 -> Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority +Microsoft.TemplateEngine.Abstractions.IBaselineInfo.DefaultOverrides.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.IBaselineInfo.Description.get -> string? +Microsoft.TemplateEngine.Abstractions.IComponentManager.OfType() -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.IComponentManager.Register(System.Type! type) -> void +Microsoft.TemplateEngine.Abstractions.IComponentManager.RegisterMany(System.Collections.Generic.IEnumerable! typeList) -> void +Microsoft.TemplateEngine.Abstractions.IComponentManager.TryGetComponent(System.Guid id, out T? component) -> bool +const Microsoft.TemplateEngine.Abstractions.Installer.InstallerConstants.InteractiveModeKey = "Interactive" -> string! +const Microsoft.TemplateEngine.Abstractions.Installer.InstallerConstants.NuGetSourcesKey = "NuGetSources" -> string! +const Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.BuiltIn.Constraint = "Constraint" -> string! +Microsoft.TemplateEngine.Abstractions.Components.IBindSymbolSource +Microsoft.TemplateEngine.Abstractions.Components.IBindSymbolSource.DisplayName.get -> string! +Microsoft.TemplateEngine.Abstractions.Components.IBindSymbolSource.GetBoundValueAsync(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! settings, string! bindName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.Components.IBindSymbolSource.RequiresPrefixMatch.get -> bool +Microsoft.TemplateEngine.Abstractions.Components.IBindSymbolSource.SourcePrefix.get -> string? +Microsoft.TemplateEngine.Abstractions.Components.ISdkInfoProvider +Microsoft.TemplateEngine.Abstractions.Components.ISdkInfoProvider.GetCurrentVersionAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.Components.ISdkInfoProvider.GetInstalledVersionsAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Abstractions.Components.ISdkInfoProvider.ProvideConstraintRemedySuggestion(System.Collections.Generic.IReadOnlyList! supportedVersions, System.Collections.Generic.IReadOnlyList! viableInstalledVersions) -> string! +Microsoft.TemplateEngine.Abstractions.Components.IWorkloadsInfoProvider.GetInstalledWorkloadsAsync(System.Threading.CancellationToken token) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Abstractions.Components.IWorkloadsInfoProvider.ProvideConstraintRemedySuggestion(System.Collections.Generic.IReadOnlyList! supportedWorkloads) -> string! +Microsoft.TemplateEngine.Abstractions.Components.WorkloadInfo.Description.get -> string! +Microsoft.TemplateEngine.Abstractions.Components.WorkloadInfo.Id.get -> string! +Microsoft.TemplateEngine.Abstractions.Components.WorkloadInfo.WorkloadInfo(string! id, string! description) -> void +Microsoft.TemplateEngine.Abstractions.Constraints.ITemplateConstraint +Microsoft.TemplateEngine.Abstractions.Constraints.ITemplateConstraint.DisplayName.get -> string! +Microsoft.TemplateEngine.Abstractions.Constraints.ITemplateConstraint.Evaluate(string? args) -> Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult! +Microsoft.TemplateEngine.Abstractions.Constraints.ITemplateConstraint.Type.get -> string! +Microsoft.TemplateEngine.Abstractions.Constraints.ITemplateConstraintFactory +Microsoft.TemplateEngine.Abstractions.Constraints.ITemplateConstraintFactory.CreateTemplateConstraintAsync(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.Constraints.ITemplateConstraintFactory.Type.get -> string! +Microsoft.TemplateEngine.Abstractions.Components.IWorkloadsInfoProvider +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintInfo +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintInfo.Args.get -> string? +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintInfo.TemplateConstraintInfo(string! type, string? args) -> void +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintInfo.Type.get -> string! +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.CallToAction.get -> string? +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.Constraint.get -> Microsoft.TemplateEngine.Abstractions.Constraints.ITemplateConstraint? +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.ConstraintType.get -> string! +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.EvaluationStatus.get -> Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.Status +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.LocalizedErrorMessage.get -> string? +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.Status +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.Status.Allowed = 1 -> Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.Status +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.Status.NotEvaluated = 0 -> Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.Status +Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.Status.Restricted = 2 -> Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.Status +Microsoft.TemplateEngine.Abstractions.Components.WorkloadInfo +Microsoft.TemplateEngine.Abstractions.EvaluatedPrecedence +Microsoft.TemplateEngine.Abstractions.EvaluatedPrecedence.Disabled = 3 -> Microsoft.TemplateEngine.Abstractions.EvaluatedPrecedence +Microsoft.TemplateEngine.Abstractions.EvaluatedPrecedence.Implicit = 2 -> Microsoft.TemplateEngine.Abstractions.EvaluatedPrecedence +Microsoft.TemplateEngine.Abstractions.EvaluatedPrecedence.Optional = 1 -> Microsoft.TemplateEngine.Abstractions.EvaluatedPrecedence +Microsoft.TemplateEngine.Abstractions.EvaluatedPrecedence.Required = 0 -> Microsoft.TemplateEngine.Abstractions.EvaluatedPrecedence +Microsoft.TemplateEngine.Abstractions.IAllowDefaultIfOptionWithoutValue.DefaultIfOptionWithoutValue.get -> string? +Microsoft.TemplateEngine.Abstractions.IAllowDefaultIfOptionWithoutValue.DefaultIfOptionWithoutValue.set -> void +Microsoft.TemplateEngine.Abstractions.IGenerator.ConvertParameterValueToType(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, Microsoft.TemplateEngine.Abstractions.ITemplateParameter! parameter, string! untypedValue, out bool valueResolutionError) -> object? +Microsoft.TemplateEngine.Abstractions.IGenerator.CreateAsync(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, Microsoft.TemplateEngine.Abstractions.ITemplate! template, Microsoft.TemplateEngine.Abstractions.IParameterSet! parameters, string! targetDirectory, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.IGenerator.CreateAsync(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, Microsoft.TemplateEngine.Abstractions.ITemplate! template, Microsoft.TemplateEngine.Abstractions.Parameters.IParameterSetData! parameters, string! targetDirectory, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.IGenerator.GetCreationEffectsAsync(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, Microsoft.TemplateEngine.Abstractions.ITemplate! template, Microsoft.TemplateEngine.Abstractions.IParameterSet! parameters, string! targetDirectory, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.IGenerator.GetCreationEffectsAsync(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, Microsoft.TemplateEngine.Abstractions.ITemplate! template, Microsoft.TemplateEngine.Abstractions.Parameters.IParameterSetData! parameters, string! targetDirectory, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.IGenerator.GetParametersForTemplate(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, Microsoft.TemplateEngine.Abstractions.ITemplate! template) -> Microsoft.TemplateEngine.Abstractions.IParameterSet! +Microsoft.TemplateEngine.Abstractions.IGenerator.GetTemplatesAndLangpacksFromDir(Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint! source, out System.Collections.Generic.IList! localizations) -> System.Collections.Generic.IList! +Microsoft.TemplateEngine.Abstractions.IGenerator.TryEvaluateFromString(Microsoft.Extensions.Logging.ILogger! logger, string! text, System.Collections.Generic.IDictionary! variables, out bool result, out string! evaluationError, System.Collections.Generic.HashSet? referencedVariablesKeys = null) -> bool +Microsoft.TemplateEngine.Abstractions.IGenerator.TryGetTemplateFromConfigInfo(Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo! config, out Microsoft.TemplateEngine.Abstractions.ITemplate? template, Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo? localeConfig, Microsoft.TemplateEngine.Abstractions.Mount.IFile? hostTemplateConfigFile, string? baselineName = null) -> bool +Microsoft.TemplateEngine.Abstractions.ILocalizationLocator.Author.get -> string? +Microsoft.TemplateEngine.Abstractions.ILocalizationLocator.ConfigPlace.get -> string! +Microsoft.TemplateEngine.Abstractions.ILocalizationLocator.Description.get -> string? +Microsoft.TemplateEngine.Abstractions.ILocalizationLocator.Identity.get -> string! +Microsoft.TemplateEngine.Abstractions.ILocalizationLocator.Locale.get -> string! +Microsoft.TemplateEngine.Abstractions.ILocalizationLocator.Name.get -> string? +Microsoft.TemplateEngine.Abstractions.ILocalizationLocator.ParameterSymbols.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.IParameterSet.ParameterDefinitions.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.IParameterSet.ResolvedValues.get -> System.Collections.Generic.IDictionary! +Microsoft.TemplateEngine.Abstractions.IParameterSet.TryGetParameterDefinition(string! name, out Microsoft.TemplateEngine.Abstractions.ITemplateParameter! parameter) -> bool +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.Precedence.get -> Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence! +Microsoft.TemplateEngine.Abstractions.Mount.IDirectory.EnumerateDirectories(string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.Mount.IDirectory.EnumerateFiles(string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.Mount.IDirectory.EnumerateFileSystemInfos(string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.Mount.IFile.OpenRead() -> System.IO.Stream! +Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo.FullPath.get -> string! +Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo.MountPoint.get -> Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint! +Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo.Name.get -> string! +Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo.Parent.get -> Microsoft.TemplateEngine.Abstractions.Mount.IDirectory? +Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint.DirectoryInfo(string! path) -> Microsoft.TemplateEngine.Abstractions.Mount.IDirectory? +Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint.EnvironmentSettings.get -> Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! +Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint.FileInfo(string! path) -> Microsoft.TemplateEngine.Abstractions.Mount.IFile? +Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint.FileSystemInfo(string! path) -> Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo? +Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint.MountPointUri.get -> string! +Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint.Root.get -> Microsoft.TemplateEngine.Abstractions.Mount.IDirectory! +Microsoft.TemplateEngine.Abstractions.Mount.IMountPointFactory.TryMount(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint? parent, string! mountPointUri, out Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint? mountPoint) -> bool +Microsoft.TemplateEngine.Abstractions.Mount.IMountPointManager.EnvironmentSettings.get -> Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! +Microsoft.TemplateEngine.Abstractions.Mount.IMountPointManager.TryDemandMountPoint(string! mountPointUri, out Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint! mountPoint) -> bool +Microsoft.TemplateEngine.Abstractions.Parameters.DataSource +Microsoft.TemplateEngine.Abstractions.Parameters.DataSource.Default = 4 -> Microsoft.TemplateEngine.Abstractions.Parameters.DataSource +Microsoft.TemplateEngine.Abstractions.Parameters.DataSource.DefaultIfNoValue = 5 -> Microsoft.TemplateEngine.Abstractions.Parameters.DataSource +Microsoft.TemplateEngine.Abstractions.Parameters.DataSource.HostDefault = 2 -> Microsoft.TemplateEngine.Abstractions.Parameters.DataSource +Microsoft.TemplateEngine.Abstractions.Parameters.DataSource.HostOnError = 3 -> Microsoft.TemplateEngine.Abstractions.Parameters.DataSource +Microsoft.TemplateEngine.Abstractions.Parameters.DataSource.HostOther = 7 -> Microsoft.TemplateEngine.Abstractions.Parameters.DataSource +Microsoft.TemplateEngine.Abstractions.Parameters.DataSource.NameParameter = 6 -> Microsoft.TemplateEngine.Abstractions.Parameters.DataSource +Microsoft.TemplateEngine.Abstractions.Parameters.DataSource.NoSource = 0 -> Microsoft.TemplateEngine.Abstractions.Parameters.DataSource +Microsoft.TemplateEngine.Abstractions.Parameters.DataSource.User = 1 -> Microsoft.TemplateEngine.Abstractions.Parameters.DataSource +Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet +Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet.AsReadonlyDictionary() -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet.ContainsKey(string! key) -> bool +Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet.Keys.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet.this[string! key].get -> Microsoft.TemplateEngine.Abstractions.ITemplateParameter! +Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet.TryGetValue(string! key, out Microsoft.TemplateEngine.Abstractions.ITemplateParameter! value) -> bool +Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet.Values.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.Parameters.IParameterSetData +Microsoft.TemplateEngine.Abstractions.Parameters.IParameterSetData.ParametersDefinition.get -> Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterData +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterData.DataSource.get -> Microsoft.TemplateEngine.Abstractions.Parameters.DataSource +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterData.IsEnabled.get -> bool +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterData.ParameterData(Microsoft.TemplateEngine.Abstractions.ITemplateParameter! parameterDefinition, object? value, Microsoft.TemplateEngine.Abstractions.Parameters.DataSource source, bool isEnabled = true) -> void +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterData.ParameterDefinition.get -> Microsoft.TemplateEngine.Abstractions.ITemplateParameter! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterData.Value.get -> object? +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.AsReadonlyDictionary() -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.ContainsKey(string! key) -> bool +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.Count.get -> int +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.GetEnumerator() -> System.Collections.Generic.IEnumerator! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.Keys.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.ParameterDefinitionSet(Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet! other) -> void +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.ParameterDefinitionSet(System.Collections.Generic.IEnumerable? parameters) -> void +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.ParameterDefinitionSet(System.Collections.Generic.IReadOnlyDictionary? parameters) -> void +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.this[int index].get -> Microsoft.TemplateEngine.Abstractions.ITemplateParameter! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.this[string! key].get -> Microsoft.TemplateEngine.Abstractions.ITemplateParameter! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.TryGetValue(string! key, out Microsoft.TemplateEngine.Abstractions.ITemplateParameter! value) -> bool +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.Values.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.ContainsKey(Microsoft.TemplateEngine.Abstractions.ITemplateParameter! key) -> bool +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.Count.get -> int +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.GetEnumerator() -> System.Collections.Generic.IEnumerator>! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.Keys.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.ParametersDefinition.get -> Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.ParameterSetData(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! templateInfo) -> void +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.ParameterSetData(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! templateInfo, System.Collections.Generic.IReadOnlyDictionary? inputParameters) -> void +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.ParameterSetData(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! templateInfo, System.Collections.Generic.IReadOnlyDictionary? inputParameters) -> void +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.ParameterSetData(Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet! parameters, System.Collections.Generic.IReadOnlyList! parameterData) -> void +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.this[Microsoft.TemplateEngine.Abstractions.ITemplateParameter! key].get -> Microsoft.TemplateEngine.Abstractions.Parameters.ParameterData! +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.TryGetValue(Microsoft.TemplateEngine.Abstractions.ITemplateParameter! key, out Microsoft.TemplateEngine.Abstractions.Parameters.ParameterData! value) -> bool +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.Values.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IFileLastWriteTimeSource.GetLastWriteTimeUtc(string! file) -> System.DateTime +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IFileLastWriteTimeSource.SetLastWriteTimeUtc(string! file, System.DateTime lastWriteTimeUtc) -> void +Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem.PathRelativeTo(string! target, string! relativeTo) -> string! +Microsoft.TemplateEngine.Abstractions.Installer.CheckUpdateResult.LatestVersion.get -> string? +Microsoft.TemplateEngine.Abstractions.Installer.IInstaller.CanInstallAsync(Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest! installationRequest, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.Installer.IInstaller.Factory.get -> Microsoft.TemplateEngine.Abstractions.Installer.IInstallerFactory! +Microsoft.TemplateEngine.Abstractions.Installer.IInstaller.GetLatestVersionAsync(System.Collections.Generic.IEnumerable! templatePackages, Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider! provider, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Abstractions.Installer.IInstaller.InstallAsync(Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest! installRequest, Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider! provider, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.Installer.IInstaller.UninstallAsync(Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! templatePackage, Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider! provider, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.Installer.IInstaller.UpdateAsync(Microsoft.TemplateEngine.Abstractions.Installer.UpdateRequest! updateRequest, Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider! provider, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.Installer.IInstallerFactory.CreateInstaller(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! settings, string! installPath) -> Microsoft.TemplateEngine.Abstractions.Installer.IInstaller! +Microsoft.TemplateEngine.Abstractions.Installer.IInstallerFactory.Name.get -> string! +Microsoft.TemplateEngine.Abstractions.Installer.InstallerOperationResult.ErrorMessage.get -> string? +Microsoft.TemplateEngine.Abstractions.Installer.InstallerOperationResult.InstallerOperationResult(Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode error, string? errorMessage, Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage? templatePackage = null) -> void +Microsoft.TemplateEngine.Abstractions.Installer.InstallerOperationResult.InstallerOperationResult(Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! managedTemplatePackage) -> void +Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest.Details.get -> System.Collections.Generic.Dictionary! +Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest.DisplayName.get -> string! +Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest.Force.get -> bool +Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest.InstallerName.get -> string? +Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest.InstallRequest(string! identifier, string? version = null, string? installerName = null, System.Collections.Generic.Dictionary? details = null, bool force = false) -> void +Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest.PackageIdentifier.get -> string! +Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest.Version.get -> string? +Microsoft.TemplateEngine.Abstractions.Installer.InstallResult.InstallRequest.get -> Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest! +Microsoft.TemplateEngine.Abstractions.Installer.ISerializableInstaller.Deserialize(Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider! provider, Microsoft.TemplateEngine.Abstractions.Installer.TemplatePackageData! data) -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! +Microsoft.TemplateEngine.Abstractions.Installer.ISerializableInstaller.Serialize(Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! templatePackage) -> Microsoft.TemplateEngine.Abstractions.Installer.TemplatePackageData! +Microsoft.TemplateEngine.Abstractions.Installer.UpdateRequest.TemplatePackage.get -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! +Microsoft.TemplateEngine.Abstractions.Installer.UpdateRequest.UpdateRequest(Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! templatePackage, string! targetVersion) -> void +Microsoft.TemplateEngine.Abstractions.Installer.UpdateRequest.Version.get -> string! +Microsoft.TemplateEngine.Abstractions.Installer.UpdateResult.UpdateRequest.get -> Microsoft.TemplateEngine.Abstractions.Installer.UpdateRequest! +Microsoft.TemplateEngine.Abstractions.ISearchPackFilter.ShouldPackBeFiltered(string! candidatePackName, string! candidatePackVersion) -> bool +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.AddMountPoint(Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint! mountPoint) -> void +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.AddProbingPath(string! probeIn) -> void +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.Components.get -> Microsoft.TemplateEngine.Abstractions.IComponentManager! +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.EnvironmentSettings.get -> Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.FindBestHostTemplateConfigFile(Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo! config) -> Microsoft.TemplateEngine.Abstractions.Mount.IFile! +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.GetTemplates(System.Collections.Generic.HashSet! templates) -> void +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.LoadTemplate(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! info, string! baselineName) -> Microsoft.TemplateEngine.Abstractions.ITemplate! +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.MountPoints.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.ReleaseMountPoint(Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint! mountPoint) -> void +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.RemoveMountPoint(Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint! mountPoint) -> void +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.RemoveMountPoints(System.Collections.Generic.IEnumerable! mountPoints) -> void +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.TryGetFileFromIdAndPath(System.Guid mountPointId, string! place, out Microsoft.TemplateEngine.Abstractions.Mount.IFile! file, out Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint! mountPoint) -> bool +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.TryGetMountPointFromPlace(string! mountPointPlace, out Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint! mountPoint) -> bool +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.TryGetMountPointInfo(System.Guid mountPointId, out object! info) -> bool +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.WriteTemplateCache(System.Collections.Generic.IList! templates, string! locale) -> void +Microsoft.TemplateEngine.Abstractions.ISettingsLoader.WriteTemplateCache(System.Collections.Generic.IList! templates, string! locale, bool hasContentChanges) -> void +Microsoft.TemplateEngine.Abstractions.IShortNameList.ShortNameList.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.ISimpleConfigModifiers.BaselineName.get -> string! +Microsoft.TemplateEngine.Abstractions.ITemplate.Configuration.get -> Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo! +Microsoft.TemplateEngine.Abstractions.ITemplate.Generator.get -> Microsoft.TemplateEngine.Abstractions.IGenerator! +Microsoft.TemplateEngine.Abstractions.ITemplate.LocaleConfiguration.get -> Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo? +Microsoft.TemplateEngine.Abstractions.ITemplateParameter.AllowMultipleValues.get -> bool +Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition +Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition.ConditionalyDisabled = 4 -> Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition +Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition.ConditionalyRequired = 1 -> Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition +Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition.Disabled = 5 -> Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition +Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition.Implicit = 3 -> Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition +Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition.Optional = 2 -> Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition +Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition.Required = 0 -> Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage.DisplayName.get -> string! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage.GetDetails() -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage.Identifier.get -> string! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage.Installer.get -> Microsoft.TemplateEngine.Abstractions.Installer.IInstaller! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage.IsLocalPackage.get -> bool +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage.ManagedProvider.get -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage.Version.get -> string? +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider.GetLatestVersionsAsync(System.Collections.Generic.IEnumerable! templatePackages, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider.InstallAsync(System.Collections.Generic.IEnumerable! installRequests, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider.UninstallAsync(System.Collections.Generic.IEnumerable! templatePackages, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider.UpdateAsync(System.Collections.Generic.IEnumerable! updateRequests, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackage.MountPointUri.get -> string! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackage.Provider.get -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProvider! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProvider.Factory.get -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProviderFactory! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProvider.GetAllTemplatePackagesAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProviderFactory.CreateProvider(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! settings) -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProvider! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProviderFactory.DisplayName.get -> string! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.TemplatePackage.MountPointUri.get -> string! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.TemplatePackage.Provider.get -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProvider! +Microsoft.TemplateEngine.Abstractions.TemplatePackage.TemplatePackage.TemplatePackage(Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProvider! provider, string! mountPointUri, System.DateTime lastChangeTime) -> void +Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence +Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence.CanBeRequired.get -> bool +Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence.IsEnabledCondition.get -> string? +Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence.IsRequired.get -> bool +Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence.IsRequiredCondition.get -> string? +Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence.PrecedenceDefinition.get -> Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition +Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence.TemplateParameterPrecedence(Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition precedenceDefinition, string? isRequiredCondition = null, string? isEnabledCondition = null, bool isRequired = false) -> void +Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedenceExtensions +override Microsoft.TemplateEngine.Abstractions.Installer.CheckUpdateResult.TemplatePackage.get -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! +override Microsoft.TemplateEngine.Abstractions.Installer.UninstallResult.TemplatePackage.get -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! +override Microsoft.TemplateEngine.Abstractions.Parameters.ParameterData.ToString() -> string! +static Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.CreateAllowed(Microsoft.TemplateEngine.Abstractions.Constraints.ITemplateConstraint! constraint) -> Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult! +static Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.CreateEvaluationFailure(Microsoft.TemplateEngine.Abstractions.Constraints.ITemplateConstraint! constraint, string! localizedErrorMessage, string? cta = null) -> Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult! +static Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.CreateInitializationFailure(string! type, string! localizedErrorMessage, string? cta = null) -> Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult! +static Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult.CreateRestricted(Microsoft.TemplateEngine.Abstractions.Constraints.ITemplateConstraint! constraint, string! localizedErrorMessage, string? cta = null) -> Microsoft.TemplateEngine.Abstractions.Constraints.TemplateConstraintResult! +static Microsoft.TemplateEngine.Abstractions.Installer.CheckUpdateResult.CreateSuccess(Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! templatePackage, string? version, bool isLatest) -> Microsoft.TemplateEngine.Abstractions.Installer.CheckUpdateResult! +static Microsoft.TemplateEngine.Abstractions.Installer.UninstallResult.CreateFailure(Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! templatePackage, Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode error, string! localizedFailureMessage) -> Microsoft.TemplateEngine.Abstractions.Installer.UninstallResult! +static Microsoft.TemplateEngine.Abstractions.Installer.UninstallResult.CreateSuccess(Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! templatePackage) -> Microsoft.TemplateEngine.Abstractions.Installer.UninstallResult! +static Microsoft.TemplateEngine.Abstractions.Installer.UpdateResult.FromInstallResult(Microsoft.TemplateEngine.Abstractions.Installer.UpdateRequest! request, Microsoft.TemplateEngine.Abstractions.Installer.InstallResult! installResult) -> Microsoft.TemplateEngine.Abstractions.Installer.UpdateResult! +static Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedenceExtensions.ToPrecedenceDefinition(this Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority priority) -> Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition +static Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedenceExtensions.ToTemplateParameterPrecedence(this Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority priority) -> Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence! +static Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedenceExtensions.ToTemplateParameterPriority(this Microsoft.TemplateEngine.Abstractions.PrecedenceDefinition precedenceDefinition) -> Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority +static readonly Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet.Empty -> Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet! +static readonly Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence.Default -> Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence! +virtual Microsoft.TemplateEngine.Abstractions.Installer.InstallerOperationResult.TemplatePackage.get -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage? +override Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest.ToString() -> string! + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PublicAPI.Unshipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..c5812ae7b1b7 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/PublicAPI.Unshipped.txt @@ -0,0 +1,76 @@ +Microsoft.TemplateEngine.Abstractions.IExtendedTemplateLocator +Microsoft.TemplateEngine.Abstractions.Installer.CheckUpdateResult.Vulnerabilities.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode.VulnerablePackage = 9 -> Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode +Microsoft.TemplateEngine.Abstractions.Installer.InstallResult.Vulnerabilities.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.Installer.UpdateResult.Vulnerabilities.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.Installer.VulnerabilityInfo +Microsoft.TemplateEngine.Abstractions.Installer.VulnerabilityInfo.AdvisoryUris.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.Installer.VulnerabilityInfo.AdvisoryUris.init -> void +Microsoft.TemplateEngine.Abstractions.Installer.VulnerabilityInfo.Severity.get -> int +Microsoft.TemplateEngine.Abstractions.Installer.VulnerabilityInfo.Severity.init -> void +Microsoft.TemplateEngine.Abstractions.Installer.VulnerabilityInfo.VulnerabilityInfo() -> void +Microsoft.TemplateEngine.Abstractions.Installer.VulnerabilityInfo.VulnerabilityInfo(int Severity, System.Collections.Generic.IReadOnlyList! AdvisoryUris) -> void +Microsoft.TemplateEngine.Abstractions.ITemplate.Localization.get -> Microsoft.TemplateEngine.Abstractions.ILocalizationLocator? +Microsoft.TemplateEngine.Abstractions.ITemplate.TemplateSourceRoot.get -> Microsoft.TemplateEngine.Abstractions.Mount.IDirectory! +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.PreferDefaultName.get -> bool +Microsoft.TemplateEngine.Abstractions.IValidationInfo.IsValid.get -> bool +Microsoft.TemplateEngine.Abstractions.IVariableCollection +Microsoft.TemplateEngine.Abstractions.IVariableCollection.Parent.get -> Microsoft.TemplateEngine.Abstractions.IVariableCollection? +Microsoft.TemplateEngine.Abstractions.IVariableCollection.Parent.set -> void +Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetDataExtensions +static Microsoft.TemplateEngine.Abstractions.Installer.CheckUpdateResult.CreateFailure(Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! templatePackage, Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode error, string! localizedFailureMessage, System.Collections.Generic.IReadOnlyList! vulnerabilities) -> Microsoft.TemplateEngine.Abstractions.Installer.CheckUpdateResult! +static Microsoft.TemplateEngine.Abstractions.Installer.InstallResult.CreateFailure(Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest! request, Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode error, string! localizedFailureMessage, System.Collections.Generic.IReadOnlyList! vulnerabilities) -> Microsoft.TemplateEngine.Abstractions.Installer.InstallResult! +static Microsoft.TemplateEngine.Abstractions.Installer.InstallResult.CreateSuccess(Microsoft.TemplateEngine.Abstractions.Installer.InstallRequest! request, Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! templatePackage, System.Collections.Generic.IReadOnlyList! vulnerabilities) -> Microsoft.TemplateEngine.Abstractions.Installer.InstallResult! +static Microsoft.TemplateEngine.Abstractions.Installer.UpdateResult.CreateFailure(Microsoft.TemplateEngine.Abstractions.Installer.UpdateRequest! request, Microsoft.TemplateEngine.Abstractions.Installer.InstallerErrorCode error, string! localizedFailureMessage, System.Collections.Generic.IReadOnlyList! vulnerabilities) -> Microsoft.TemplateEngine.Abstractions.Installer.UpdateResult! +static Microsoft.TemplateEngine.Abstractions.Installer.UpdateResult.CreateSuccess(Microsoft.TemplateEngine.Abstractions.Installer.UpdateRequest! request, Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage! templatePackage, System.Collections.Generic.IReadOnlyList! vulnerabilities) -> Microsoft.TemplateEngine.Abstractions.Installer.UpdateResult! +static Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetData.Empty.get -> Microsoft.TemplateEngine.Abstractions.Parameters.IParameterSetData! +static Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetDataExtensions.GetValue(this Microsoft.TemplateEngine.Abstractions.Parameters.IParameterSetData! data, string! parameterName) -> Microsoft.TemplateEngine.Abstractions.Parameters.ParameterData! +static Microsoft.TemplateEngine.Abstractions.Parameters.ParameterSetDataExtensions.TryGetValue(this Microsoft.TemplateEngine.Abstractions.Parameters.IParameterSetData! data, string! parameterName, out Microsoft.TemplateEngine.Abstractions.Parameters.ParameterData? parameterData) -> bool +Microsoft.TemplateEngine.Abstractions.IGenerator.GetTemplatesFromMountPointAsync(Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint! source, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Abstractions.IGenerator.LoadTemplateAsync(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! settings, Microsoft.TemplateEngine.Abstractions.ITemplateLocator! config, string? baselineName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Abstractions.IExtendedTemplateLocator.HostConfigPlace.get -> string? +Microsoft.TemplateEngine.Abstractions.IExtendedTemplateLocator.LocaleConfigPlace.get -> string? +Microsoft.TemplateEngine.Abstractions.IScanTemplateInfo +Microsoft.TemplateEngine.Abstractions.IScanTemplateInfo.HostConfigFiles.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.IScanTemplateInfo.Localizations.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.ITemplate.HostSpecificConfiguration.get -> Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo? +Microsoft.TemplateEngine.Abstractions.ITemplateLocator +Microsoft.TemplateEngine.Abstractions.ITemplateLocator.ConfigPlace.get -> string! +Microsoft.TemplateEngine.Abstractions.ITemplateLocator.GeneratorId.get -> System.Guid +Microsoft.TemplateEngine.Abstractions.ITemplateLocator.MountPointUri.get -> string! +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.Author.get -> string? +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.BaselineInfo.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.Classifications.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.Constraints.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.DefaultName.get -> string? +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.Description.get -> string? +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.GroupIdentity.get -> string? +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.Identity.get -> string! +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.Name.get -> string! +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.ParameterDefinitions.get -> Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet! +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.PostActions.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.Precedence.get -> int +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.ShortNameList.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.TagsCollection.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Abstractions.ITemplateMetadata.ThirdPartyNotices.get -> string? +Microsoft.TemplateEngine.Abstractions.IValidationEntry +Microsoft.TemplateEngine.Abstractions.IValidationEntry.Code.get -> string! +Microsoft.TemplateEngine.Abstractions.IValidationEntry.ErrorLocation +Microsoft.TemplateEngine.Abstractions.IValidationEntry.ErrorLocation.ErrorLocation() -> void +Microsoft.TemplateEngine.Abstractions.IValidationEntry.ErrorLocation.Filename.get -> string! +Microsoft.TemplateEngine.Abstractions.IValidationEntry.ErrorLocation.Filename.init -> void +Microsoft.TemplateEngine.Abstractions.IValidationEntry.ErrorLocation.LineNumber.get -> int +Microsoft.TemplateEngine.Abstractions.IValidationEntry.ErrorLocation.LineNumber.init -> void +Microsoft.TemplateEngine.Abstractions.IValidationEntry.ErrorLocation.Position.get -> int +Microsoft.TemplateEngine.Abstractions.IValidationEntry.ErrorLocation.Position.init -> void +Microsoft.TemplateEngine.Abstractions.IValidationEntry.ErrorMessage.get -> string! +Microsoft.TemplateEngine.Abstractions.IValidationEntry.Location.get -> Microsoft.TemplateEngine.Abstractions.IValidationEntry.ErrorLocation? +Microsoft.TemplateEngine.Abstractions.IValidationEntry.Severity.get -> Microsoft.TemplateEngine.Abstractions.IValidationEntry.SeverityLevel +Microsoft.TemplateEngine.Abstractions.IValidationEntry.SeverityLevel +Microsoft.TemplateEngine.Abstractions.IValidationEntry.SeverityLevel.Error = 3 -> Microsoft.TemplateEngine.Abstractions.IValidationEntry.SeverityLevel +Microsoft.TemplateEngine.Abstractions.IValidationEntry.SeverityLevel.Info = 1 -> Microsoft.TemplateEngine.Abstractions.IValidationEntry.SeverityLevel +Microsoft.TemplateEngine.Abstractions.IValidationEntry.SeverityLevel.None = 0 -> Microsoft.TemplateEngine.Abstractions.IValidationEntry.SeverityLevel +Microsoft.TemplateEngine.Abstractions.IValidationEntry.SeverityLevel.Warning = 2 -> Microsoft.TemplateEngine.Abstractions.IValidationEntry.SeverityLevel +Microsoft.TemplateEngine.Abstractions.IValidationInfo +Microsoft.TemplateEngine.Abstractions.IValidationInfo.ValidationErrors.get -> System.Collections.Generic.IReadOnlyList! diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateFiltering/ITemplateMatchInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateFiltering/ITemplateMatchInfo.cs new file mode 100644 index 000000000000..6ddc678f3ecc --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateFiltering/ITemplateMatchInfo.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.TemplateFiltering +{ + /// + /// Template with information about matching the filters. + /// + public interface ITemplateMatchInfo + { + /// + /// Gets the template the filters applied to. + /// + ITemplateInfo Info { get; } + + /// + /// Gets match information for the filters applied to template. + /// + IReadOnlyList MatchDisposition { get; } + + /// + /// Adds the match information. + /// + /// + void AddMatchDisposition(MatchInfo newDisposition); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateFiltering/MatchInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateFiltering/MatchInfo.cs new file mode 100644 index 000000000000..f16470f5b718 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateFiltering/MatchInfo.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Constraints; + +namespace Microsoft.TemplateEngine.Abstractions.TemplateFiltering +{ + /// + /// Represents match information for the filter applied to template. + /// + public class MatchInfo + { + /// + /// Creates instance. + /// + /// the name for the match. See default names in . + /// the value matched for. + /// the match kind between value and . + public MatchInfo(string name, string? value, MatchKind kind) + { + _ = string.IsNullOrWhiteSpace(name) ? throw new ArgumentException($"{nameof(name)} should not be null or empty") : false; + + Name = name; + Value = value; + Kind = kind; + } + + /// + /// Defines the match status. + /// + public MatchKind Kind { get; } + + /// + /// Gets the name of the match. + /// For default filter names, see ). + /// + public string Name { get; } + + /// + /// Gets the value provided to match for. + /// + public string? Value { get; } + + /// + /// Frequently used filter names. They are also used by Utils.WellKnownSearchFilters. + /// + public static class BuiltIn + { + /// + /// Template name . + /// + public const string Name = "Name"; + + /// + /// Template short names . + /// + public const string ShortName = "ShortName"; + + /// + /// Template classifications . + /// + public const string Classification = "Classification"; + + /// + /// Template language ( named "language"). + /// + public const string Language = "Language"; + + /// + /// Template type ( named "type"). + /// + public const string Type = "Type"; + + /// + /// Template baseline names . + /// + public const string Baseline = "Baseline"; + + /// + /// Template author . + /// + public const string Author = "Author"; + + /// + /// Prefix used for matching. + /// The full name is 'Constraint.<constraint type>'. + /// + public const string Constraint = "Constraint"; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateFiltering/MatchKind.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateFiltering/MatchKind.cs new file mode 100644 index 000000000000..3ca6720d4ffe --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateFiltering/MatchKind.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.TemplateFiltering +{ + /// + /// Defines the match kind: how the filter with its input values matches the property(ies) of . + /// + public enum MatchKind + { + /// + /// The filter exactly matches the value of . + /// + Exact, + + /// + /// The filter partially matches the value of . + /// For example, contains the value of the filter but not equal to it. + /// + Partial, + + /// + /// The filter does not match the value of . + /// Example: the filter is checking and the parameter name is not defined in . + /// + Mismatch, + + /// + /// The input passed to the filter is incorrect, impossible to identify property of to check. + /// Example: the filter is checking and the parameter name is not defined in . + /// + InvalidName, + + /// + /// The input passed to the filter is incorrect: the value is different format that is supported by property. + /// Example: the filter is checking and the parameter is boolean but value to match with is string. + /// + InvalidValue, + + [Obsolete("This value will be removed in next release, use + " + nameof(MatchKind.Mismatch) + " or " + nameof(MatchKind.InvalidValue) + " instead")] + AmbiguousValue, + + [Obsolete("This value will be removed in next release, use + " + nameof(MatchKind.Mismatch) + " or " + nameof(MatchKind.InvalidValue) + " instead")] + SingleStartsWith + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/IManagedTemplatePackage.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/IManagedTemplatePackage.cs new file mode 100644 index 000000000000..2188d8cebb2f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/IManagedTemplatePackage.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Installer; + +namespace Microsoft.TemplateEngine.Abstractions.TemplatePackage +{ + /// + /// Represents the package that can be managed by . can additionally install, update and uninstall template package. + /// + public interface IManagedTemplatePackage : ITemplatePackage + { + /// + /// Gets the name to be used when displaying template package in UI. + /// + string DisplayName { get; } + + /// + /// Gets the identifier of template package. + /// Identifier should be unique in scope of that manages the . + /// + /// + /// This can be NuGet PackageId, path to .nupkg, folder name, depends on implementation. + /// + string Identifier { get; } + + /// + /// Gets that installed the template package. + /// This serves as helper for grouping template package by installer so caller doesn't need to keep track of installer->template package relation. + /// + IInstaller Installer { get; } + + /// + /// Gets that manages the template package. + /// This serves as helper for grouping packages by + /// so caller doesn't need to keep track of "managed provider"->"managed package" relation. + /// + IManagedTemplatePackageProvider ManagedProvider { get; } + + /// + /// Gets the version of the template package. + /// + string? Version { get; } + + /// + /// Indicates whether package was installed from local source (e.g. local extracted folder, local nuget etc.). + /// + bool IsLocalPackage { get; } + + /// + /// Gets additional details about template package. The details depend on implementation. + /// + IReadOnlyDictionary GetDetails(); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/IManagedTemplatePackageProvider.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/IManagedTemplatePackageProvider.cs new file mode 100644 index 000000000000..26ee140d58f9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/IManagedTemplatePackageProvider.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Installer; + +namespace Microsoft.TemplateEngine.Abstractions.TemplatePackage +{ + /// + /// The provider is responsible for managing s. + /// Besides base functionality of , it adds ability to install, update and uninstall template packages. + /// + /// + /// The keeps track of template packages managed by the provider. The actual installation is done by implementations. + /// + public interface IManagedTemplatePackageProvider : ITemplatePackageProvider + { + /// + /// Gets the latest version for the template packages. + /// + /// List of to get latest version for. + /// + /// List of containing the check results. + Task> GetLatestVersionsAsync(IEnumerable templatePackages, CancellationToken cancellationToken); + + /// + /// Updates the template packages given in to specified version. + /// + /// List of to be processed. + /// + /// List of with update results. + Task> UpdateAsync(IEnumerable updateRequests, CancellationToken cancellationToken); + + /// + /// Uninstalls the template packages. + /// + /// list of s to be uninstalled. + /// + /// List of with uninstall results. + Task> UninstallAsync(IEnumerable templatePackages, CancellationToken cancellationToken); + + /// + /// Installs new based on data. + /// All s are considered via and if only 1 + /// returns . is executed and result is returned. + /// + /// Contains the list of to perform. + /// + /// List of with installation results. + Task> InstallAsync(IEnumerable installRequests, CancellationToken cancellationToken); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/ITemplatePackage.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/ITemplatePackage.cs new file mode 100644 index 000000000000..d22e6ce7884e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/ITemplatePackage.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Abstractions.TemplatePackage +{ + /// + /// Represents the template package. + /// Template package is a folder, .nupkg or other container that can contain single or multiple templates. + /// See for more information. + /// + public interface ITemplatePackage + { + /// + /// Gets the last changed time for the template package. + /// To avoid scanning for changes every time, template engine is caching templates from + /// template packages, this timestamp is used to invalidate content and re-scan this template package. + /// + DateTime LastChangeTime { get; } + + /// + /// Gets mount point URI - unique location of template package. + /// This can be full URI like file://, http:// or simply file path. + /// + /// + /// Supported mount points are defined in implementations. + /// + string MountPointUri { get; } + + /// + /// Gets the that created the template package. + /// This is mostly helper for grouping packages by provider + /// so caller doesn't need to keep track of provider->package relation. + /// + ITemplatePackageProvider Provider { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/ITemplatePackageProvider.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/ITemplatePackageProvider.cs new file mode 100644 index 000000000000..349affcb5473 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/ITemplatePackageProvider.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.TemplatePackage +{ + /// + /// Provides set of s available to the host. + /// + public interface ITemplatePackageProvider + { + /// + /// Raised when template packages have been changed. Indicates that caller should refresh the list of template packages in use. + /// + event Action? TemplatePackagesChanged; + + /// + /// Gets that created the provider. + /// + ITemplatePackageProviderFactory Factory { get; } + + /// + /// Gets the list of template packages available for the provider. + /// + /// + /// The list of s. + Task> GetAllTemplatePackagesAsync(CancellationToken cancellationToken); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/ITemplatePackageProviderFactory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/ITemplatePackageProviderFactory.cs new file mode 100644 index 000000000000..093e69991489 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/ITemplatePackageProviderFactory.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.TemplatePackage +{ + /// + /// Factory responsible for creating or . + /// This is registered with either via or + /// . + /// + public interface ITemplatePackageProviderFactory : IIdentifiedComponent + { + /// + /// Gets the readable name of the factory. + /// This is used for two reasons: + /// 1) To show user which provider is source of template packages (when debug/verbose flag is set) + /// 2) To allow the user to pick specific provider to install templates to. + /// + string DisplayName { get; } + + /// + /// Creates new provider with specified environment, the provider may also implement . + /// + ITemplatePackageProvider CreateProvider(IEngineEnvironmentSettings settings); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/TemplatePackage.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/TemplatePackage.cs new file mode 100644 index 000000000000..32194c1f8c9e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplatePackage/TemplatePackage.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions.TemplatePackage +{ + /// + /// Basic implementation so each + /// doesn't need to re-implement it. + /// + public class TemplatePackage : ITemplatePackage + { + public TemplatePackage(ITemplatePackageProvider provider, string mountPointUri, DateTime lastChangeTime) + { + Provider = provider; + MountPointUri = mountPointUri; + LastChangeTime = lastChangeTime; + } + + /// + public ITemplatePackageProvider Provider { get; } + + /// + public string MountPointUri { get; } + + /// + public DateTime LastChangeTime { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateParameterPrecedence.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateParameterPrecedence.cs new file mode 100644 index 000000000000..e7c2e35f023f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateParameterPrecedence.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions; + +/// +/// Indication of template parameter precedence and conditions that could influence it. +/// +public class TemplateParameterPrecedence +{ + /// + /// Default optional precedence. + /// + public static readonly TemplateParameterPrecedence Default = new(PrecedenceDefinition.Optional); + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + public TemplateParameterPrecedence( + PrecedenceDefinition precedenceDefinition, + string? isRequiredCondition = null, + string? isEnabledCondition = null, + bool isRequired = false) + { + PrecedenceDefinition = precedenceDefinition; + IsRequiredCondition = isRequiredCondition; + IsEnabledCondition = isEnabledCondition; + IsRequired = isRequired; + VerifyConditions(); + } + + /// + /// Actual precedence definition - can be used to sort parameters (e.g. for tab completion purposes). + /// + public PrecedenceDefinition PrecedenceDefinition { get; } + + /// + /// IsRequiredCondition value - if it was specified in template. + /// + public string? IsRequiredCondition { get; } + + /// + /// IsEnabledCondition value - if it was specified in template. + /// + public string? IsEnabledCondition { get; } + + /// + /// Indicates whether parameter is unconditionally required. + /// + public bool IsRequired { get; } + + /// + /// Indicates whether parameter might be required (depending on values of other parameters). + /// + public bool CanBeRequired => + PrecedenceDefinition == PrecedenceDefinition.Required || + !string.IsNullOrEmpty(IsRequiredCondition) || + IsRequired; + + private void VerifyConditions() + { + // If enable condition is set - parameter is conditionally disabled (regardless if require condition is set or not) + // Conditionally required is if and only if the only require condition is set + + if (!(string.IsNullOrEmpty(IsRequiredCondition) ^ PrecedenceDefinition == PrecedenceDefinition.ConditionalyRequired + || + !string.IsNullOrEmpty(IsEnabledCondition) ^ PrecedenceDefinition == PrecedenceDefinition.ConditionalyDisabled) + && + !(!string.IsNullOrEmpty(IsRequiredCondition) && !string.IsNullOrEmpty(IsEnabledCondition) && PrecedenceDefinition == PrecedenceDefinition.ConditionalyDisabled)) + { + // TODO: localize + throw new ArgumentException("Mismatched precedence definition"); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateParameterPrecedenceExtensions.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateParameterPrecedenceExtensions.cs new file mode 100644 index 000000000000..1ef2c4b3744a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateParameterPrecedenceExtensions.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions; + +public static class TemplateParameterPrecedenceExtensions +{ + /// + /// Converts legacy parameter priority to the PrecedenceDefinition. + /// + /// + /// + /// + [Obsolete("TemplateParameterPriority is Obsolete and should be replaced with PrecedenceDefinition.")] + public static PrecedenceDefinition ToPrecedenceDefinition(this TemplateParameterPriority priority) + { + return priority switch + { + TemplateParameterPriority.Required => PrecedenceDefinition.Required, + TemplateParameterPriority.Optional => PrecedenceDefinition.Optional, + TemplateParameterPriority.Implicit => PrecedenceDefinition.Implicit, + TemplateParameterPriority.Suggested => throw new NotImplementedException(), + _ => throw new ArgumentOutOfRangeException(nameof(priority), priority, null), + }; + } + + /// + /// Converts legacy parameter priority to the TemplateParameterPrecedence. + /// + /// + /// + [Obsolete("TemplateParameterPriority is Obsolete and should be replaced with PrecedenceDefinition.")] + public static TemplateParameterPrecedence ToTemplateParameterPrecedence(this TemplateParameterPriority priority) + { + return new TemplateParameterPrecedence(priority.ToPrecedenceDefinition(), null, null); + } + + /// + /// Converts the PrecedenceDefinition to legacy TemplateParameterPriority. + /// + /// + /// + /// + [Obsolete("TemplateParameterPriority is Obsolete and should be replaced with PrecedenceDefinition.")] + public static TemplateParameterPriority ToTemplateParameterPriority(this PrecedenceDefinition precedenceDefinition) + { + return precedenceDefinition switch + { + PrecedenceDefinition.Required => TemplateParameterPriority.Required, + PrecedenceDefinition.Optional => TemplateParameterPriority.Optional, + PrecedenceDefinition.Implicit => TemplateParameterPriority.Implicit, + _ => TemplateParameterPriority.Optional, + }; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateParameterPriority.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateParameterPriority.cs new file mode 100644 index 000000000000..d1c09692a5fc --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Abstractions/TemplateParameterPriority.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Abstractions +{ + /// + /// Defines the priority of a template parameter. + /// + [Obsolete("Use Precedence instead.")] + public enum TemplateParameterPriority + { + /// + /// The parameter is mandatory. + /// + Required, + + [Obsolete("the value was never used and is deprecated.")] + Suggested, + + /// + /// The parameter is optional. + /// + Optional, + + /// + /// The parameter is implicit (built-in). + /// + Implicit + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IEncodingConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IEncodingConfig.cs new file mode 100644 index 000000000000..6a4f88433017 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IEncodingConfig.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IEncodingConfig + { + Encoding Encoding { get; } + + ITokenTrie LineEndings { get; } + + IReadOnlyList VariableKeys { get; } + + IReadOnlyList> VariableValues { get; } + + ITokenTrie Variables { get; } + + ITokenTrie Whitespace { get; } + + ITokenTrie WhitespaceOrLineEnding { get; } + + object this[int index] { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IEngineConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IEngineConfig.cs new file mode 100644 index 000000000000..66dba22e6b4d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IEngineConfig.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IEngineConfig + { + ILogger Logger { get; } + + IReadOnlyList LineEndings { get; } + + string VariableFormatString { get; } + + IVariableCollection Variables { get; } + + IReadOnlyList Whitespaces { get; } + + IDictionary Flags { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IGlobalRunSpec.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IGlobalRunSpec.cs new file mode 100644 index 000000000000..95b540f93ccb --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IGlobalRunSpec.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IGlobalRunSpec + { + IReadOnlyList Exclude { get; } + + IReadOnlyList Include { get; } + + IReadOnlyList CopyOnly { get; } + + IReadOnlyList Operations { get; } + + IVariableCollection RootVariableCollection { get; } + + IReadOnlyList> Special { get; } + + IReadOnlyList IgnoreFileNames { get; } + + bool TryGetTargetRelPath(string sourceRelPath, out string targetRelPath); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IKeysChangedEventArgs.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IKeysChangedEventArgs.cs new file mode 100644 index 000000000000..e20b8fe01275 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IKeysChangedEventArgs.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IKeysChangedEventArgs; +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IOperation.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IOperation.cs new file mode 100644 index 000000000000..6adc734025b7 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IOperation.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IOperation + { + IReadOnlyList Tokens { get; } + + string? Id { get; } + + bool IsInitialStateOn { get; } + + int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IOperationProvider.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IOperationProvider.cs new file mode 100644 index 000000000000..4aa078121acc --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IOperationProvider.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IOperationProvider + { + string? Id { get; } + + IOperation GetOperation(Encoding encoding, IProcessorState processorState); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IOrchestrator.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IOrchestrator.cs new file mode 100644 index 000000000000..cc83c43e5bc9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IOrchestrator.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IOrchestrator + { + void Run(string runSpecPath, IDirectory sourceDir, string targetDir); + + void Run(IGlobalRunSpec spec, IDirectory sourceDir, string targetDir); + + IReadOnlyList GetFileChanges(string runSpecPath, IDirectory sourceDir, string targetDir); + + IReadOnlyList GetFileChanges(IGlobalRunSpec spec, IDirectory sourceDir, string targetDir); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IPathMatcher.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IPathMatcher.cs new file mode 100644 index 000000000000..a12a49d61d4b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IPathMatcher.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IPathMatcher + { + string Pattern { get; } + + bool IsMatch(string path); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IProcessor.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IProcessor.cs new file mode 100644 index 000000000000..d565902f21b5 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IProcessor.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IProcessor + { + bool Run(Stream source, Stream target); + + bool Run(Stream source, Stream target, int bufferSize); + + bool Run(Stream source, Stream target, int bufferSize, int flushThreshold); + + IProcessor CloneAndAppendOperations(IReadOnlyList tempOperations); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IProcessorState.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IProcessorState.cs new file mode 100644 index 000000000000..7dbfeec82a58 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IProcessorState.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IProcessorState + { + IEngineConfig Config { get; } + + /// + /// Gets the buffer containing the chunk of source stream that is being processed. + /// + byte[] CurrentBuffer { get; } + + /// + /// Gets the length of useful data in . + /// + int CurrentBufferLength { get; } + + /// + /// Gets the current position in . + /// + int CurrentBufferPosition { get; } + + /// + /// Gets the current position in source stream. + /// + int CurrentSequenceNumber { get; } + + IEncodingConfig EncodingConfig { get; } + + Encoding Encoding { get; } + + /// + /// Advances source stream to position . + /// + bool AdvanceBuffer(int bufferPosition); + + /// + /// Seeks source stream until is found. + /// + /// The token to find. + /// The length of the buffer after the token is found. + /// The position in the buffer after the token is found. + /// True if token should be sought through. + void SeekSourceForwardUntil(ITokenTrie match, ref int bufferLength, ref int currentBufferPosition, bool consumeToken = false); + + /// + /// Seeks source stream while is found. + /// + void SeekSourceForwardWhile(ITokenTrie match, ref int bufferLength, ref int currentBufferPosition); + + /// + /// Seeks target stream backwards until is found. + /// + /// The token to find. + /// True if token should be sought through. + void SeekTargetBackUntil(ITokenTrie match, bool consumeToken = false); + + /// + /// Seeks target stream backwards while is found. + /// + void SeekTargetBackWhile(ITokenTrie match); + + /// + /// Writes to target stream. + /// + /// The buffer to write. + /// The start position in the buffer to write. + /// The count of bytes to write. + void WriteToTarget(byte[] buffer, int offset, int count); + + void Inject(Stream staged); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IReplacementTokens.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IReplacementTokens.cs new file mode 100644 index 000000000000..007f8c8a7400 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IReplacementTokens.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IReplacementTokens + { + string VariableName { get; } + + ITokenConfig OriginalValue { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IRunSpec.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IRunSpec.cs new file mode 100644 index 000000000000..ab6f95b2813f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IRunSpec.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IRunSpec + { + string VariableFormatString { get; } + + bool TryGetTargetRelPath(string sourceRelPath, out string? targetRelPath); + + IReadOnlyList GetOperations(IReadOnlyList sourceOperations); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IToken.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IToken.cs new file mode 100644 index 000000000000..4f88795c9cd3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IToken.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IToken + { + /// + /// The value to match. + /// + byte[] Value { get; } + + /// + /// Start of actual token. May be not 0, when look arounds are used. + /// + int Start { get; } + + /// + /// Start of actual token. May be not Value.Length, when look arounds are used. + /// + int End { get; } + + /// + /// The length of the actual token. + /// + int Length { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ITokenConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ITokenConfig.cs new file mode 100644 index 000000000000..3d545a522117 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ITokenConfig.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface ITokenConfig : IEquatable + { + string? After { get; } + + string? Before { get; } + + string? Value { get; } + + IToken ToToken(Encoding encoding); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ITokenTrie.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ITokenTrie.cs new file mode 100644 index 000000000000..76917ce1d325 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ITokenTrie.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface ITokenTrie + { + int Count { get; } + + int MaxLength { get; } + + int MinLength { get; } + + IReadOnlyList TokenLength { get; } + + IReadOnlyList Tokens { get; } + + int AddToken(IToken token); + + void AddToken(IToken? token, int index); + + bool GetOperation(byte[] buffer, int bufferLength, ref int currentBufferPosition, out int token); + + bool GetOperation(byte[] buffer, int bufferLength, ref int currentBufferPosition, bool mustMatchPosition, out int token); + + void Append(ITokenTrie trie); + + ITokenTrieEvaluator CreateEvaluator(); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ITokenTrieEvaluator.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ITokenTrieEvaluator.cs new file mode 100644 index 000000000000..543c90264fca --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ITokenTrieEvaluator.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface ITokenTrieEvaluator + { + int BytesToKeepInBuffer { get; } + + bool Accept(byte data, ref int bufferPosition, out int token); + + bool TryFinalizeMatchesInProgress(ref int bufferPosition, out int token); + } +} \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IValueReadEventArgs.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IValueReadEventArgs.cs new file mode 100644 index 000000000000..da5b4b80d7df --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IValueReadEventArgs.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IValueReadEventArgs + { + string Key { get; } + + object Value { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IVariableCollection.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IVariableCollection.cs new file mode 100644 index 000000000000..c5659944bce6 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IVariableCollection.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + /// + /// Extends with the events raised when collection is being read or changed. + /// + public interface IMonitoredVariableCollection : IVariableCollection + { + event KeysChangedEventHander KeysChanged; + + event ValueReadEventHander ValueRead; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IVariableConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IVariableConfig.cs new file mode 100644 index 000000000000..426c96a3c76d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/IVariableConfig.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public interface IVariableConfig + { + IReadOnlyDictionary Sources { get; } + + IReadOnlyList Order { get; } + + string? FallbackFormat { get; } + + bool Expand { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/KeysChangedEventHander.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/KeysChangedEventHander.cs new file mode 100644 index 000000000000..66637244b12c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/KeysChangedEventHander.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public delegate void KeysChangedEventHander(object sender, IKeysChangedEventArgs args); +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/Microsoft.TemplateEngine.Core.Contracts.csproj b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/Microsoft.TemplateEngine.Core.Contracts.csproj new file mode 100644 index 000000000000..b63552193378 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/Microsoft.TemplateEngine.Core.Contracts.csproj @@ -0,0 +1,19 @@ + + + + $(NetMinimum);$(NetCurrent);netstandard2.0;$(NetFrameworkMinimum) + Contracts for extending Microsoft.TemplateEngine.Core + true + true + + true + + + + + + + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/PublicAPI.Shipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..ad54f300c420 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/PublicAPI.Shipped.txt @@ -0,0 +1,95 @@ +#nullable enable +Microsoft.TemplateEngine.Core.Contracts.IEncodingConfig +Microsoft.TemplateEngine.Core.Contracts.IEngineConfig +Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec +Microsoft.TemplateEngine.Core.Contracts.IKeysChangedEventArgs +Microsoft.TemplateEngine.Core.Contracts.IOperation +Microsoft.TemplateEngine.Core.Contracts.IOperation.IsInitialStateOn.get -> bool +Microsoft.TemplateEngine.Core.Contracts.IOperationProvider +Microsoft.TemplateEngine.Core.Contracts.IOrchestrator +Microsoft.TemplateEngine.Core.Contracts.IPathMatcher +Microsoft.TemplateEngine.Core.Contracts.IProcessor +Microsoft.TemplateEngine.Core.Contracts.IProcessorState +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.AdvanceBuffer(int bufferPosition) -> bool +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.CurrentBufferLength.get -> int +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.CurrentBufferPosition.get -> int +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.CurrentSequenceNumber.get -> int +Microsoft.TemplateEngine.Core.Contracts.IReplacementTokens +Microsoft.TemplateEngine.Core.Contracts.IRunSpec +Microsoft.TemplateEngine.Core.Contracts.IToken +Microsoft.TemplateEngine.Core.Contracts.IToken.End.get -> int +Microsoft.TemplateEngine.Core.Contracts.IToken.Length.get -> int +Microsoft.TemplateEngine.Core.Contracts.IToken.Start.get -> int +Microsoft.TemplateEngine.Core.Contracts.ITokenConfig +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie.Count.get -> int +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie.MaxLength.get -> int +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie.MinLength.get -> int +Microsoft.TemplateEngine.Core.Contracts.ITokenTrieEvaluator +Microsoft.TemplateEngine.Core.Contracts.ITokenTrieEvaluator.Accept(byte data, ref int bufferPosition, out int token) -> bool +Microsoft.TemplateEngine.Core.Contracts.ITokenTrieEvaluator.BytesToKeepInBuffer.get -> int +Microsoft.TemplateEngine.Core.Contracts.ITokenTrieEvaluator.TryFinalizeMatchesInProgress(ref int bufferPosition, out int token) -> bool +Microsoft.TemplateEngine.Core.Contracts.IValueReadEventArgs +Microsoft.TemplateEngine.Core.Contracts.IVariableConfig +Microsoft.TemplateEngine.Core.Contracts.IVariableConfig.Expand.get -> bool +Microsoft.TemplateEngine.Core.Contracts.KeysChangedEventHander +Microsoft.TemplateEngine.Core.Contracts.ValueReadEventHander +Microsoft.TemplateEngine.Core.Contracts.IEncodingConfig.Encoding.get -> System.Text.Encoding! +Microsoft.TemplateEngine.Core.Contracts.IEncodingConfig.LineEndings.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +Microsoft.TemplateEngine.Core.Contracts.IEncodingConfig.this[int index].get -> object! +Microsoft.TemplateEngine.Core.Contracts.IEncodingConfig.VariableKeys.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IEncodingConfig.Variables.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +Microsoft.TemplateEngine.Core.Contracts.IEncodingConfig.VariableValues.get -> System.Collections.Generic.IReadOnlyList!>! +Microsoft.TemplateEngine.Core.Contracts.IEncodingConfig.Whitespace.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +Microsoft.TemplateEngine.Core.Contracts.IEncodingConfig.WhitespaceOrLineEnding.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +Microsoft.TemplateEngine.Core.Contracts.IEngineConfig.Flags.get -> System.Collections.Generic.IDictionary! +Microsoft.TemplateEngine.Core.Contracts.IEngineConfig.LineEndings.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IEngineConfig.Logger.get -> Microsoft.Extensions.Logging.ILogger! +Microsoft.TemplateEngine.Core.Contracts.IEngineConfig.VariableFormatString.get -> string! +Microsoft.TemplateEngine.Core.Contracts.IEngineConfig.Whitespaces.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec.CopyOnly.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec.Exclude.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec.IgnoreFileNames.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec.Include.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec.Operations.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec.Special.get -> System.Collections.Generic.IReadOnlyList>! +Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec.TryGetTargetRelPath(string! sourceRelPath, out string! targetRelPath) -> bool +Microsoft.TemplateEngine.Core.Contracts.IOperation.HandleMatch(Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor, int bufferLength, ref int currentBufferPosition, int token) -> int +Microsoft.TemplateEngine.Core.Contracts.IOperationProvider.GetOperation(System.Text.Encoding! encoding, Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processorState) -> Microsoft.TemplateEngine.Core.Contracts.IOperation! +Microsoft.TemplateEngine.Core.Contracts.IOrchestrator.GetFileChanges(Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec! spec, Microsoft.TemplateEngine.Abstractions.Mount.IDirectory! sourceDir, string! targetDir) -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IOrchestrator.GetFileChanges(string! runSpecPath, Microsoft.TemplateEngine.Abstractions.Mount.IDirectory! sourceDir, string! targetDir) -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IOrchestrator.Run(Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec! spec, Microsoft.TemplateEngine.Abstractions.Mount.IDirectory! sourceDir, string! targetDir) -> void +Microsoft.TemplateEngine.Core.Contracts.IOrchestrator.Run(string! runSpecPath, Microsoft.TemplateEngine.Abstractions.Mount.IDirectory! sourceDir, string! targetDir) -> void +Microsoft.TemplateEngine.Core.Contracts.IPathMatcher.IsMatch(string! path) -> bool +Microsoft.TemplateEngine.Core.Contracts.IPathMatcher.Pattern.get -> string! +Microsoft.TemplateEngine.Core.Contracts.IProcessor.CloneAndAppendOperations(System.Collections.Generic.IReadOnlyList! tempOperations) -> Microsoft.TemplateEngine.Core.Contracts.IProcessor! +Microsoft.TemplateEngine.Core.Contracts.IProcessor.Run(System.IO.Stream! source, System.IO.Stream! target) -> bool +Microsoft.TemplateEngine.Core.Contracts.IProcessor.Run(System.IO.Stream! source, System.IO.Stream! target, int bufferSize) -> bool +Microsoft.TemplateEngine.Core.Contracts.IProcessor.Run(System.IO.Stream! source, System.IO.Stream! target, int bufferSize, int flushThreshold) -> bool +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.Config.get -> Microsoft.TemplateEngine.Core.Contracts.IEngineConfig! +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.CurrentBuffer.get -> byte[]! +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.Encoding.get -> System.Text.Encoding! +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.EncodingConfig.get -> Microsoft.TemplateEngine.Core.Contracts.IEncodingConfig! +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.Inject(System.IO.Stream! staged) -> void +Microsoft.TemplateEngine.Core.Contracts.IReplacementTokens.OriginalValue.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Contracts.IReplacementTokens.VariableName.get -> string! +Microsoft.TemplateEngine.Core.Contracts.IRunSpec.GetOperations(System.Collections.Generic.IReadOnlyList! sourceOperations) -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IRunSpec.VariableFormatString.get -> string! +Microsoft.TemplateEngine.Core.Contracts.IToken.Value.get -> byte[]! +Microsoft.TemplateEngine.Core.Contracts.ITokenConfig.After.get -> string? +Microsoft.TemplateEngine.Core.Contracts.ITokenConfig.Before.get -> string? +Microsoft.TemplateEngine.Core.Contracts.ITokenConfig.ToToken(System.Text.Encoding! encoding) -> Microsoft.TemplateEngine.Core.Contracts.IToken! +Microsoft.TemplateEngine.Core.Contracts.ITokenConfig.Value.get -> string? +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie.AddToken(Microsoft.TemplateEngine.Core.Contracts.IToken! token) -> int +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie.Append(Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! trie) -> void +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie.CreateEvaluator() -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrieEvaluator! +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie.GetOperation(byte[]! buffer, int bufferLength, ref int currentBufferPosition, bool mustMatchPosition, out int token) -> bool +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie.GetOperation(byte[]! buffer, int bufferLength, ref int currentBufferPosition, out int token) -> bool +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie.TokenLength.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie.Tokens.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IValueReadEventArgs.Key.get -> string! +Microsoft.TemplateEngine.Core.Contracts.IValueReadEventArgs.Value.get -> object! +Microsoft.TemplateEngine.Core.Contracts.IVariableConfig.FallbackFormat.get -> string? +Microsoft.TemplateEngine.Core.Contracts.IVariableConfig.Order.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IVariableConfig.Sources.get -> System.Collections.Generic.IReadOnlyDictionary! + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/PublicAPI.Unshipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..250a1f58eab3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/PublicAPI.Unshipped.txt @@ -0,0 +1,15 @@ +Microsoft.TemplateEngine.Core.Contracts.IEngineConfig.Variables.get -> Microsoft.TemplateEngine.Abstractions.IVariableCollection! +Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec.RootVariableCollection.get -> Microsoft.TemplateEngine.Abstractions.IVariableCollection! +Microsoft.TemplateEngine.Core.Contracts.IMonitoredVariableCollection +Microsoft.TemplateEngine.Core.Contracts.IMonitoredVariableCollection.KeysChanged -> Microsoft.TemplateEngine.Core.Contracts.KeysChangedEventHander! +Microsoft.TemplateEngine.Core.Contracts.IMonitoredVariableCollection.ValueRead -> Microsoft.TemplateEngine.Core.Contracts.ValueReadEventHander! +Microsoft.TemplateEngine.Core.Contracts.IOperation.Id.get -> string? +Microsoft.TemplateEngine.Core.Contracts.IOperation.Tokens.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Contracts.IOperationProvider.Id.get -> string? +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.SeekSourceForwardUntil(Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! match, ref int bufferLength, ref int currentBufferPosition, bool consumeToken = false) -> void +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.SeekSourceForwardWhile(Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! match, ref int bufferLength, ref int currentBufferPosition) -> void +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.SeekTargetBackUntil(Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! match, bool consumeToken = false) -> void +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.SeekTargetBackWhile(Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! match) -> void +Microsoft.TemplateEngine.Core.Contracts.IProcessorState.WriteToTarget(byte[]! buffer, int offset, int count) -> void +Microsoft.TemplateEngine.Core.Contracts.IRunSpec.TryGetTargetRelPath(string! sourceRelPath, out string? targetRelPath) -> bool +Microsoft.TemplateEngine.Core.Contracts.ITokenTrie.AddToken(Microsoft.TemplateEngine.Core.Contracts.IToken? token, int index) -> void \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ValueReadEventHander.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ValueReadEventHander.cs new file mode 100644 index 000000000000..2a2880b8c129 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core.Contracts/ValueReadEventHander.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Contracts +{ + public delegate void ValueReadEventHander(object sender, IValueReadEventArgs args); +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/CommonOperations.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/CommonOperations.cs new file mode 100644 index 000000000000..157d0e13ac6b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/CommonOperations.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core +{ + public static class CommonOperations + { + public static void WhitespaceHandler(this IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, bool wholeLine = false, bool trim = false, bool trimForward = false, bool trimBackward = false) + { + if (wholeLine) + { + processor.ConsumeWholeLine(ref bufferLength, ref currentBufferPosition); + return; + } + + if (trim) + { + trimForward = true; + trimBackward = true; + } + + processor.TrimWhitespace(trimForward, trimBackward, ref bufferLength, ref currentBufferPosition); + } + + public static void ConsumeWholeLine(this IProcessorState processor, ref int bufferLength, ref int currentBufferPosition) + { + processor.SeekTargetBackWhile(processor.EncodingConfig.Whitespace); + processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: true); + } + + public static void TrimWhitespace(this IProcessorState processor, bool forward, bool backward, ref int bufferLength, ref int currentBufferPosition) + { + if (backward) + { + processor.SeekTargetBackWhile(processor.EncodingConfig.Whitespace); + } + + if (forward) + { + processor.SeekSourceForwardWhile(processor.EncodingConfig.Whitespace, ref bufferLength, ref currentBufferPosition); + //Consume the trailing line end if possible + _ = processor.EncodingConfig.LineEndings.GetOperation(processor.CurrentBuffer, bufferLength, ref currentBufferPosition, out _); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/BinaryScope.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/BinaryScope.cs new file mode 100644 index 000000000000..41337b341fca --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/BinaryScope.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public class BinaryScope : IEvaluable + { + private readonly Func _evaluate; + + public BinaryScope(IEvaluable? parent, TOperator @operator, Func evaluate) + { + Parent = parent; + Operator = @operator; + _evaluate = evaluate; + } + + public bool IsFull => Left != null && Right != null; + + public bool IsIndivisible => false; + + public IEvaluable? Left { get; set; } + + public TOperator Operator { get; } + + public IEvaluable? Parent { get; set; } + + public IEvaluable? Right { get; set; } + + public object Evaluate() + { + object? left = Left?.Evaluate(); + object? right = Right?.Evaluate(); + return _evaluate(left, right); + } + + public override string ToString() + { + return $@"({Left} -{Operator}- {Right})"; + } + + public bool TryAccept(IEvaluable? child) + { + if (Left == null) + { + Left = child; + return true; + } + + if (Right == null) + { + Right = child; + return true; + } + + return false; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Converter.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Converter.cs new file mode 100644 index 000000000000..0c678aa773bb --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Converter.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public static class Converter + { + static Converter() + { + Register(new BoolConverter()); + Register(new IntConverter()); + Register(new LongConverter()); + Register(new FloatConverter()); + Register(new DoubleConverter()); + Register(new StringConverter()); + } + + public static bool TryConvert(object? source, out T? result) + { + return ConverterItem.TryExecute(source, out result); + } + + private static void Register(ConverterItem item) + { + ConverterItem.IsHandledBy(item); + } + + private abstract class ConverterItem + { + public static ConverterItem? Instance { get; private set; } + + public static void IsHandledBy(TInstance instance) + where TInstance : ConverterItem + { + Instance = instance; + } + + public static bool TryExecute(object? source, out T? result) + { + if (source is T x) + { + result = x; + return true; + } + + T? handlerValue = default; + bool? handlerResult = Instance?.TryExecuteInternal(source, out handlerValue); + + if (handlerResult.HasValue) + { + result = handlerValue; + return handlerResult.Value; + } + + if (typeof(T).IsEnum && source is string s) + { + try + { + result = (T)Enum.Parse(typeof(T), s, true); + return true; + } + catch + { + } + } + + try + { + result = (T)Convert.ChangeType(source, typeof(T)); + return true; + } + catch + { + result = default; + return false; + } + } + + protected abstract bool? TryExecuteInternal(object? source, out T? result); + } + + private class BoolConverter : ConverterItem + { + protected override bool? TryExecuteInternal(object? source, out bool result) + { + if (source is string s) + { + return bool.TryParse(s, out result); + } + + result = false; + return null; + } + } + + private class IntConverter : ConverterItem + { + protected override bool? TryExecuteInternal(object? source, out int result) + { + if (source is string s) + { + return int.TryParse(s, out result); + } + + result = 0; + return null; + } + } + + private class LongConverter : ConverterItem + { + protected override bool? TryExecuteInternal(object? source, out long result) + { + if (source is string s) + { + return long.TryParse(s, out result); + } + + result = 0; + return null; + } + } + + private class FloatConverter : ConverterItem + { + protected override bool? TryExecuteInternal(object? source, out float result) + { + if (source is string s) + { + return float.TryParse(s, out result); + } + + result = 0; + return null; + } + } + + private class DoubleConverter : ConverterItem + { + protected override bool? TryExecuteInternal(object? source, out double result) + { + if (source is string s) + { + return ParserExtensions.DoubleTryParseCurrentOrInvariant(s, out result); + } + + result = 0; + return null; + } + } + + private class StringConverter : ConverterItem + { + protected override bool? TryExecuteInternal(object? source, out string? result) + { + result = source?.ToString(); + return true; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/CppStyleEvaluatorDefinition.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/CppStyleEvaluatorDefinition.cs new file mode 100644 index 000000000000..efc8d17dfec4 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/CppStyleEvaluatorDefinition.cs @@ -0,0 +1,580 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Core.Expressions.Cpp +{ + public static class CppStyleEvaluatorDefinition + { + private const int ReservedTokenCount = 24; + private const int ReservedTokenMaxIndex = ReservedTokenCount - 1; + private static readonly IOperationProvider[] NoOperationProviders = []; + private static readonly char[] SupportedQuotes = { '"', '\'' }; + + public static bool EvaluateFromString(ILogger logger, string text, IVariableCollection variables) + { + using MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(text)); + using MemoryStream res = new MemoryStream(); + EngineConfig cfg = new EngineConfig(logger, variables); + IProcessorState state = new ProcessorState(ms, res, (int)ms.Length, (int)ms.Length, cfg, NoOperationProviders); + int len = (int)ms.Length; + int pos = 0; + return Evaluate(state, ref len, ref pos, out bool faulted); + } + + public static bool Evaluate(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, out bool faulted) + { + faulted = false; + TokenTrie trie = new TokenTrie(); + + //Logic + trie.AddToken(processor.Encoding.GetBytes("&&"), 0); + trie.AddToken(processor.Encoding.GetBytes("||"), 1); + trie.AddToken(processor.Encoding.GetBytes("^"), 2); + trie.AddToken(processor.Encoding.GetBytes("!"), 3); + trie.AddToken(processor.Encoding.GetBytes(">"), 4); + trie.AddToken(processor.Encoding.GetBytes(">="), 5); + trie.AddToken(processor.Encoding.GetBytes("<"), 6); + trie.AddToken(processor.Encoding.GetBytes("<="), 7); + trie.AddToken(processor.Encoding.GetBytes("=="), 8); + trie.AddToken(processor.Encoding.GetBytes("="), 9); + trie.AddToken(processor.Encoding.GetBytes("!="), 10); + + //Bitwise + trie.AddToken(processor.Encoding.GetBytes("&"), 11); + trie.AddToken(processor.Encoding.GetBytes("|"), 12); + trie.AddToken(processor.Encoding.GetBytes("<<"), 13); + trie.AddToken(processor.Encoding.GetBytes(">>"), 14); + + //Braces + trie.AddToken(processor.Encoding.GetBytes("("), 15); + trie.AddToken(processor.Encoding.GetBytes(")"), 16); + + //Whitespace + trie.AddToken(processor.Encoding.GetBytes(" "), 17); + trie.AddToken(processor.Encoding.GetBytes("\t"), 18); + + //EOLs + trie.AddToken(processor.Encoding.GetBytes("\r\n"), 19); + trie.AddToken(processor.Encoding.GetBytes("\n"), 20); + trie.AddToken(processor.Encoding.GetBytes("\r"), 21); + + // quotes + trie.AddToken(processor.Encoding.GetBytes("\""), 22); + trie.AddToken(processor.Encoding.GetBytes("'"), 23); + + //Tokens + trie.Append(processor.EncodingConfig.Variables); + + //Run forward to EOL and collect args + TokenFamily currentTokenFamily; + List currentTokenBytes = new List(); + List tokens = new List(); + if (!trie.GetOperation(processor.CurrentBuffer, bufferLength, ref currentBufferPosition, out int token)) + { + currentTokenFamily = TokenFamily.Literal; + currentTokenBytes.Add(processor.CurrentBuffer[currentBufferPosition++]); + } + else if (token > ReservedTokenMaxIndex) + { + currentTokenFamily = TokenFamily.Reference | (TokenFamily)token; + tokens.Add(new TokenRef + { + Family = currentTokenFamily + }); + } + else + { + currentTokenFamily = (TokenFamily)token; + + if (currentTokenFamily is not TokenFamily.WindowsEOL and not TokenFamily.LegacyMacEOL and not TokenFamily.UnixEOL) + { + tokens.Add(new TokenRef + { + Family = currentTokenFamily + }); + } + else + { + return EvaluateCondition(tokens, processor.EncodingConfig.VariableValues); + } + } + + int braceDepth = 0; + if (tokens.Count > 0 && tokens[0].Family == TokenFamily.OpenBrace) + { + ++braceDepth; + } + + bool first = true; + QuotedRegionKind inQuoteType = QuotedRegionKind.None; + + while ((first || braceDepth > 0) && bufferLength > 0) + { + int targetLen = Math.Min(bufferLength, trie.MaxLength); + for (; currentBufferPosition < bufferLength - targetLen + 1;) + { + int oldBufferPos = currentBufferPosition; + if (trie.GetOperation(processor.CurrentBuffer, bufferLength, ref currentBufferPosition, out token)) + { + if (braceDepth == 0) + { + switch (tokens[tokens.Count - 1].Family) + { + case TokenFamily.Whitespace: + case TokenFamily.Tab: + case TokenFamily.CloseBrace: + case TokenFamily.WindowsEOL: + case TokenFamily.UnixEOL: + case TokenFamily.LegacyMacEOL: + TokenFamily thisFamily = (TokenFamily)token; + if (thisFamily is TokenFamily.WindowsEOL or TokenFamily.UnixEOL or TokenFamily.LegacyMacEOL) + { + currentBufferPosition = oldBufferPos; + } + + break; + default: + currentBufferPosition = oldBufferPos; + first = false; + break; + } + + if (!first) + { + break; + } + } + + // We matched an item, so whatever this is, it's not a literal. + // if the current token is a literal, end it. + if (currentTokenFamily == TokenFamily.Literal) + { + string literal = processor.Encoding.GetString(currentTokenBytes.ToArray()); + tokens.Add(new TokenRef + { + Family = TokenFamily.Literal, + Literal = literal + }); + currentTokenBytes.Clear(); + } + + TokenFamily foundTokenFamily = (TokenFamily)token; + + if (foundTokenFamily is TokenFamily.QuotedLiteral or TokenFamily.SingleQuotedLiteral) + { + var incomingQuoteKind = foundTokenFamily switch + { + TokenFamily.QuotedLiteral => QuotedRegionKind.DoubleQuoteRegion, + TokenFamily.SingleQuotedLiteral => QuotedRegionKind.SingleQuoteRegion, + _ => QuotedRegionKind.None, + }; + if (inQuoteType == QuotedRegionKind.None) + { + // starting quote found + currentTokenBytes.AddRange(trie.Tokens[token].Value); + inQuoteType = incomingQuoteKind; + } + else if (incomingQuoteKind == inQuoteType) + { + // end quote found + currentTokenBytes.AddRange(trie.Tokens[token].Value); + tokens.Add(new TokenRef + { + Family = TokenFamily.Literal, + Literal = processor.Encoding.GetString(currentTokenBytes.ToArray()) + }); + currentTokenBytes.Clear(); + inQuoteType = QuotedRegionKind.None; + } + else + { + // this is a different quote type. Treat it like a non-match, just add the token to the currentTokenBytes + currentTokenBytes.AddRange(trie.Tokens[token].Value); + } + } + else if (inQuoteType != QuotedRegionKind.None) + { + // we're inside a quoted literal, the token found by the trie should not be processed, just included with the literal + currentTokenBytes.AddRange(trie.Tokens[token].Value); + } + else if (token > ReservedTokenMaxIndex) + { + currentTokenFamily = TokenFamily.Reference | (TokenFamily)token; + tokens.Add(new TokenRef + { + Family = currentTokenFamily + }); + } + else + { + //If we have a normal token... + currentTokenFamily = (TokenFamily)token; + + if (currentTokenFamily is not TokenFamily.WindowsEOL and not TokenFamily.LegacyMacEOL and not TokenFamily.UnixEOL) + { + switch (currentTokenFamily) + { + case TokenFamily.OpenBrace: + ++braceDepth; + break; + case TokenFamily.CloseBrace: + --braceDepth; + break; + } + + tokens.Add(new TokenRef + { + Family = currentTokenFamily + }); + } + else + { + return EvaluateCondition(tokens, processor.EncodingConfig.VariableValues); + } + } + } + else if (inQuoteType != QuotedRegionKind.None) + { + // we're in a quoted literal but did not match a token at the current position. + // so just add the current byte to the currentTokenBytes + currentTokenBytes.Add(processor.CurrentBuffer[currentBufferPosition++]); + } + else if (braceDepth > 0) + { + currentTokenFamily = TokenFamily.Literal; + currentTokenBytes.Add(processor.CurrentBuffer[currentBufferPosition++]); + } + else + { + first = false; + break; + } + } + + processor.AdvanceBuffer(currentBufferPosition); + currentBufferPosition = processor.CurrentBufferPosition; + bufferLength = processor.CurrentBufferLength; + } + +#if DEBUG + Debug.Assert( + inQuoteType == QuotedRegionKind.None, + $"Malformed predicate due to unmatched quotes. InitialBuffer = {processor.Encoding.GetString(processor.CurrentBuffer)} currentTokenFamily = {currentTokenFamily} | TokenFamily.QuotedLiteral = {TokenFamily.QuotedLiteral} | TokenFamily.SingleQuotedLiteral = {TokenFamily.SingleQuotedLiteral}"); +#endif + + return EvaluateCondition(tokens, processor.EncodingConfig.VariableValues); + } + + private static bool IsLogicalOperator(Operator op) + { + return op is Operator.And or Operator.Or or Operator.Xor or Operator.Not; + } + + private static void CombineExpressionOperator(ref Scope current, Stack parents) + { + if (current.Operator == Operator.None) + { + if (current.TargetPlacement != Scope.NextPlacement.Right + || current.Left is not Scope leftScope + || !IsLogicalOperator(leftScope.Operator)) + { + return; + } + + Scope tmp2 = new Scope + { + Value = leftScope.Right! + }; + + leftScope.Right = tmp2; + parents.Push(leftScope); + current = tmp2; + return; + } + + Scope tmp = new Scope(); + + if (!IsLogicalOperator(current.Operator)) + { + tmp.Value = current; + } + else + { + tmp.Value = current.Right!; + current.Right = tmp; + parents.Push(current); + } + + current = tmp; + } + + private static bool EvaluateCondition(IReadOnlyList tokens, IReadOnlyList> values) + { + //Skip over all leading whitespace + int i = 0; + for (; i < tokens.Count && (tokens[i].Family == TokenFamily.Whitespace || tokens[i].Family == TokenFamily.Tab); ++i) + { + } + + //Scan through all remaining tokens and put them in a form the standard evaluator can understand + List outputTokens = new List(); + for (; i < tokens.Count; ++i) + { + if (tokens[i].Family is TokenFamily.Whitespace or TokenFamily.Tab) + { + //Ignore whitespace + } + else + { + if (tokens[i].Family.HasFlag(TokenFamily.Reference)) + { + outputTokens.Add(tokens[i]); + } + else if (tokens[i].Family == TokenFamily.Literal) + { + //Combine literals + string literalValue = tokens[i].Literal!; + string followingWhitespace = string.Empty; + int reach = i; + + for (int j = i + 1; j < tokens.Count; ++j) + { + switch (tokens[j].Family) + { + case TokenFamily.Literal: + literalValue += followingWhitespace + tokens[j].Literal; + followingWhitespace = string.Empty; + reach = j; + break; + case TokenFamily.Tab: + followingWhitespace += '\t'; + break; + case TokenFamily.Whitespace: + followingWhitespace += ' '; + break; + default: + j = tokens.Count; + break; + } + } + + i = reach; + outputTokens.Add(new TokenRef + { + Family = TokenFamily.Literal, + Literal = literalValue + }); + } + else + { + outputTokens.Add(tokens[i]); + } + } + } + + Scope root = new Scope(); + Scope current = root; + Stack parents = new Stack(); + + for (i = 0; i < outputTokens.Count; ++i) + { + switch (outputTokens[i].Family) + { + case TokenFamily.Not: + { + Scope nextScope = new Scope + { + TargetPlacement = Scope.NextPlacement.Right + }; + + current.Value = nextScope; + parents.Push(current); + current = nextScope; + current.Operator = Operator.Not; + break; + } + case TokenFamily.Literal: + current.Value = InferTypeAndConvertLiteral(outputTokens[i].Literal!)!; + + while (parents.Count > 0 && current.TargetPlacement == Scope.NextPlacement.None) + { + current = parents.Pop(); + } + break; + case TokenFamily.And: + if (current.Operator != Operator.None) + { + Scope s = new Scope + { + Value = current + }; + + current = s; + } + + current.Operator = Operator.And; + current.TargetPlacement = Scope.NextPlacement.Right; + break; + case TokenFamily.BitwiseAnd: + CombineExpressionOperator(ref current, parents); + current.Operator = Operator.BitwiseAnd; + break; + case TokenFamily.BitwiseOr: + CombineExpressionOperator(ref current, parents); + current.Operator = Operator.BitwiseOr; + break; + case TokenFamily.CloseBrace: + //If the grouping is valid, the grouping has already been closed + // due to argument fulfillment, do nothing. + // -- OR -- + //This is a grouping around a unary operator + if (parents.Count > 0 && current.TargetPlacement == Scope.NextPlacement.Right) + { + current = parents.Pop(); + } + break; + case TokenFamily.EqualTo: + case TokenFamily.EqualToShort: + CombineExpressionOperator(ref current, parents); + current.Operator = Operator.EqualTo; + break; + case TokenFamily.GreaterThan: + CombineExpressionOperator(ref current, parents); + current.Operator = Operator.GreaterThan; + break; + case TokenFamily.GreaterThanOrEqualTo: + CombineExpressionOperator(ref current, parents); + current.Operator = Operator.GreaterThanOrEqualTo; + break; + case TokenFamily.LeftShift: + CombineExpressionOperator(ref current, parents); + current.Operator = Operator.LeftShift; + break; + case TokenFamily.LessThan: + CombineExpressionOperator(ref current, parents); + current.Operator = Operator.LessThan; + break; + case TokenFamily.LessThanOrEqualTo: + CombineExpressionOperator(ref current, parents); + current.Operator = Operator.LessThanOrEqualTo; + break; + case TokenFamily.NotEqualTo: + CombineExpressionOperator(ref current, parents); + current.Operator = Operator.NotEqualTo; + break; + case TokenFamily.OpenBrace: + { + Scope nextScope = new Scope(); + current.Value = nextScope; + parents.Push(current); + current = nextScope; + break; + } + case TokenFamily.Or: + if (current.Operator != Operator.None) + { + Scope s = new Scope + { + Value = current + }; + + current = s; + } + + current.Operator = Operator.Or; + break; + case TokenFamily.RightShift: + CombineExpressionOperator(ref current, parents); + current.Operator = Operator.RightShift; + break; + case TokenFamily.Xor: + if (current.Operator != Operator.None) + { + Scope s = new Scope + { + Value = current + }; + + current = s; + } + + current.Operator = Operator.Xor; + break; + default: + current.Value = ResolveToken(outputTokens[i], values); + + while (parents.Count > 0 && current.TargetPlacement == Scope.NextPlacement.None) + { + current = parents.Pop(); + } + + break; + } + } + + Debug.Assert(parents.Count == 0, "Unbalanced condition"); + return (bool)Convert.ChangeType(current.Evaluate() ?? "false", typeof(bool)); + } + + private static object? InferTypeAndConvertLiteral(string literal) + { + // A properly quoted string must be... + // At least two characters long + // Start and end with the same character + // The character that the string starts with must be one of the supported quote kinds + if (literal.Length < 2 || literal[0] != literal[literal.Length - 1] || !SupportedQuotes.Contains(literal[0])) + { + if (string.Equals(literal, "true", StringComparison.OrdinalIgnoreCase)) // CodeQL [cs/campaign/constantine] False Positive: CodeQL wrongly detected "literal" + { + return true; + } + + if (string.Equals(literal, "false", StringComparison.OrdinalIgnoreCase)) // CodeQL [cs/campaign/constantine] False Positive: CodeQL wrongly detected "literal" + { + return false; + } + + if (string.Equals(literal, "null", StringComparison.OrdinalIgnoreCase)) // CodeQL [cs/campaign/constantine] False Positive: CodeQL wrongly detected "literal" + { + return null; + } + + if ((literal.Contains(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator) + || literal.Contains(CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator)) + && ParserExtensions.DoubleTryParseCurrentOrInvariant(literal, out double literalDouble)) + { + return literalDouble; + } + + if (long.TryParse(literal, out long literalLong)) + { + return literalLong; + } + + if (literal.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + && long.TryParse(literal.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out literalLong)) + { + return literalLong; + } + + return null; + } + + return literal.Substring(1, literal.Length - 2); + } + + private static object ResolveToken(TokenRef tokenRef, IReadOnlyList> values) + { + return values[(int)(tokenRef.Family & ~TokenFamily.Reference) - ReservedTokenCount](); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/Operator.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/Operator.cs new file mode 100644 index 000000000000..3e4bf204d6cd --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/Operator.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions.Cpp +{ + internal enum Operator + { + None, + And, + Or, + Xor, + Not, + GreaterThan, + GreaterThanOrEqualTo, + LessThan, + LessThanOrEqualTo, + EqualTo, + NotEqualTo, + BitwiseAnd, + BitwiseOr, + LeftShift, + RightShift + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/QuotedRegionKind.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/QuotedRegionKind.cs new file mode 100644 index 000000000000..8aa26d00dc28 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/QuotedRegionKind.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions.Cpp +{ + public enum QuotedRegionKind + { + None, + DoubleQuoteRegion, + SingleQuoteRegion + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/Scope.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/Scope.cs new file mode 100644 index 000000000000..e3f9714155fe --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/Scope.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Core.Expressions.Cpp +{ + internal class Scope + { + public enum NextPlacement + { + Left, + Right, + None + } + + public object? Left { get; set; } + + public Operator Operator { get; set; } + + public object? Right { get; set; } + + public object Value + { + set + { + switch (TargetPlacement) + { + case NextPlacement.Left: + Left = value; + TargetPlacement = NextPlacement.Right; + break; + case NextPlacement.Right: + Right = value; + TargetPlacement = NextPlacement.None; + break; + } + } + } + + public NextPlacement TargetPlacement { get; set; } + + public static TResult EvaluateSides(object left, object right, Func convertLeft, Func convertRight, Func combine) + { + TLeft l = EvaluateSide(left, convertLeft); + TRight r = EvaluateSide(right, convertRight); + return combine(l, r); + } + + public static TResult EvaluateSides(object left, object right, Func convert, Func combine) + { + return EvaluateSides(left, right, convert, convert, combine); + } + + public object Evaluate() + { + switch (Operator) + { + case Operator.Not: + return !EvaluateSide(Right!, x => Convert.ToBoolean(x ?? "False")); + case Operator.And: + return EvaluateSides(Left!, Right!, x => (bool)x, (x, y) => x && y); + case Operator.Or: + return EvaluateSides(Left!, Right!, x => (bool)x, (x, y) => x || y); + case Operator.Xor: + return EvaluateSides(Left!, Right!, x => (bool)x, (x, y) => x ^ y); + case Operator.EqualTo: + return EvaluateSides(Left!, Right!, x => x, LenientEquals); + case Operator.NotEqualTo: + return EvaluateSides(Left!, Right!, x => x, (x, y) => !LenientEquals(x, y)); + case Operator.GreaterThan: + return EvaluateSides(Left!, Right!, ParserExtensions.ConvertToDoubleCurrentOrInvariant, (x, y) => x > y); + case Operator.GreaterThanOrEqualTo: + return EvaluateSides(Left!, Right!, ParserExtensions.ConvertToDoubleCurrentOrInvariant, (x, y) => x >= y); + case Operator.LessThan: + return EvaluateSides(Left!, Right!, ParserExtensions.ConvertToDoubleCurrentOrInvariant, (x, y) => x < y); + case Operator.LessThanOrEqualTo: + return EvaluateSides(Left!, Right!, ParserExtensions.ConvertToDoubleCurrentOrInvariant, (x, y) => x <= y); + case Operator.LeftShift: + return EvaluateSides(Left!, Right!, Convert.ToInt64, Convert.ToInt32, (x, y) => x << y); + case Operator.RightShift: + return EvaluateSides(Left!, Right!, Convert.ToInt64, Convert.ToInt32, (x, y) => x >> y); + case Operator.BitwiseAnd: + return EvaluateSides(Left!, Right!, Convert.ToInt64, (x, y) => x & y); + case Operator.BitwiseOr: + return EvaluateSides(Left!, Right!, Convert.ToInt64, (x, y) => x | y); + default: + if (Left != null) + { + return EvaluateSide(Left, x => x); + } + + return false; + } + } + + private static T EvaluateSide(object side, Func convert) + { + if (side is Scope scope) + { + return convert(scope.Evaluate()); + } + + return convert(side); + } + + private static bool LenientEquals(object x, object y) + { + if (x is string sx && y is string sy) + { + return string.Equals(sx, sy, StringComparison.OrdinalIgnoreCase); + } + + if (MultiValueParameter.TryPerformMultiValueEqual(x, y, out bool result)) + { + return result; + } + + return Equals(x, y); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/TokenFamily.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/TokenFamily.cs new file mode 100644 index 000000000000..86e914b64345 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/TokenFamily.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions.Cpp +{ + [Flags] + internal enum TokenFamily + { + And, + Or, + Xor, + Not, + GreaterThan, + GreaterThanOrEqualTo, + LessThan, + LessThanOrEqualTo, + EqualTo, + EqualToShort, + NotEqualTo, + BitwiseAnd, + BitwiseOr, + LeftShift, + RightShift, + OpenBrace, + CloseBrace, + Whitespace, + Tab, + WindowsEOL, + UnixEOL, + LegacyMacEOL, + QuotedLiteral, + SingleQuotedLiteral, + Literal, + Reference = 0x40000000 + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/TokenRef.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/TokenRef.cs new file mode 100644 index 000000000000..43557acc38b0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp/TokenRef.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions.Cpp +{ + internal class TokenRef + { + public TokenFamily Family { get; set; } + + public string? Literal { get; set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp2/Cpp2StyleEvaluatorDefinition.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp2/Cpp2StyleEvaluatorDefinition.cs new file mode 100644 index 000000000000..3623b5f8eae3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Cpp2/Cpp2StyleEvaluatorDefinition.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Expressions.Shared; +using Microsoft.TemplateEngine.Core.Util; + +namespace Microsoft.TemplateEngine.Core.Expressions.Cpp2 +{ + public class Cpp2StyleEvaluatorDefinition : SharedEvaluatorDefinition + { + private static readonly ConcurrentDictionary TokenCache = new(); + + public enum Tokens + { + And = 0, + Or = 1, + Not = 2, + GreaterThan = 3, + GreaterThanOrEqualTo = 4, + LessThan = 5, + LessThanOrEqualTo = 6, + EqualTo = 7, + NotEqualTo = 8, + OpenBrace = 9, + CloseBrace = 10, + Space = 11, + Tab = 12, + WindowsEOL = 13, + UnixEOL = 14, + LegacyMacEOL = 15, + Quote = 16, + LeftShift = 17, + RightShift = 18, + Add = 19, + Subtract = 20, + Multiply = 21, + Divide = 22, + BitwiseAnd = 23, + BitwiseOr = 24, + SingleQuote = 25, + DoubleQuote = 26, + Literal = 27 + } + + protected override bool DereferenceInLiterals => false; + + protected override string NullTokenValue => "null"; + + protected override IOperatorMap GenerateMap() => new OperatorSetBuilder(CppStyleConverters.Encode, CppStyleConverters.Decode) + .And(Tokens.And) + .Or(Tokens.Or) + .Not(Tokens.Not) + .GreaterThan(Tokens.GreaterThan, evaluate: (x, y) => Compare(x, y) > 0) + .GreaterThanOrEqualTo(Tokens.GreaterThanOrEqualTo, evaluate: (x, y) => Compare(x, y) >= 0) + .LessThan(Tokens.LessThan, evaluate: (x, y) => Compare(x, y) < 0) + .LessThanOrEqualTo(Tokens.LessThanOrEqualTo, evaluate: (x, y) => Compare(x, y) <= 0) + .EqualTo(Tokens.EqualTo, evaluate: (x, y) => Compare(x, y) == 0) + .NotEqualTo(Tokens.NotEqualTo, evaluate: (x, y) => Compare(x, y) != 0) + .Ignore(Tokens.Space, Tokens.Tab) + .LiteralBoundsMarkers(Tokens.Quote) + .OpenGroup(Tokens.OpenBrace) + .CloseGroup(Tokens.CloseBrace) + .TerminateWith(Tokens.WindowsEOL, Tokens.UnixEOL, Tokens.LegacyMacEOL) + .LeftShift(Tokens.LeftShift) + .RightShift(Tokens.RightShift) + .Add(Tokens.Add) + .Subtract(Tokens.Subtract) + .Multiply(Tokens.Multiply) + .Divide(Tokens.Divide) + .BitwiseAnd(Tokens.BitwiseAnd) + .BitwiseOr(Tokens.BitwiseOr) + .Literal(Tokens.Literal) + .LiteralBoundsMarkers(Tokens.SingleQuote, Tokens.DoubleQuote) + .TypeConverter(CppStyleConverters.ConfigureConverters); + + protected override ITokenTrie GetSymbols(IProcessorState processor) + { + if (!TokenCache.TryGetValue(processor.Encoding, out ITokenTrie tokens)) + { + TokenTrie trie = new(); + + // Logic + trie.AddToken(processor.Encoding.GetBytes("&&")); + trie.AddToken(processor.Encoding.GetBytes("||")); + trie.AddToken(processor.Encoding.GetBytes("!")); + trie.AddToken(processor.Encoding.GetBytes(">")); + trie.AddToken(processor.Encoding.GetBytes(">=")); + trie.AddToken(processor.Encoding.GetBytes("<")); + trie.AddToken(processor.Encoding.GetBytes("<=")); + trie.AddToken(processor.Encoding.GetBytes("==")); + trie.AddToken(processor.Encoding.GetBytes("!=")); + + // Braces + trie.AddToken(processor.Encoding.GetBytes("(")); + trie.AddToken(processor.Encoding.GetBytes(")")); + + // Whitespace + trie.AddToken(processor.Encoding.GetBytes(" ")); + trie.AddToken(processor.Encoding.GetBytes("\t")); + + // EOLs + trie.AddToken(processor.Encoding.GetBytes("\r\n")); + trie.AddToken(processor.Encoding.GetBytes("\n")); + trie.AddToken(processor.Encoding.GetBytes("\r")); + + // quotes + trie.AddToken(processor.Encoding.GetBytes("'")); + + // Shifts + trie.AddToken(processor.Encoding.GetBytes("<<")); + trie.AddToken(processor.Encoding.GetBytes(">>")); + + // Math operators + trie.AddToken(processor.Encoding.GetBytes("+")); + trie.AddToken(processor.Encoding.GetBytes("-")); + trie.AddToken(processor.Encoding.GetBytes("*")); + trie.AddToken(processor.Encoding.GetBytes("/")); + + // Bitwise operators + trie.AddToken(processor.Encoding.GetBytes("&")); + trie.AddToken(processor.Encoding.GetBytes("|")); + + // quotes + trie.AddToken(processor.Encoding.GetBytes("'")); + trie.AddToken(processor.Encoding.GetBytes("\"")); + + TokenCache[processor.Encoding] = tokens = trie; + } + + return tokens; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/IEvaluable.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/IEvaluable.cs new file mode 100644 index 000000000000..c3326b5e6df2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/IEvaluable.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public interface IEvaluable + { + bool IsFull { get; } + + bool IsIndivisible { get; } + + IEvaluable? Parent { get; set; } + + object? Evaluate(); + + bool TryAccept(IEvaluable? child); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/IOperatorMap.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/IOperatorMap.cs new file mode 100644 index 000000000000..718929357a28 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/IOperatorMap.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public interface IOperatorMap + where TToken : struct + { + ISet BadSyntaxTokens { get; } + + TToken CloseGroupToken { get; } + + TOperator Identity { get; } + + ISet LiteralSequenceBoundsMarkers { get; } + + TToken LiteralToken { get; } + + ISet NoOpTokens { get; } + + TToken OpenGroupToken { get; } + + IReadOnlyDictionary> OperatorScopeLookupFactory { get; } + + ISet Terminators { get; } + + IReadOnlyDictionary TokensToOperatorsMap { get; } + + bool TryConvert(object? source, out T? result); + + string Decode(string value); + + string Encode(string value); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/ITypeConverter.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/ITypeConverter.cs new file mode 100644 index 000000000000..86df3fadbd73 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/ITypeConverter.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public interface ITypeConverter + { + ITypeConverter Register(TypeConverterDelegate converter); + + bool TryConvert(object? source, out T? result); + + bool TryCoreConvert(object? source, out T? result); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/MSBuild/MSBuildStyleEvaluatorDefinition.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/MSBuild/MSBuildStyleEvaluatorDefinition.cs new file mode 100644 index 000000000000..a3c195e3e5e2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/MSBuild/MSBuildStyleEvaluatorDefinition.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Expressions.Shared; +using Microsoft.TemplateEngine.Core.Util; + +namespace Microsoft.TemplateEngine.Core.Expressions.MSBuild +{ + public class MSBuildStyleEvaluatorDefinition : SharedEvaluatorDefinition + { + private static readonly Dictionary TokenCache = new Dictionary(); + + public enum Tokens + { + And = 0, + Or = 1, + Not = 2, + GreaterThan = 3, + GreaterThanOrEqualTo = 4, + LessThan = 5, + LessThanOrEqualTo = 6, + EqualTo = 7, + NotEqualTo = 8, + OpenBrace = 9, + CloseBrace = 10, + Space = 11, + Tab = 12, + WindowsEOL = 13, + UnixEOL = 14, + LegacyMacEOL = 15, + Quote = 16, + VariableStart = 17, + Literal = 18 + } + + protected override bool DereferenceInLiterals => true; + + protected override string NullTokenValue => "null"; + + protected override IOperatorMap GenerateMap() => new OperatorSetBuilder(XmlStyleConverters.XmlEncode, XmlStyleConverters.XmlDecode) + .And(Tokens.And) + .Or(Tokens.Or) + .Not(Tokens.Not) + .GreaterThan(Tokens.GreaterThan, evaluate: (x, y) => Compare(x, y) > 0) + .GreaterThanOrEqualTo(Tokens.GreaterThanOrEqualTo, evaluate: (x, y) => Compare(x, y) >= 0) + .LessThan(Tokens.LessThan, evaluate: (x, y) => Compare(x, y) < 0) + .LessThanOrEqualTo(Tokens.LessThanOrEqualTo, evaluate: (x, y) => Compare(x, y) <= 0) + .EqualTo(Tokens.EqualTo, evaluate: (x, y) => Compare(x, y) == 0) + .NotEqualTo(Tokens.NotEqualTo, evaluate: (x, y) => Compare(x, y) != 0) + .BadSyntax(Tokens.VariableStart) + .Ignore(Tokens.Space, Tokens.Tab) + .LiteralBoundsMarkers(Tokens.Quote) + .OpenGroup(Tokens.OpenBrace) + .CloseGroup(Tokens.CloseBrace) + .TerminateWith(Tokens.WindowsEOL, Tokens.UnixEOL, Tokens.LegacyMacEOL) + .Literal(Tokens.Literal) + .TypeConverter(CppStyleConverters.ConfigureConverters); + + protected override ITokenTrie GetSymbols(IProcessorState processor) + { + if (!TokenCache.TryGetValue(processor.Encoding, out ITokenTrie tokens)) + { + TokenTrie trie = new TokenTrie(); + + //Logic + trie.AddToken(processor.Encoding.GetBytes("AND")); + trie.AddToken(processor.Encoding.GetBytes("OR")); + trie.AddToken(processor.Encoding.GetBytes("!")); + trie.AddToken(processor.Encoding.GetBytes(">")); + trie.AddToken(processor.Encoding.GetBytes(">=")); + trie.AddToken(processor.Encoding.GetBytes("<")); + trie.AddToken(processor.Encoding.GetBytes("<=")); + trie.AddToken(processor.Encoding.GetBytes("==")); + trie.AddToken(processor.Encoding.GetBytes("!=")); + + //Braces + trie.AddToken(processor.Encoding.GetBytes("(")); + trie.AddToken(processor.Encoding.GetBytes(")")); + + //Whitespace + trie.AddToken(processor.Encoding.GetBytes(" ")); + trie.AddToken(processor.Encoding.GetBytes("\t")); + + //EOLs + trie.AddToken(processor.Encoding.GetBytes("\r\n")); + trie.AddToken(processor.Encoding.GetBytes("\n")); + trie.AddToken(processor.Encoding.GetBytes("\r")); + + // quotes + trie.AddToken(processor.Encoding.GetBytes("'")); + + // variable start + trie.AddToken(processor.Encoding.GetBytes("$(")); + + TokenCache[processor.Encoding] = tokens = trie; + } + + return tokens; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/OperatorSetBuilder.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/OperatorSetBuilder.cs new file mode 100644 index 000000000000..aad9f9fc067f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/OperatorSetBuilder.cs @@ -0,0 +1,637 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public class OperatorSetBuilder : IOperatorMap + where TToken : struct + { + private readonly Func _decoder; + private readonly Func _encoder; + private readonly Dictionary> _operatorScopeLookupFactory = new Dictionary>(); + private readonly Dictionary _tokensToOperatorsMap = new Dictionary(); + private ITypeConverter _converter; + + //TODO: The signatures for encoder and decoder will need to be updated to account + // for encoding/decoding errors, the host will need to be inputted here as well + // for the purposes of logging evaluation/parse errors + public OperatorSetBuilder(Func encoder, Func decoder) + { + _encoder = encoder ?? Passthrough; + _decoder = decoder ?? Passthrough; + BadSyntaxTokens = new HashSet(); + NoOpTokens = new HashSet(); + LiteralSequenceBoundsMarkers = new HashSet(); + Terminators = new HashSet(); + _converter = new CustomTypeConverter>(); + } + + public ISet BadSyntaxTokens { get; } + + public TToken CloseGroupToken { get; private set; } + + public Operators Identity => Operators.Identity; + + public ISet LiteralSequenceBoundsMarkers { get; } + + public TToken LiteralToken { get; private set; } + + public ISet NoOpTokens { get; } + + public TToken OpenGroupToken { get; private set; } + + public IReadOnlyDictionary> OperatorScopeLookupFactory => _operatorScopeLookupFactory; + + public ISet Terminators { get; } + + public IReadOnlyDictionary TokensToOperatorsMap => _tokensToOperatorsMap; + + public OperatorSetBuilder Add(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.Add, token, evaluate ?? Add, precedesOperator); + } + + public OperatorSetBuilder And(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.And, token, evaluate ?? And, precedesOperator); + } + + public OperatorSetBuilder BadSyntax(params TToken[] token) + { + BadSyntaxTokens.UnionWith(token); + return this; + } + + public OperatorSetBuilder BitwiseAnd(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.BitwiseAnd, token, evaluate ?? BitwiseAnd, precedesOperator); + } + + public OperatorSetBuilder BitwiseOr(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.BitwiseOr, token, evaluate ?? BitwiseOr, precedesOperator); + } + + public OperatorSetBuilder CloseGroup(TToken token) + { + CloseGroupToken = token; + return this; + } + + public string Decode(string value) + { + return _decoder(value); + } + + public OperatorSetBuilder Divide(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.Divide, token, evaluate ?? Divide, precedesOperator); + } + + public string Encode(string value) + { + return _encoder(value); + } + + public OperatorSetBuilder EqualTo(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.EqualTo, token, evaluate ?? EqualTo, precedesOperator); + } + + public OperatorSetBuilder Exponentiate(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.Exponentiate, token, evaluate ?? Exponentiate, precedesOperator); + } + + public OperatorSetBuilder GreaterThan(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.GreaterThan, token, evaluate ?? GreaterThan, precedesOperator); + } + + public OperatorSetBuilder GreaterThanOrEqualTo(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.GreaterThanOrEqualTo, token, evaluate ?? GreaterThanOrEqualTo, precedesOperator); + } + + public OperatorSetBuilder Ignore(params TToken[] token) + { + NoOpTokens.UnionWith(token); + return this; + } + + public OperatorSetBuilder LeftShift(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.LeftShift, token, evaluate ?? LeftShift, precedesOperator); + } + + public OperatorSetBuilder LessThan(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.LessThan, token, evaluate ?? LessThan, precedesOperator); + } + + public OperatorSetBuilder LessThanOrEqualTo(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.LessThanOrEqualTo, token, evaluate ?? LessThanOrEqualTo, precedesOperator); + } + + public OperatorSetBuilder Literal(TToken token) + { + LiteralToken = token; + return this; + } + + public OperatorSetBuilder LiteralBoundsMarkers(params TToken[] token) + { + LiteralSequenceBoundsMarkers.UnionWith(token); + return this; + } + + public OperatorSetBuilder Multiply(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.Multiply, token, evaluate ?? Multiply, precedesOperator); + } + + public OperatorSetBuilder Not(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + _operatorScopeLookupFactory[Operators.Not] = + x => CreateUnaryChild(x, Operators.Not, evaluate ?? Not); + _tokensToOperatorsMap[token] = Operators.Not; + return this; + } + + public OperatorSetBuilder NotEqualTo(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.NotEqualTo, token, evaluate ?? NotEqualTo, precedesOperator); + } + + public OperatorSetBuilder OpenGroup(TToken token) + { + OpenGroupToken = token; + return this; + } + + public OperatorSetBuilder Or(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.Or, token, evaluate ?? Or, precedesOperator); + } + + public OperatorSetBuilder Other(Operators @operator, TToken token, Func nodeFactory) + { + _operatorScopeLookupFactory[@operator] = nodeFactory; + _tokensToOperatorsMap[token] = @operator; + return this; + } + + public OperatorSetBuilder RightShift(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.RightShift, token, evaluate ?? RightShift, precedesOperator); + } + + public OperatorSetBuilder Subtract(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.Subtract, token, evaluate ?? Subtract, precedesOperator); + } + + public OperatorSetBuilder TerminateWith(params TToken[] token) + { + Terminators.UnionWith(token); + return this; + } + + public bool TryConvert(object? sender, out T? result) + { + return _converter.TryConvert(sender, out result); + } + + public OperatorSetBuilder TypeConverter(Action configureConverter) + { + _converter = new CustomTypeConverter(); + configureConverter(_converter); + return this; + } + + public OperatorSetBuilder Xor(TToken token, Func? precedesOperator = null, Func? evaluate = null) + { + return SetupBinary(Operators.Xor, token, evaluate ?? Xor, precedesOperator); + } + + private static IEvaluable CreateBinaryChild(IEvaluable active, Operators op, Func precedesOperator, Func evaluate) + { + BinaryScope self; + + //If we could steal an arg... + if (!active.IsIndivisible) + { + if (active is BinaryScope left && precedesOperator(left.Operator)) + { + self = new BinaryScope(active, op, evaluate) + { + Left = left.Right + }; + left.Right!.Parent = self; + left.Right = self; + return self; + } + } + + //We couldn't steal an arg, "active" is now our left, inject ourselves into + // active's parent in its place + self = new BinaryScope(active.Parent, op, evaluate); + + if (active.Parent != null) + { + switch (active.Parent) + { + case UnaryScope unary: + unary.Parent = self; + break; + case BinaryScope binary: + if (binary.Left == active) + { + binary.Left = self; + } + else if (binary.Right == active) + { + binary.Right = self; + } + break; + } + } + + active.Parent = self; + self.Left = active; + return self; + } + + private static IEvaluable CreateUnaryChild(IEvaluable active, Operators op, Func evaluate) + { + UnaryScope self = new UnaryScope(active, op, evaluate); + active.TryAccept(self); + return self; + } + + private static object EqualTo(object? left, object? right) + { + string? l = left as string; + string? r = right as string; + + if (l != null && r != null) + { + return string.Equals(l, r, StringComparison.OrdinalIgnoreCase); + } + + return Equals(l, r); + } + + private static object GreaterThan(object? left, object? right) + { + return ((IComparable)left!).CompareTo(right) > 0; + } + + private static object GreaterThanOrEqualTo(object? left, object? right) + { + return ((IComparable)left!).CompareTo(right) >= 0; + } + + private static object LessThan(object? left, object? right) + { + return ((IComparable)left!).CompareTo(right) < 0; + } + + private static object LessThanOrEqualTo(object? left, object? right) + { + return ((IComparable)left!).CompareTo(right) <= 0; + } + + private static string Passthrough(string arg) + { + return arg; + } + + private static bool Precedes(Operators check, Operators arg) + { + return check < arg; + } + + private object Add(object? left, object? right) + { + if (_converter.TryConvert(left, out long longLeft)) + { + if (_converter.TryConvert(right, out long longRight)) + { + return longLeft + longRight; + } + + if (_converter.TryConvert(right, out double doubleRight)) + { + return longLeft + doubleRight; + } + } + else + { + if (_converter.TryConvert(left, out double doubleLeft)) + { + if (_converter.TryConvert(right, out long longRight)) + { + return doubleLeft + longRight; + } + + if (_converter.TryConvert(right, out double doubleRight)) + { + return doubleLeft + doubleRight; + } + } + } + + return string.Concat(left, right); + } + + private object And(object? left, object? right) + { + if (_converter.TryConvert(left, out bool boolLeft) && _converter.TryConvert(right, out bool boolRight)) + { + return boolLeft && boolRight; + } + + throw new Exception($"Unable to logical and {left?.GetType()} and {right?.GetType()}"); + } + + private object BitwiseAnd(object? left, object? right) + { + if (_converter.TryConvert(left, out long longLeft) && _converter.TryConvert(right, out long longRight)) + { + return longLeft & longRight; + } + + throw new Exception($"Unable to bitwise and {left?.GetType()} and {right?.GetType()}"); + } + + private object BitwiseOr(object? left, object? right) + { + if (_converter.TryConvert(left, out long longLeft) && _converter.TryConvert(right, out long longRight)) + { + return longLeft | longRight; + } + + throw new Exception($"Unable to bitwise or {left?.GetType()} and {right?.GetType()}"); + } + + private object Divide(object? left, object? right) + { + long longRight; + int doubleRight; + + if (_converter.TryConvert(left, out long longLeft)) + { + if (_converter.TryConvert(right, out longRight)) + { + return longLeft / longRight; + } + + if (_converter.TryConvert(right, out doubleRight)) + { + return longLeft / doubleRight; + } + } + else if (_converter.TryConvert(left, out int doubleLeft)) + { + if (_converter.TryConvert(right, out longRight)) + { + return doubleLeft / longRight; + } + + if (_converter.TryConvert(right, out doubleRight)) + { + return doubleLeft / doubleRight; + } + } + + throw new Exception($"Cannot divide {left?.GetType()} and {right?.GetType()}"); + } + + private object Exponentiate(object? left, object? right) + { + long longRight; + int intRight; + + if (_converter.TryConvert(left, out long longLeft)) + { + if (_converter.TryConvert(right, out longRight)) + { + return Math.Pow(longLeft, longRight); + } + + if (_converter.TryConvert(right, out intRight)) + { + return Math.Pow(longLeft, intRight); + } + } + else if (_converter.TryConvert(left, out int intLeft)) + { + if (_converter.TryConvert(right, out longRight)) + { + return Math.Pow(intLeft, longRight); + } + + if (_converter.TryConvert(right, out intRight)) + { + return Math.Pow(intLeft, intRight); + } + } + + throw new Exception($"Cannot exponentiate {left?.GetType()} and {right?.GetType()}"); + } + + private object LeftShift(object? left, object? right) + { + if (_converter.TryConvert(left, out long longLeft) && _converter.TryConvert(right, out int intRight)) + { + return longLeft << intRight; + } + + throw new Exception($"Unable to left shift {left?.GetType()} and {right?.GetType()}"); + } + + private object Multiply(object? left, object? right) + { + long longRight; + int doubleRight; + + if (_converter.TryConvert(left, out long longLeft)) + { + if (_converter.TryConvert(right, out longRight)) + { + return longLeft * longRight; + } + + if (_converter.TryConvert(right, out doubleRight)) + { + return longLeft * doubleRight; + } + } + else if (_converter.TryConvert(left, out int doubleLeft)) + { + if (_converter.TryConvert(right, out longRight)) + { + return doubleLeft * longRight; + } + + if (_converter.TryConvert(right, out doubleRight)) + { + return doubleLeft * doubleRight; + } + } + + throw new Exception($"Cannot multiply {left?.GetType()} and {right?.GetType()}"); + } + + private object Not(object? operand) + { + if (_converter.TryConvert(operand, out bool l)) + { + return !l; + } + + throw new Exception($"Unable to logical not {operand?.GetType()}"); + } + + private object NotEqualTo(object? left, object? right) + { + if (left is string l && right is string r) + { + return !string.Equals(l, r, StringComparison.OrdinalIgnoreCase); + } + + return Not(EqualTo(left, right)); + } + + private object Or(object? left, object? right) + { + if (_converter.TryConvert(left, out bool l) && _converter.TryConvert(right, out bool r)) + { + return l || r; + } + + throw new Exception($"Unable to logical or {left?.GetType()} and {right?.GetType()}"); + } + + private object RightShift(object? left, object? right) + { + if (_converter.TryConvert(left, out long longLeft) && _converter.TryConvert(right, out int intRight)) + { + return longLeft >> intRight; + } + + throw new Exception($"Unable to right shift {left?.GetType()} and {right?.GetType()}"); + } + + private OperatorSetBuilder SetupBinary(Operators op, TToken token, Func evaluate, Func? precedesOperator = null) + { + _operatorScopeLookupFactory[op] = + x => CreateBinaryChild(x, op, precedesOperator ?? (a => Precedes(op, a)), evaluate ?? Add); + _tokensToOperatorsMap[token] = op; + return this; + } + + private object Subtract(object? left, object? right) + { + long longRight; + int doubleRight; + + if (_converter.TryConvert(left, out long longLeft)) + { + if (_converter.TryConvert(right, out longRight)) + { + return longLeft - longRight; + } + + if (_converter.TryConvert(right, out doubleRight)) + { + return longLeft - doubleRight; + } + } + else if (_converter.TryConvert(left, out int doubleLeft)) + { + if (_converter.TryConvert(right, out longRight)) + { + return doubleLeft - longRight; + } + + if (_converter.TryConvert(right, out doubleRight)) + { + return doubleLeft - doubleRight; + } + } + + throw new Exception($"Cannot subtract {left?.GetType()} and {right?.GetType()}"); + } + + private object Xor(object? left, object? right) + { + if (_converter.TryConvert(left, out bool l) && _converter.TryConvert(right, out bool r)) + { + return l ^ r; + } + + long longRight; + int doubleRight; + + if (_converter.TryConvert(left, out long longLeft)) + { + if (_converter.TryConvert(right, out longRight)) + { + return longLeft ^ longRight; + } + + if (_converter.TryConvert(right, out doubleRight)) + { + return longLeft ^ doubleRight; + } + } + else if (_converter.TryConvert(left, out int doubleLeft)) + { + if (_converter.TryConvert(right, out longRight)) + { + return doubleLeft ^ longRight; + } + + if (_converter.TryConvert(right, out doubleRight)) + { + return doubleLeft ^ doubleRight; + } + } + + throw new Exception($"Can't xor {left?.GetType()} and {right?.GetType()}"); + } + + public class CustomTypeConverter : ITypeConverter + { + public Type ScopeType => typeof(TScope); + + public ITypeConverter Register(TypeConverterDelegate converter) + { + TypeConverterLookup.TryConvert = converter; + return this; + } + + public bool TryConvert(object? source, out T? result) + { + TypeConverterDelegate? converter = TypeConverterLookup.TryConvert; + + return converter != null + ? converter(source, out result) + : TryCoreConvert(source, out result); + } + + public bool TryCoreConvert(object? source, out T? result) + { + return Converter.TryConvert(source, out result); + } + + private static class TypeConverterLookup + { + public static TypeConverterDelegate? TryConvert { get; set; } + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Operators.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Operators.cs new file mode 100644 index 000000000000..bde0bc7a2a89 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Operators.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public enum Operators + { + None = 0, + And = 41, + Or = 42, + Xor = 39, + Not = 2, //Unary, gets precedence + GreaterThan = 33, + GreaterThanOrEqualTo = 35, + LessThan = 32, + LessThanOrEqualTo = 34, + EqualTo = 36, + NotEqualTo = 37, + BadSyntax = -1, + Identity = 1, //Equiv to "scope resolution" + LeftShift = 30, + RightShift = 31, + BitwiseAnd = 38, + BitwiseOr = 40, + Add = 28, + Subtract = 29, + Multiply = 25, + Divide = 26, + Modulus = 27, + Exponentiate = 24 + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/ScopeBuilder.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/ScopeBuilder.cs new file mode 100644 index 000000000000..eb712fbcc34e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/ScopeBuilder.cs @@ -0,0 +1,339 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Util; + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public class ScopeBuilder + where TOperator : struct + where TToken : struct + { + private readonly ISet _badSyntaxTokens; + private readonly TToken _closeGroup; + private readonly TOperator _identity; + private readonly bool _isSymbolDereferenceInLiteralSequenceRequired; + private readonly int _knownTokensCount; + private readonly TToken _literal; + private readonly ISet _literalSequenceBoundsMarkers; + private readonly ISet _noops; + private readonly TToken _openGroup; + private readonly IReadOnlyDictionary> _operatorScopeFactory; + private readonly IProcessorState _processor; + private readonly IReadOnlyList _symbolValues; + private readonly IReadOnlyList _symbolKeys; + private readonly ISet _terminators; + private readonly ITokenTrie _tokens; + private readonly IReadOnlyDictionary _tokenToOperatorMap; + private readonly Func _valueDecoder; + private readonly Func _valueEncoder; + + public ScopeBuilder(IProcessorState processor, ITokenTrie tokens, IOperatorMap operatorMap, bool dereferenceInLiterals) + { + TokenTrie trie = new TokenTrie(); + trie.Append(tokens); + + _badSyntaxTokens = operatorMap.BadSyntaxTokens; + _noops = operatorMap.NoOpTokens; + _openGroup = operatorMap.OpenGroupToken; + _closeGroup = operatorMap.CloseGroupToken; + _literal = operatorMap.LiteralToken; + _identity = operatorMap.Identity; + _literalSequenceBoundsMarkers = operatorMap.LiteralSequenceBoundsMarkers; + _terminators = operatorMap.Terminators; + _isSymbolDereferenceInLiteralSequenceRequired = dereferenceInLiterals; + _processor = processor; + _knownTokensCount = tokens.Count; + _valueEncoder = operatorMap.Encode; + _valueDecoder = operatorMap.Decode; + + List symbolValues = new List(); + List symbolKeys = new List(); + + foreach (KeyValuePair variable in processor.Config.Variables) + { + trie.AddToken(processor.Encoding.GetBytes(string.Format(processor.Config.VariableFormatString, variable.Key))); + symbolValues.Add(variable.Value); + symbolKeys.Add(variable.Key); + } + + _symbolValues = symbolValues; + _symbolKeys = symbolKeys; + _tokenToOperatorMap = operatorMap.TokensToOperatorsMap; + _operatorScopeFactory = operatorMap.OperatorScopeLookupFactory; + _tokens = trie; + } + + /// + /// Traverses the given buffer position and creates an evaluable expression. + /// If non-null bag for variable references is passed, it will be populated with references of variables used within the evaluable expression. + /// + /// + /// + /// + /// If passed (if not null) it will be populated with references to variables used within the inspected expression. + /// + public IEvaluable? Build(ref int bufferLength, ref int bufferPosition, Action> onFault, HashSet? referencedVariablesKeys = null) + { + Stack parents = new Stack(); + ScopeIsolator isolator = new ScopeIsolator + { + Root = new UnaryScope(null, _identity, o => o) + }; + isolator.Active = isolator.Root; + TToken? activeLiteralSequenceBoundsMarker = null; + List currentLiteral = new List(); + List allData = new List(); + + while (bufferLength > 0) + { + int targetLen = Math.Min(bufferLength, _tokens.MaxLength); + for (; bufferPosition < bufferLength - targetLen + 1;) + { + int oldBufferPos = bufferPosition; + if (_tokens.GetOperation(_processor.CurrentBuffer, bufferLength, ref bufferPosition, out int token)) + { + allData.AddRange(_tokens.Tokens[token].Value); + TToken mappedToken = (TToken)(object)token; + + if (_badSyntaxTokens.Contains(mappedToken)) + { + onFault(allData); + return null; + } + + //Hit a terminator? Return the root + if (_terminators.Contains(mappedToken)) + { + bufferPosition = oldBufferPos; + + //If we had an active literal, it has to be over now - all literal types have already been processed + if (currentLiteral.Count > 0) + { + string value = _processor.Encoding.GetString(currentLiteral.ToArray()); + value = _valueDecoder(value); + currentLiteral.Clear(); + Token t = new Token(_literal, value); + TokenScope scope = new TokenScope(isolator.Active, t); + + while (isolator.Active != null && !isolator.Active.TryAccept(scope)) + { + isolator.Active = isolator.Active.Parent; + } + + if (isolator.Active == null) + { + onFault(allData); + return null; + } + } + + return isolator.Root; + } + + //Start or end of a literal sequence (string)? + if (_literalSequenceBoundsMarkers.Contains(mappedToken)) + { + //Don't add the literal start/end, otherwise we'll have to deal with them + // in strings, guessing whether they're supposed to be there or not + + if (activeLiteralSequenceBoundsMarker.HasValue) + { + if (Equals(activeLiteralSequenceBoundsMarker.Value, mappedToken)) + { + activeLiteralSequenceBoundsMarker = null; + string value = _processor.Encoding.GetString(currentLiteral.ToArray()); + value = _valueDecoder(value); + currentLiteral.Clear(); + Token t = new Token(_literal, value); + TokenScope scope = new TokenScope(isolator.Active, t) + { + IsQuoted = true + }; + + while (isolator.Active != null && !isolator.Active.TryAccept(scope)) + { + isolator.Active = isolator.Active.Parent; + } + + if (isolator.Active == null) + { + onFault(allData); + return null; + } + } + } + else + { + activeLiteralSequenceBoundsMarker = mappedToken; + } + } + //In a literal sequence (string)? + else if (activeLiteralSequenceBoundsMarker.HasValue) + { + //Have a symbol & dereferencing in literal sequences is on? + if (_knownTokensCount <= token && _isSymbolDereferenceInLiteralSequenceRequired) + { + object val = _symbolValues[token - _knownTokensCount]; + referencedVariablesKeys?.Add(_symbolKeys[token - _knownTokensCount]); + string valText = (val ?? "null").ToString(); + valText = _valueEncoder(valText); + byte[] data = _processor.Encoding.GetBytes(valText); + currentLiteral.AddRange(data); + } + else + { + currentLiteral.AddRange(_tokens.Tokens[token].Value); + } + } + else + { + //If we had an active literal, it has to be over now - all literal types have already been processed + if (currentLiteral.Count > 0) + { + activeLiteralSequenceBoundsMarker = null; + string value = _processor.Encoding.GetString(currentLiteral.ToArray()); + value = _valueDecoder(value); + currentLiteral.Clear(); + Token t = new Token(_literal, value); + TokenScope scope = new TokenScope(isolator.Active, t); + + while (isolator.Active != null && !isolator.Active.TryAccept(scope)) + { + isolator.Active = isolator.Active.Parent; + } + + if (isolator.Active == null) + { + onFault(allData); + return null; + } + } + + //Start of a group? + if (Equals(_openGroup, mappedToken)) + { + parents.Push(isolator); + isolator = new ScopeIsolator + { + Root = new UnaryScope(null, _identity, o => o) + }; + isolator.Active = isolator.Root; + } + //End of a group? + else if (Equals(_closeGroup, mappedToken)) + { + ScopeIsolator tmp = parents.Pop(); + tmp.Active!.TryAccept(isolator.Root); + isolator.Root = tmp.Active; + isolator = tmp; + } + //Is it a variable? + else if (_knownTokensCount <= token) + { + object? value = _symbolValues[token - _knownTokensCount] ?? null; + referencedVariablesKeys?.Add(_symbolKeys[token - _knownTokensCount]); + Token t = new Token(_literal, value); + TokenScope scope = new TokenScope(isolator.Active, t); + + while (isolator.Active != null && !isolator.Active.TryAccept(scope)) + { + isolator.Active = isolator.Active.Parent; + } + + if (isolator.Active == null) + { + onFault(allData); + return null; + } + } + //Discardable tokens? + else if (_noops.Contains(mappedToken)) + { + } + //All the special possibilities have been exhausted, try to process operations + else + { + //We got a token we understand, but it's not an operator + if (_tokenToOperatorMap.TryGetValue(mappedToken, out TOperator op)) + { + if (_operatorScopeFactory.TryGetValue(op, out Func factory)) + { + IEvaluable oldActive = isolator.Active; + isolator.Active = factory(isolator.Active); + + if (oldActive.Parent == isolator.Active + && (oldActive == isolator.Root + || (isolator.Root is UnaryScope o + && Equals(o.Operator, _identity) + && o.Operand == oldActive))) + { + isolator.Root = isolator.Active; + } + } + } + } + } + } + //If we've encountered a literal after fully filling the tree, return + else if (isolator.Active.IsFull) + { + IEvaluable? parent = isolator.Active.Parent; + + while (parent != null && parent.IsFull) + { + parent = parent.Parent; + } + + if (parent == null) + { + return isolator.Root; + } + } + else + { + allData.Add(_processor.CurrentBuffer[bufferPosition]); + currentLiteral.Add(_processor.CurrentBuffer[bufferPosition]); + ++bufferPosition; + } + } + + _processor.AdvanceBuffer(bufferPosition); + bufferPosition = _processor.CurrentBufferPosition; + bufferLength = _processor.CurrentBufferLength; + } + + //If we had an active literal, it has to be over now - all literal types have already been processed + if (currentLiteral.Count > 0) + { + string value = _processor.Encoding.GetString(currentLiteral.ToArray()); + value = _valueDecoder(value); + currentLiteral.Clear(); + Token t = new Token(_literal, value); + TokenScope scope = new TokenScope(isolator.Active, t); + + while (isolator.Active != null && !isolator.Active.TryAccept(scope)) + { + isolator.Active = isolator.Active.Parent; + } + + if (isolator.Active == null) + { + onFault(allData); + return null; + } + } + + return isolator.Root; + } + + private class ScopeIsolator + { + public IEvaluable? Active { get; set; } + + public IEvaluable? Root { get; set; } + } + } +} + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/ScopeBuilderHelper.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/ScopeBuilderHelper.cs new file mode 100644 index 000000000000..401a77b58963 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/ScopeBuilderHelper.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public static class ScopeBuilderHelper + { + public static ScopeBuilder ScopeBuilder(this IProcessorState processor, ITokenTrie tokens, IOperatorMap operatorMap, bool dereferenceInLiterals = false) + where TOperator : struct + where TToken : struct + { + return new ScopeBuilder(processor, tokens, operatorMap, dereferenceInLiterals); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/CoreConverters.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/CoreConverters.cs new file mode 100644 index 000000000000..f9594947f737 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/CoreConverters.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; + +namespace Microsoft.TemplateEngine.Core.Expressions.Shared +{ + public static class CoreConverters + { + public static bool TryHexConvert(string prefix, ITypeConverter obj, object? source, out long result) + { + if (!obj.TryConvert(source, out string? ls)) + { + result = 0; + return false; + } + + if (ls!.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && long.TryParse(ls.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out result)) + { + return true; + } + + result = 0; + return false; + } + + public static bool TryHexConvert(string prefix, ITypeConverter obj, object? source, out int result) + { + if (!obj.TryConvert(source, out string? ls)) + { + result = 0; + return false; + } + + if (ls!.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && int.TryParse(ls.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out result)) + { + return true; + } + + result = 0; + return false; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/CppStyleConverters.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/CppStyleConverters.cs new file mode 100644 index 000000000000..05a3ca29eb6e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/CppStyleConverters.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions.Shared +{ + public static class CppStyleConverters + { + public static void ConfigureConverters(ITypeConverter obj) + { + obj.Register((object? o, out long r) => CoreConverters.TryHexConvert("0x", obj, o, out r) || obj.TryCoreConvert(o, out r)) + .Register((object? o, out int r) => CoreConverters.TryHexConvert("0x", obj, o, out r) || obj.TryCoreConvert(o, out r)); + } + + public static string Decode(string arg) + { + return arg.Replace("\\\"", "\"").Replace("\\'", "'"); + } + + public static string Encode(string arg) + { + return arg.Replace("\"", "\\\"").Replace("'", "\\'"); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/SharedEvaluatorDefinition.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/SharedEvaluatorDefinition.cs new file mode 100644 index 000000000000..c1c09a090c6f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/SharedEvaluatorDefinition.cs @@ -0,0 +1,294 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Core.Expressions.Shared +{ + public abstract class SharedEvaluatorDefinition + where TSelf : SharedEvaluatorDefinition, new() + where TTokens : struct + { + private static readonly TSelf Instance = new(); + private static readonly IOperatorMap Map = Instance.GenerateMap(); + private static readonly bool DereferenceInLiteralsSetting = Instance.DereferenceInLiterals; + private static readonly string NullToken = Instance.NullTokenValue; + private static readonly IOperationProvider[] NoOperationProviders = []; + + protected abstract string NullTokenValue { get; } + + protected abstract bool DereferenceInLiterals { get; } + + public static bool Evaluate(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, out bool faulted) + { + bool result = Evaluate(processor, ref bufferLength, ref currentBufferPosition, out string? faultedMessage, null, false); + faulted = !string.IsNullOrEmpty(faultedMessage); + return result; + } + + /// + /// Inspect the passed string, creates the expression, substitutes parameters within expression, evaluates substituted expression and returns result. + /// If non-null bag for variable references is passed, it will be populated with references of variables used within the evaluable expression. + /// + /// The logger to be used to log the messages during evaluation. + /// The string to be inspected and turned into expression. + /// Variables to be substituted within the expression. + /// A boolean value indicating the result of the evaluation. + public static bool EvaluateFromString(ILogger logger, string text, IVariableCollection variables) + { + return EvaluateFromString(logger, text, variables, out string? _, null); + } + + /// + /// Inspect the passed string, creates the expression, substitutes parameters within expression, evaluates substituted expression and returns result. + /// If non-null bag for variable references is passed, it will be populated with references of variables used within the evaluable expression. + /// + /// The logger to be used to log the messages during evaluation. + /// The string to be inspected and turned into expression. + /// Variables to be substituted within the expression. + /// Error message detailing failing evaluation, should it fail. + /// If passed (if not null) it will be populated with references to variables used within the inspected expression. + /// A boolean value indicating the result of the evaluation. + public static bool EvaluateFromString(ILogger logger, string text, IVariableCollection variables, out string? faultedMessage, HashSet? referencedVariablesKeys = null) + { + using MemoryStream ms = new(Encoding.UTF8.GetBytes(text)); + using MemoryStream res = new(); + EngineConfig cfg = new(logger, variables); + IProcessorState state = new ProcessorState(ms, res, (int)ms.Length, (int)ms.Length, cfg, NoOperationProviders); + int len = (int)ms.Length; + int pos = 0; + return Evaluate(state, ref len, ref pos, out faultedMessage, referencedVariablesKeys, true); + } + + /// + /// Creates the evaluable expression based on passed string, + /// collects used symbols in the expression and reports if any errors occurs on expression creation. + /// + /// The logger to be used to log the messages during building the evaluable expression. + /// The string to be inspected and turned into expression. + /// Variables to be substituted within the expression. + /// Error message detailing failing building evaluable expression. + /// If passed (if not null) it will be populated with references to variables used within the inspected expression. + /// Evaluable expression that represents decomposed . + public static IEvaluable? GetEvaluableExpression( + ILogger logger, + string text, + IVariableCollection variables, + out string? evaluableExpressionError, + HashSet referencedVariablesKeys) + { + using MemoryStream ms = new(Encoding.UTF8.GetBytes(text)); + using MemoryStream res = new(); + EngineConfig cfg = new(logger, variables); + IProcessorState state = new ProcessorState(ms, res, (int)ms.Length, (int)ms.Length, cfg, NoOperationProviders); + int len = (int)ms.Length; + int pos = 0; + + return GetEvaluableExpression(state, ref len, ref pos, out evaluableExpressionError, referencedVariablesKeys); + } + + protected static int Compare(object? left, object? right) + { + if (Equals(right, NullToken)) + { + right = null; + } + + if (Equals(left, NullToken)) + { + left = null; + } + + return AttemptNumericComparison(left, right) + ?? AttemptBooleanComparison(left, right) + ?? AttemptVersionComparison(left, right) + ?? AttemptMultiValueComparison(left, right) + ?? AttemptLexicographicComparison(left, right) + ?? AttemptComparableComparison(left, right) + ?? 0; + } + + protected abstract IOperatorMap GenerateMap(); + + protected abstract ITokenTrie GetSymbols(IProcessorState processor); + + private static IEvaluable? GetEvaluableExpression( + IProcessorState processor, + ref int bufferLength, + ref int currentBufferPosition, + out string? faultedMessage, + HashSet referencedVariablesKeys) + { + faultedMessage = null; + ITokenTrie tokens = Instance.GetSymbols(processor); + ScopeBuilder builder = processor.ScopeBuilder(tokens, Map, DereferenceInLiteralsSetting); + string? faultedSection = null; + + return builder.Build( + ref bufferLength, + ref currentBufferPosition, + x => faultedSection = processor.Encoding.GetString(x.ToArray()), + referencedVariablesKeys); + } + + private static bool Evaluate( + IProcessorState processor, + ref int bufferLength, + ref int currentBufferPosition, + out string? faultedMessage, + HashSet? referencedVariablesKeys, + // indicates whether passed buffer within processor contains only the analyzed expression, + // or it can possibly contain other content (e.g. the full template) + bool shouldProcessWholeBuffer) + { + string? faultedSection = null; + + IEvaluable? expression = GetEvaluableExpression( + processor, + ref bufferLength, + ref currentBufferPosition, + out faultedMessage, + referencedVariablesKeys ?? new HashSet()); + + bool result; + if (faultedSection != null) + { + faultedMessage = faultedSection; + result = false; + } + else + { + // Buffer continues after expression - let's populate error only if this is single expression evaluation + // as we want to avoid creation of message that would contain whole template content after some condition + if (shouldProcessWholeBuffer && bufferLength != 0) + { + faultedMessage = LocalizableStrings.Error_Evaluation_Expression_Substring + + processor.Encoding.GetString( + processor.CurrentBuffer, + currentBufferPosition, + bufferLength - currentBufferPosition); + } + + try + { + object? evalResult = expression?.Evaluate(); + result = (bool)Convert.ChangeType(evalResult, typeof(bool)); + } + catch (Exception e) + { + faultedMessage = faultedMessage == null + ? e.Message + : (faultedMessage + Environment.NewLine + e.Message); + result = false; + } + } + + if (!string.IsNullOrEmpty(faultedMessage)) + { + processor.Config.Logger.LogDebug(LocalizableStrings.Error_Evaluation_Expression + faultedMessage); + } + return result; + } + + private static int? AttemptBooleanComparison(object? left, object? right) + { + bool leftIsBool = Map.TryConvert(left!, out bool lb); + bool rightIsBool = Map.TryConvert(right!, out bool rb); + + if (!leftIsBool || !rightIsBool) + { + return null; + } + + return lb.CompareTo(rb); + } + + private static int? AttemptComparableComparison(object? left, object? right) + { + if (left is not IComparable ls || right is not IComparable rs) + { + return null; + } + + return ls.CompareTo(rs); + } + + 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? AttemptLexicographicComparison(object? left, object? right) + { + if (left is not string ls || right is not string rs) + { + return null; + } + + return string.Compare(ls, rs, StringComparison.OrdinalIgnoreCase); + } + + private static int? AttemptNumericComparison(object? left, object? right) + { + bool leftIsDouble = Map.TryConvert(left!, out double ld); + bool rightIsDouble = Map.TryConvert(right!, out double rd); + + if (!leftIsDouble) + { + if (!Map.TryConvert(left!, out long ll)) + { + return null; + } + + ld = ll; + } + + if (!rightIsDouble) + { + if (!Map.TryConvert(right!, out long rl)) + { + return null; + } + + rd = rl; + } + + return ld.CompareTo(rd); + } + + private static int? AttemptVersionComparison(object? left, object? right) + { + Version? lv = left as Version; + + if (lv == null) + { + if (left is not string ls || !Version.TryParse(ls, out lv)) + { + return null; + } + } + + Version? rv = right as Version; + + if (rv == null) + { + if (right is not string rs || !Version.TryParse(rs, out rv)) + { + return null; + } + } + + return lv.CompareTo(rv); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/VisualBasicStyleConverters.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/VisualBasicStyleConverters.cs new file mode 100644 index 000000000000..c9f4264e2f36 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/VisualBasicStyleConverters.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions.Shared +{ + public static class VisualBasicStyleConverters + { + public static void ConfigureConverters(ITypeConverter obj) + { + obj.Register((object? o, out long r) => CoreConverters.TryHexConvert("&H", obj, o, out r) || obj.TryCoreConvert(o, out r)) + .Register((object? o, out int r) => CoreConverters.TryHexConvert("&H", obj, o, out r) || obj.TryCoreConvert(o, out r)); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/XmlStyleConverters.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/XmlStyleConverters.cs new file mode 100644 index 000000000000..eb6d7a60901e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Shared/XmlStyleConverters.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; + +namespace Microsoft.TemplateEngine.Core.Expressions.Shared +{ + //TODO: When the ability to have more descriptive returns is available, update this with + // bounds checking + public static class XmlStyleConverters + { + public static string XmlDecode(string arg) + { + List output = new List(); + + for (int i = 0; i < arg.Length; ++i) + { + //Not entity mode + if (arg[i] != '&') + { + output.Add(arg[i]); + continue; + } + + ++i; + //Entity mode, decimal or hex + if (arg[i] == '#') + { + ++i; + + //Hex entity mode + if (arg[i] == 'x') + { + string hex = arg.Substring(i + 1, 4); + char c = (char)short.Parse(hex.TrimStart('0'), NumberStyles.HexNumber); + output.Add(c); + i += 5; //x, 4 digits, semicolon (consumed by the loop bound) + } + else + { + string dec = arg.Substring(i, 4); + char c = (char)short.Parse(dec.TrimStart('0'), NumberStyles.Integer); + output.Add(c); + i += 4; //4 digits, semicolon (consumed by the loop bound) + } + } + else + { + switch (arg[i]) + { + case 'q': + switch (arg[i + 1]) + { + case 'u': + switch (arg[i + 2]) + { + case 'o': + switch (arg[i + 3]) + { + case 't': + switch (arg[i + 4]) + { + case ';': + output.Add('"'); + i += 4; + break; + } + break; + } + break; + } + break; + } + break; + case 'a': + switch (arg[i + 1]) + { + case 'm': + switch (arg[i + 2]) + { + case 'p': + switch (arg[i + 3]) + { + case ';': + output.Add('&'); + i += 3; + break; + } + break; + } + break; + case 'p': + switch (arg[i + 2]) + { + case 'o': + switch (arg[i + 3]) + { + case 's': + switch (arg[i + 4]) + { + case ';': + output.Add('\''); + i += 4; + break; + } + break; + } + break; + } + break; + } + break; + case 'l': + switch (arg[i + 1]) + { + case 't': + switch (arg[i + 2]) + { + case ';': + output.Add('<'); + i += 2; + break; + } + break; + } + break; + case 'g': + switch (arg[i + 1]) + { + case 't': + switch (arg[i + 2]) + { + case ';': + output.Add('>'); + i += 2; + break; + } + break; + } + break; + } + } + } + + string s = new string(output.ToArray()); + return s; + } + + public static string XmlEncode(string arg) + { + return arg.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """).Replace("'", "'"); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Token.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Token.cs new file mode 100644 index 000000000000..b5ca7c25011b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/Token.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public class Token + { + public Token(TToken family, object? value) + { + Family = family; + Value = value; + } + + public TToken Family { get; } + + public object? Value { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/TokenScope.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/TokenScope.cs new file mode 100644 index 000000000000..a78232573fa8 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/TokenScope.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public class TokenScope : IEvaluable + { + public TokenScope(IEvaluable parent, Token token) + { + Parent = parent; + Token = token; + } + + public bool IsFull => true; + + public bool IsIndivisible => true; + + public bool IsQuoted { get; set; } + + public IEvaluable? Parent { get; set; } + + public Token Token { get; } + + public object? Evaluate() + { + return Token.Value; + } + + public override string ToString() + { + return $@"""{Token.Value}"""; + } + + public bool TryAccept(IEvaluable? child) => false; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/TypeConverterDelegate.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/TypeConverterDelegate.cs new file mode 100644 index 000000000000..c945414ba0f5 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/TypeConverterDelegate.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public delegate bool TypeConverterDelegate(object? source, out T result); +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/UnaryScope.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/UnaryScope.cs new file mode 100644 index 000000000000..af928f505a15 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/UnaryScope.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Expressions +{ + public class UnaryScope : IEvaluable + { + private readonly Func _evaluate; + + public UnaryScope(IEvaluable? parent, TOperator @operator, Func evaluate) + { + Parent = parent; + Operator = @operator; + _evaluate = evaluate; + } + + public bool IsFull => Operand != null; + + public bool IsIndivisible => true; + + public IEvaluable? Operand { get; set; } + + public TOperator Operator { get; } + + public IEvaluable? Parent { get; set; } + + public object? Evaluate() + { + object? operand = Operand?.Evaluate(); + return _evaluate(operand); + } + + public override string ToString() + { + return $@"{Operator}({Operand})"; + } + + public bool TryAccept(IEvaluable? child) + { + if (Operand == null) + { + Operand = child; + return true; + } + + return false; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/VisualBasic/VisualBasicStyleEvaluatorDefintion.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/VisualBasic/VisualBasicStyleEvaluatorDefintion.cs new file mode 100644 index 000000000000..b70336bc083f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Expressions/VisualBasic/VisualBasicStyleEvaluatorDefintion.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Expressions.Shared; +using Microsoft.TemplateEngine.Core.Util; + +namespace Microsoft.TemplateEngine.Core.Expressions.VisualBasic +{ + public class VisualBasicStyleEvaluatorDefintion : SharedEvaluatorDefinition + { + private static readonly Dictionary TokenCache = new Dictionary(); + + public enum Tokens + { + And = 0, + AndAlso = 1, + Or = 2, + OrElse = 3, + Not = 4, + GreaterThan = 5, + GreaterThanOrEqualTo = 6, + LessThan = 7, + LessThanOrEqualTo = 8, + EqualTo = 9, + NotEqualTo = 10, + Xor = 11, + OpenBrace = 12, + CloseBrace = 13, + Space = 14, + Tab = 15, + WindowsEOL = 16, + UnixEOL = 17, + LegacyMacEOL = 18, + Quote = 19, + LeftShift = 20, + RightShift = 21, + Add = 22, + Subtract = 23, + Multiply = 24, + Divide = 25, + Exponentiate = 26, + DoubleQuote = 27, + Literal = 28, + } + + protected override bool DereferenceInLiterals => false; + + protected override string NullTokenValue => "Nothing"; + + protected override IOperatorMap GenerateMap() => new OperatorSetBuilder(CppStyleConverters.Encode, CppStyleConverters.Decode) + .And(Tokens.And) + .And(Tokens.AndAlso) + .Or(Tokens.Or) + .Or(Tokens.OrElse) + .Not(Tokens.Not) + .Xor(Tokens.Xor) + .GreaterThan(Tokens.GreaterThan, evaluate: (x, y) => Compare(x, y) > 0) + .GreaterThanOrEqualTo(Tokens.GreaterThanOrEqualTo, evaluate: (x, y) => Compare(x, y) >= 0) + .LessThan(Tokens.LessThan, evaluate: (x, y) => Compare(x, y) < 0) + .LessThanOrEqualTo(Tokens.LessThanOrEqualTo, evaluate: (x, y) => Compare(x, y) <= 0) + .EqualTo(Tokens.EqualTo, evaluate: (x, y) => Compare(x, y) == 0) + .NotEqualTo(Tokens.NotEqualTo, evaluate: (x, y) => Compare(x, y) != 0) + .Ignore(Tokens.Space, Tokens.Tab) + .LiteralBoundsMarkers(Tokens.Quote) + .OpenGroup(Tokens.OpenBrace) + .CloseGroup(Tokens.CloseBrace) + .TerminateWith(Tokens.WindowsEOL, Tokens.UnixEOL, Tokens.LegacyMacEOL) + .LeftShift(Tokens.LeftShift) + .RightShift(Tokens.RightShift) + .Add(Tokens.Add) + .Subtract(Tokens.Subtract) + .Multiply(Tokens.Multiply) + .Divide(Tokens.Divide) + .Exponentiate(Tokens.Exponentiate) + .Literal(Tokens.Literal) + .LiteralBoundsMarkers(Tokens.DoubleQuote) + .TypeConverter(VisualBasicStyleConverters.ConfigureConverters); + + protected override ITokenTrie GetSymbols(IProcessorState processor) + { + if (!TokenCache.TryGetValue(processor.Encoding, out ITokenTrie tokens)) + { + TokenTrie trie = new TokenTrie(); + + // Logic + trie.AddToken(processor.Encoding.GetBytes("And")); + trie.AddToken(processor.Encoding.GetBytes("AndAlso")); + trie.AddToken(processor.Encoding.GetBytes("Or")); + trie.AddToken(processor.Encoding.GetBytes("OrElse")); + trie.AddToken(processor.Encoding.GetBytes("Not")); + trie.AddToken(processor.Encoding.GetBytes(">")); + trie.AddToken(processor.Encoding.GetBytes(">=")); + trie.AddToken(processor.Encoding.GetBytes("<")); + trie.AddToken(processor.Encoding.GetBytes("<=")); + trie.AddToken(processor.Encoding.GetBytes("=")); + trie.AddToken(processor.Encoding.GetBytes("<>")); + trie.AddToken(processor.Encoding.GetBytes("Xor")); + + // Braces + trie.AddToken(processor.Encoding.GetBytes("(")); + trie.AddToken(processor.Encoding.GetBytes(")")); + + // Whitespace + trie.AddToken(processor.Encoding.GetBytes(" ")); + trie.AddToken(processor.Encoding.GetBytes("\t")); + + // EOLs + trie.AddToken(processor.Encoding.GetBytes("\r\n")); + trie.AddToken(processor.Encoding.GetBytes("\n")); + trie.AddToken(processor.Encoding.GetBytes("\r")); + + // quotes + trie.AddToken(processor.Encoding.GetBytes("'")); + + // Shifts + trie.AddToken(processor.Encoding.GetBytes("<<")); + trie.AddToken(processor.Encoding.GetBytes(">>")); + + // Math operators + trie.AddToken(processor.Encoding.GetBytes("+")); + trie.AddToken(processor.Encoding.GetBytes("-")); + trie.AddToken(processor.Encoding.GetBytes("*")); + trie.AddToken(processor.Encoding.GetBytes("/")); + trie.AddToken(processor.Encoding.GetBytes("^")); + + // quotes + trie.AddToken(processor.Encoding.GetBytes("\"")); + + TokenCache[processor.Encoding] = tokens = trie; + } + + return tokens; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/FileChange.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/FileChange.cs new file mode 100644 index 000000000000..1f5083d25bed --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/FileChange.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Core +{ + public class FileChange : IFileChange2 + { + public FileChange(string sourceRelativePath, string targetRelativePath, ChangeKind changeKind, byte[]? contents = null) + { + SourceRelativePath = sourceRelativePath; + TargetRelativePath = targetRelativePath; + ChangeKind = changeKind; + Contents = contents ?? []; + } + + public string SourceRelativePath { get; } + + public string TargetRelativePath { get; } + + public ChangeKind ChangeKind { get; } + + //this property is always empty now - never set. + public byte[] Contents { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/KeysChangedEventArgs.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/KeysChangedEventArgs.cs new file mode 100644 index 000000000000..7661d387ca13 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/KeysChangedEventArgs.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core +{ + public class KeysChangedEventArgs : EventArgs, IKeysChangedEventArgs + { + private static KeysChangedEventArgs? s_default; + + public static KeysChangedEventArgs Default => s_default ??= new KeysChangedEventArgs(); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/LocalizableStrings.resx b/src/TemplateEngine/Microsoft.TemplateEngine.Core/LocalizableStrings.resx new file mode 100644 index 000000000000..cfe0fd61dd26 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/LocalizableStrings.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Encountered following error when parsing and evaluating expression: + + + Unexpected substring after parsed expression: + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/OperationTerminal.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/OperationTerminal.cs new file mode 100644 index 000000000000..2c174ef6b8f0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/OperationTerminal.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Matching +{ + public class OperationTerminal : TerminalBase + { + public OperationTerminal(IOperation operation, int token, int tokenLength, int start = 0, int end = -1) + : base(tokenLength, start, end) + { + Operation = operation; + Token = token; + } + + /// + /// Operation to perform. The tokens that operation matches are part of itself. + /// + public IOperation Operation { get; } + + /// + /// This is not an actual token to match, but index of token defined in . + /// + public int Token { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TerminalBase.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TerminalBase.cs new file mode 100644 index 000000000000..7fca7a2749a3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TerminalBase.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Matching +{ + public abstract class TerminalBase + { + protected TerminalBase(int tokenLength, int start, int end) + { + Start = start; + End = end != -1 ? end : (tokenLength - 1); + Length = tokenLength; + } + + /// + /// Start position of the token. + /// + public int Start { get; protected set; } + + /// + /// End position of the token. + /// + public int End { get; protected set; } + + /// + /// Length of the token. + /// + public int Length { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TerminalLocation.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TerminalLocation.cs new file mode 100644 index 000000000000..a59cb5f53949 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TerminalLocation.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Matching +{ + public class TerminalLocation + where T : TerminalBase + { + public TerminalLocation(T terminal, int location) + { + Terminal = terminal; + Location = location; + } + + /// + /// Start position of the terminal. Relative location of matching token is defined in . + /// + public int Location { get; set; } + + public T Terminal { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/Trie.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/Trie.cs new file mode 100644 index 000000000000..948c036a4e1b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/Trie.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Matching +{ + public class Trie + where T : TerminalBase + { + public Trie() + { + NextNodes = new Dictionary>(); + } + + public Dictionary> NextNodes { get; } + + public int MaxRemainingLength { get; private set; } + + public void AddPath(byte[] path, T terminal) + { + if (path.Length > MaxRemainingLength) + { + MaxRemainingLength = path.Length; + } + + int remainingLength = path.Length - 1; + Dictionary>? current = NextNodes; + for (int i = 0; i < path.Length; ++i, --remainingLength) + { + if (!current.TryGetValue(path[i], out TrieNode next)) + { + current[path[i]] = next = new TrieNode(path[i]) + { + MaxRemainingLength = remainingLength + }; + } + else + { + if (next.MaxRemainingLength < remainingLength) + { + next.MaxRemainingLength = remainingLength; + } + } + + if (i == path.Length - 1) + { + next.Terminals ??= new List(); + + int sameMatcherIndex = next.Terminals.FindIndex(t => t.Start == terminal.Start && t.End == terminal.End); + + if (sameMatcherIndex > -1) + { + // this matching is identical to another terminal already added to the trie. Overwrite it. + next.Terminals[sameMatcherIndex] = terminal; + } + else + { + next.Terminals.Add(terminal); + } + } + + current = next.NextNodes; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TrieEvaluationDriver.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TrieEvaluationDriver.cs new file mode 100644 index 000000000000..5bd97a146b06 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TrieEvaluationDriver.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Matching +{ + public class TrieEvaluationDriver + where T : TerminalBase + { + private readonly TrieEvaluator _evaluator; + private int _sequenceNumber; + + public TrieEvaluationDriver(TrieEvaluator trie) + { + _evaluator = trie; + } + + public TerminalLocation? Evaluate(byte[] buffer, int bufferLength, bool isFinalBuffer, int lastNetBufferEffect, ref int bufferPosition) + { + _sequenceNumber += lastNetBufferEffect; + int sequenceNumberToBufferPositionRelationship = _sequenceNumber - bufferPosition; + int originalSequenceNumber = _sequenceNumber; + + if (lastNetBufferEffect != 0 || !_evaluator.TryGetNext(isFinalBuffer && bufferPosition >= bufferLength, ref _sequenceNumber, out TerminalLocation? terminal)) + { + while (!_evaluator.Accept(buffer[bufferPosition], ref _sequenceNumber, out terminal)) + { + ++_sequenceNumber; + ++bufferPosition; + + if (bufferPosition >= bufferLength) + { + if (!isFinalBuffer) + { + break; + } + else + { + _evaluator.FinalizeMatchesInProgress(ref _sequenceNumber, out terminal); + break; + } + } + originalSequenceNumber = _sequenceNumber; + } + } + + if (terminal != null) + { + terminal.Location -= sequenceNumberToBufferPositionRelationship; + + if (originalSequenceNumber > _sequenceNumber + 1) + { + int expectedShift = terminal.Terminal.Length - terminal.Terminal.End - 1; + + if (expectedShift == 0) + { + int actualShift = originalSequenceNumber - _sequenceNumber - 1; + bufferPosition -= actualShift; + } + } + } + + return terminal; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TrieEvaluator.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TrieEvaluator.cs new file mode 100644 index 000000000000..a0b7469e17d3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TrieEvaluator.cs @@ -0,0 +1,234 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Matching +{ + public class TrieEvaluator + where T : TerminalBase + { + private readonly List> _activePaths; + private readonly Trie _trie; + private int _expectedSequenceNumber; + private int _lastReturnedTerminalEndSequenceNumber; + private int _lastSeenSequenceNumber; + private bool _waitForSequenceNumberCatchUp; + + public TrieEvaluator(Trie trie) + { + _trie = trie; + _activePaths = new List>(); + MaxLength = trie.MaxRemainingLength; + } + + public int MaxLength { get; } + + public int OldestRequiredSequenceNumber => _activePaths.Count == 0 ? _expectedSequenceNumber : _activePaths[0].StartSequenceNumber; + + //TODO: Figure out how to do readahead detection better - tracking the last seen sequence number + // is too slow + public bool Accept(byte data, ref int sequenceNumber, out TerminalLocation? terminal) + { + //If we're getting data from closer to the end of the stream instead + // of the next byte we're expecting, things that start before the new + // sequence number can be discarded (as they won't have read what they + // think they've read + if (sequenceNumber > _expectedSequenceNumber) + { + for (int i = 0; i < _activePaths.Count; ++i) + { + if (_activePaths[i].StartSequenceNumber < sequenceNumber) + { + _activePaths.RemoveAt(i--); + } + } + } + //The sequence number could also have lessened (an early terminal in + // a path has finally been deemed OK to return, the buffer position + // returns to the position after the matched token). If the buffer + // position advances into data where a match has already been read + // we need to remove those paths. For paths that start after the + // new sequence number, they can simply wait to be processed again + // until the sequence number catches up to the expected sequence + // number + else if (sequenceNumber < _expectedSequenceNumber) + { + if (!_waitForSequenceNumberCatchUp || sequenceNumber < _lastSeenSequenceNumber) + { + //Remove entries that have been impacted by a readahead + if (_lastReturnedTerminalEndSequenceNumber != sequenceNumber) + { + for (int i = 0; i < _activePaths.Count; ++i) + { + if (_activePaths[i].StartSequenceNumber < sequenceNumber) + { + _activePaths.RemoveAt(i--); + } + } + } + + _waitForSequenceNumberCatchUp = true; + } + + //We may be walking through an overlapped match + // here, check to see if we've made it to the + // end of an already completed path + _lastSeenSequenceNumber = sequenceNumber; + return TryGetNext(false, ref sequenceNumber, out terminal); + } + + _lastSeenSequenceNumber = sequenceNumber; + + //If we've made it here (we didn't end up quitting early due to + // sequence numbers being too low), we can process matches. + // Bump up the next expected byte and make sure we don't think + // we're in waiting mode. + _expectedSequenceNumber = sequenceNumber + 1; + _waitForSequenceNumberCatchUp = false; + TrieNode next; + + //Process paths in progress + for (int i = 0; i < _activePaths.Count; ++i) + { + TriePath path = _activePaths[i]; + + //If the current path can advance... + if (path.CurrentNode != null) + { + //If we matched another byte, advance the current node in + // the path and log the encountered terminal (if applicable) + if (path.CurrentNode.NextNodes.TryGetValue(data, out next)) + { + path.CurrentNode = next; + + if (next.IsTerminal) + { + path.EncounteredTerminals.AddRange(next.Terminals); + } + } + //If we didn't match and no terminals were found in the + // path, remove the path from tracking + else if (path.EncounteredTerminals.Count == 0) + { + _activePaths.RemoveAt(i--); + } + //If we didn't match, but did find terminals, indicate + // that the path can no longer advance + else + { + path.CurrentNode = null; + } + } + } + + //Try to start a new path in the trie + if (_trie.NextNodes.TryGetValue(data, out next)) + { + TriePath path = new TriePath(sequenceNumber) + { + CurrentNode = next + }; + + if (next.IsTerminal) + { + path.EncounteredTerminals.AddRange(next.Terminals); + } + + _activePaths.Add(path); + } + + return TryGetNext(false, ref sequenceNumber, out terminal); + } + + public void FinalizeMatchesInProgress(ref int sequenceNumber, out TerminalLocation? terminals) + { + TryGetNext(true, ref sequenceNumber, out terminals); + } + + public bool TryGetNext(bool isFinal, ref int sequenceNumber, out TerminalLocation? terminalLocation) + { + //See if there's anything we can return yet using the following + // conditions + // 1) There must be at least one active path + // 2) The 0th path must have terminated + // 3) Any terminal being returned must be the leftmost available + // 4) The same terminal should never be returned more than once + if (_activePaths.Count > 0) + { + int endedAt = 0; + T? best = null; + int bestPath = -1; + int minNonTerminatedPathStart = int.MaxValue; + int minTerminalStart = int.MaxValue; + + for (int i = 0; i < _activePaths.Count; ++i) + { + TriePath path = _activePaths[i]; + + if (path.CurrentNode != null && !isFinal) + { + if (path.StartSequenceNumber < minNonTerminatedPathStart) + { + minNonTerminatedPathStart = path.StartSequenceNumber; + + if (best != null && minNonTerminatedPathStart < minTerminalStart) + { + best = null; + } + } + } + else + { + int ssn = path.StartSequenceNumber; + for (int j = 0; j < path.EncounteredTerminals.Count; ++j) + { + T terminal = path.EncounteredTerminals[j]; + int start = terminal.Start + ssn; + + if (start >= _lastReturnedTerminalEndSequenceNumber) + { + if (start < minNonTerminatedPathStart && (best == null || start < minTerminalStart || (start == minTerminalStart && terminal.End > best.End))) + { + bestPath = i; + best = terminal; + minTerminalStart = start; + endedAt = terminal.End + ssn; + } + } + else + { + path.EncounteredTerminals.RemoveAt(j--); + } + } + + if (path.EncounteredTerminals.Count == 0) + { + _activePaths.RemoveAt(i--); + } + } + } + + if (best != null) + { + terminalLocation = new TerminalLocation(best, minTerminalStart); + _lastReturnedTerminalEndSequenceNumber = endedAt + 1; + sequenceNumber = endedAt; + + if (bestPath > -1 && bestPath < _activePaths.Count && _activePaths[bestPath].EncounteredTerminals.Contains(best)) + { + _activePaths[bestPath].EncounteredTerminals.Remove(best); + + if (_activePaths[bestPath].EncounteredTerminals.Count == 0) + { + _activePaths.RemoveAt(bestPath); + } + } + + return true; + } + } + + terminalLocation = null; + return false; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TrieNode.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TrieNode.cs new file mode 100644 index 000000000000..9e8a1d00b1a8 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TrieNode.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Matching +{ + public class TrieNode : Trie + where T : TerminalBase + { + public TrieNode(byte match) + { + Match = match; + } + + public byte Match { get; } + + public List? Terminals { get; set; } + + public bool IsTerminal => Terminals != null; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TriePath.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TriePath.cs new file mode 100644 index 000000000000..b0de9b89f3af --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Matching/TriePath.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Matching +{ + internal class TriePath + where T : TerminalBase + { + public TriePath(int startSequenceNumber) + { + StartSequenceNumber = startSequenceNumber; + EncounteredTerminals = new List(); + } + + public List EncounteredTerminals { get; } + + public int StartSequenceNumber { get; } + + public TrieNode? CurrentNode { get; set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Microsoft.TemplateEngine.Core.csproj b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Microsoft.TemplateEngine.Core.csproj new file mode 100644 index 000000000000..fc34c94724cc --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Microsoft.TemplateEngine.Core.csproj @@ -0,0 +1,36 @@ + + + + $(NetMinimum);$(NetCurrent);netstandard2.0;$(NetFrameworkMinimum) + Common operations for instantiating templates using forward-only input stream operations + true + true + + true + + + + + + annotations + + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/BalancedNesting.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/BalancedNesting.cs new file mode 100644 index 000000000000..f3aaa7d7c7fa --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/BalancedNesting.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + // TODO: Determine how to reset this if the stream is lead token heavy. + // Or better yet force balancing, in which case nothing needs to be done. + // but that requires a lookahead. + // + // If (when) more general use cases are found for this type of thing, this class will + // probably need to be reworked to deal with more generalized balancing + // and more generalized actions. + + public class BalancedNesting : IOperationProvider + { + public static readonly string OperationName = "balancednesting"; + + private readonly ITokenConfig _startToken; + private readonly ITokenConfig _realEndToken; + private readonly ITokenConfig _pseudoEndToken; + private readonly string? _resetFlag; + private readonly bool _initialState; + + public BalancedNesting(ITokenConfig startToken, ITokenConfig realEndToken, ITokenConfig pseudoEndToken, string? id, string? resetFlag, bool initialState) + { + _startToken = startToken; + _realEndToken = realEndToken; + _pseudoEndToken = pseudoEndToken; + Id = id; + _resetFlag = resetFlag; + _initialState = initialState; + } + + public string? Id { get; } + + public IOperation GetOperation(Encoding encoding, IProcessorState processorState) + { + IToken startToken = _startToken.ToToken(encoding); + IToken realEndToken = _realEndToken.ToToken(encoding); + IToken pseudoEndToken = _pseudoEndToken.ToToken(encoding); + + return new Implementation(startToken, realEndToken, pseudoEndToken, Id, _resetFlag, _initialState); + } + + private class Implementation : IOperation + { + // the order they're added to this.Tokens in the constructor must be the same as this! + private const int StartTokenIndex = 0; + + private const int RealEndTokenIndex = 1; + private const int PseudoEndTokenIndex = 2; + private readonly IToken _realEndToken; + private readonly IToken _pseudoEndToken; + private readonly string? _resetFlag; + private int _depth; + + public Implementation(IToken start, IToken realEnd, IToken pseudoEnd, string? id, string? resetFlag, bool initialState) + { + _realEndToken = realEnd; + _pseudoEndToken = pseudoEnd; + Id = id; + _resetFlag = resetFlag; + Tokens = new[] { start, _realEndToken, _pseudoEndToken }; + _depth = 0; + IsInitialStateOn = string.IsNullOrEmpty(id) || initialState; + } + + public string? Id { get; } + + public IReadOnlyList Tokens { get; } + + public bool IsInitialStateOn { get; } + + public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + // check if this operation has been reset. If so, set _depth = 0 + // this fixes the reset problem, but not the trailing unbalanced pseudo comment problem, i.e. it won't turn this: + // + if (processor.Config.Flags.TryGetValue(_resetFlag!, out bool resetFlagValue) && resetFlagValue) + { + processor.Config.Flags.Remove(_resetFlag!); + _depth = 0; + } + + if (token == StartTokenIndex) + { + ++_depth; + } + else if (token is RealEndTokenIndex or PseudoEndTokenIndex) + { + --_depth; + } + + if (_depth < 0) + { + // TODO: determine a better way to deal with this. + // The reset operation should limit the scope of the problem. + // The depth-zero pseudo-comment is the only one that will be "fixed". + // But this could be changed to also fix any where depth < 0. + processor.Config.Logger.LogInformation($"Balanced nesting depth < 0. CurrentBufferPosition = {currentBufferPosition}"); + } + + if (_depth == 0 && token == PseudoEndTokenIndex) + { + processor.WriteToTarget(_realEndToken.Value, _realEndToken.Start, _realEndToken.Length); + return _pseudoEndToken.Length; // the source buffer needs to skip over this token. + } + else + { + processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length); + } + + return 0; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ConditionEvaluator.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ConditionEvaluator.cs new file mode 100644 index 000000000000..ec0dca121608 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ConditionEvaluator.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public delegate bool ConditionEvaluator(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, out bool faulted); +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Conditional.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Conditional.cs new file mode 100644 index 000000000000..11f12db7f9f6 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Conditional.cs @@ -0,0 +1,460 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Util; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class Conditional : IOperationProvider + { + public static readonly string OperationName = "conditional"; + + // the unusual order of these is historical, no special meaning + // if actual_token_index % 10 == baseTokenIndex + // then actual_token_index is of the baseTokenIndex type + // these are now "Base indexes" + private const int IfTokenBaseIndex = 0; + + private const int EndTokenBaseIndex = 1; + private const int ElseIfTokenBaseIndex = 2; + private const int ElseTokenBaseIndex = 3; + private const int IfTokenActionableBaseIndex = 4; + private const int ElseIfTokenActionableBaseIndex = 5; + private const int ElseTokenActionableBaseIndex = 6; + + // must be > the highest token type index + private const int TokenTypeModulus = 10; + + private readonly bool _initialState; + + public Conditional(ConditionalTokens tokenVariants, bool wholeLine, bool trimWhitespace, ConditionEvaluator evaluator, string? id, bool initialState) + { + TrimWhitespace = trimWhitespace; + WholeLine = wholeLine; + Evaluator = evaluator; + Tokens = tokenVariants; + Id = id; + _initialState = initialState; + } + + public string? Id { get; } + + public bool WholeLine { get; } + + public bool TrimWhitespace { get; } + + public ConditionEvaluator Evaluator { get; } + + public ConditionalTokens Tokens { get; } + + /// + /// Returns the number of elements in the longest of the token variant lists. + /// + private int LongestTokenVariantListSize + { + get + { + int maxListSize = Math.Max(Tokens.IfTokens.Count, Tokens.ElseTokens.Count); + maxListSize = Math.Max(maxListSize, Tokens.ElseIfTokens.Count); + maxListSize = Math.Max(maxListSize, Tokens.EndIfTokens.Count); + maxListSize = Math.Max(maxListSize, Tokens.ActionableIfTokens.Count); + maxListSize = Math.Max(maxListSize, Tokens.ActionableElseTokens.Count); + maxListSize = Math.Max(maxListSize, Tokens.ActionableElseIfTokens.Count); + + return maxListSize; + } + } + + public IOperation GetOperation(Encoding encoding, IProcessorState processorState) + { + TokenTrie trie = new TokenTrie(); + + List tokens = new List(TokenTypeModulus * LongestTokenVariantListSize); + for (int i = 0; i < tokens.Capacity; i++) + { + tokens.Add(null!); + } + + AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.IfTokens, IfTokenBaseIndex, encoding); + AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.ElseTokens, ElseTokenBaseIndex, encoding); + AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.ElseIfTokens, ElseIfTokenBaseIndex, encoding); + AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.EndIfTokens, EndTokenBaseIndex, encoding); + AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.ActionableIfTokens, IfTokenActionableBaseIndex, encoding); + AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.ActionableElseTokens, ElseTokenActionableBaseIndex, encoding); + AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.ActionableElseIfTokens, ElseIfTokenActionableBaseIndex, encoding); + + return new Implementation(this, tokens, trie, Id, _initialState); + } + + /// + /// Returns true if the tokenIndex indicates the token is a variant of its base type, + /// false otherwise. + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsTokenIndexOfType(int tokenIndex, int baseTypeIndex) + { + return (tokenIndex % TokenTypeModulus) == baseTypeIndex; + } + + /// + /// Puts the tokensOfType into the tokenMasterList at indexes which are congruent to typeRemainder mod TokenTypeModulus. + /// + /// + /// + /// + /// + /// + private void AddTokensOfTypeToTokenListAndTrie(ITokenTrie trie, List tokenMasterList, IReadOnlyList tokensOfType, int typeRemainder, Encoding encoding) + { + int tokenIndex = typeRemainder; + + for (int i = 0; i < tokensOfType.Count; i++) + { + tokenMasterList[tokenIndex] = tokensOfType[i].ToToken(encoding); + trie.AddToken(tokenMasterList[tokenIndex], typeRemainder); + tokenIndex += TokenTypeModulus; + } + } + + private class Implementation : IOperation + { + private readonly Conditional _definition; + private readonly Stack _pendingCompletion = new Stack(); + private readonly ITokenTrie _trie; + private EvaluationState? _current; + + public Implementation(Conditional definition, IReadOnlyList tokens, ITokenTrie trie, string? id, bool initialState) + { + _trie = trie; + _definition = definition; + Tokens = tokens; + Id = id; + IsInitialStateOn = string.IsNullOrEmpty(id) || initialState; + } + + public string? Id { get; } + + public IReadOnlyList Tokens { get; } + + public bool IsInitialStateOn { get; } + + public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + if (processor.Config.Flags.TryGetValue(OperationName, out bool flag) && !flag) + { + processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length); + return Tokens[token].Length; + } + + // conditional has not started, or this is the "if" + if (_current != null || IsTokenIndexOfType(token, IfTokenBaseIndex) || IsTokenIndexOfType(token, IfTokenActionableBaseIndex)) + { + if (_definition.WholeLine) + { + processor.SeekTargetBackUntil(processor.EncodingConfig.LineEndings); + } + else if (_definition.TrimWhitespace) + { + processor.TrimWhitespace(false, true, ref bufferLength, ref currentBufferPosition); + } + } + + BEGIN: + //Got the "if" token... + if (IsTokenIndexOfType(token, IfTokenBaseIndex) || IsTokenIndexOfType(token, IfTokenActionableBaseIndex)) + { + if (_current == null) + { + _current = new EvaluationState(this); + } + else + { + _pendingCompletion.Push(_current); + _current = new EvaluationState(this); + } + + //If the "if" branch is taken, all else and elseif blocks will be omitted, return + // control to the processor so nested "if"s/mutations can be processed. Note that + // this block will not be terminated until the corresponding endif is found + if (_current.Evaluate(processor, ref bufferLength, ref currentBufferPosition)) + { + if (_definition.WholeLine) + { + processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: true); + } + + if (IsTokenIndexOfType(token, IfTokenActionableBaseIndex)) + { + // "Actionable" if token, so enable the flag operation(s) + _current.ToggleActionableOperations(true, processor); + } + + // if (true_condition) was found. + return 0; + } + else + { + // if (false_condition) was found. Skip to the next token of the if-elseif-elseif-...elseif-else-endif + SeekToNextTokenAtSameLevel(processor, ref bufferLength, ref currentBufferPosition, out token); + goto BEGIN; + } + } + + // If we've got an unbalanced statement, emit the token + if (_current == null) + { + processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length); + return Tokens[token].Length; + } + + //Got the endif token, exit to the parent "if" scope if it exists + if (IsTokenIndexOfType(token, EndTokenBaseIndex)) + { + if (_pendingCompletion.Count > 0) + { + _current = _pendingCompletion.Pop(); + _current.ToggleActionableOperations(_current.ActionableOperationsEnabled, processor); + } + else + { + // disable the special case operations (note: they may already be disabled, but cheaper to do than check) + _current.ToggleActionableOperations(false, processor); + _current = null; + } + + if (_definition.WholeLine) + { + processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: true); + } + else if (_definition.TrimWhitespace) + { + processor.TrimWhitespace(true, false, ref bufferLength, ref currentBufferPosition); + } + + return 0; + } + + if (_current.BranchTaken) + { + processor.SeekTargetBackUntil(processor.EncodingConfig.LineEndings, true); + //A previous branch was taken. Skip to the endif token. + // NOTE: this can probably use the new method SeekToNextTokenAtSameLevel() - they do almost the same thing. + SkipToMatchingEndif(processor, ref bufferLength, ref currentBufferPosition, ref token); + + if (_pendingCompletion.Count > 0) + { + _current = _pendingCompletion.Pop(); + _current.ToggleActionableOperations(_current.ActionableOperationsEnabled, processor); + } + else + { + // disable the special case operation (note: it may already be disabled, but cheaper to do than check) + _current.ToggleActionableOperations(false, processor); + _current = null; + } + + if (_definition.WholeLine) + { + processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: false); + } + else if (_definition.TrimWhitespace) + { + processor.TrimWhitespace(true, false, ref bufferLength, ref currentBufferPosition); + } + + return 0; + } + + //We have an "elseif" and haven't taken a previous branch + if (IsTokenIndexOfType(token, ElseIfTokenBaseIndex) || IsTokenIndexOfType(token, ElseIfTokenActionableBaseIndex)) + { + // 8-19 attempt to make the same as if() handling + // + if (_current.Evaluate(processor, ref bufferLength, ref currentBufferPosition)) + { + if (_definition.WholeLine) + { + processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: true); + } + + if (IsTokenIndexOfType(token, ElseIfTokenActionableBaseIndex)) + { + // the elseif branch is taken. + _current.ToggleActionableOperations(true, processor); + } + + return 0; + } + else + { + SeekToNextTokenAtSameLevel(processor, ref bufferLength, ref currentBufferPosition, out token); + + // In the original version this was conditional on SeekToToken() succeeding. + // Not sure if it should be conditional. It should never fail, unless the template is malformed. + goto BEGIN; + } + } + + //We have an "else" token and haven't taken any other branches, return control + // after setting that a branch has been taken + if (IsTokenIndexOfType(token, ElseTokenBaseIndex) || IsTokenIndexOfType(token, ElseTokenActionableBaseIndex)) + { + if (IsTokenIndexOfType(token, ElseTokenActionableBaseIndex)) + { + _current.ToggleActionableOperations(true, processor); + } + + _current.BranchTaken = true; + processor.WhitespaceHandler(ref bufferLength, ref currentBufferPosition, wholeLine: _definition.WholeLine, trim: _definition.TrimWhitespace); + return 0; + } + else + { + Debug.Assert(true, "Unknown token index: " + token); + return 0; // TODO: revisit. Not sure what's best here. + } + } + + // moves the buffer to the next token at the same level. + // Returns false if no end token can be found at the same level. + // this is probably indicative of a template authoring problem, or possibly a buffer problem. + private bool SkipToMatchingEndif(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, ref int token) + { + while (!IsTokenIndexOfType(token, EndTokenBaseIndex)) + { + bool seekSucceeded = SeekToNextTokenAtSameLevel(processor, ref bufferLength, ref currentBufferPosition, out token); + + if (!seekSucceeded) + { + return false; + } + } + + return true; + } + + // Moves the buffer to the next token at the same level of nesting as the current token. + // Should never be called if we're on an end token!!! + // Returns false if no next token can be found at the same level. + // this is probably indicative of a template authoring problem, or possibly a buffer problem. + private bool SeekToNextTokenAtSameLevel(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, out int token) + { + if (_definition.WholeLine) + { + processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: true); + } + + bool seekSucceeded = SeekToToken(processor, ref bufferLength, ref currentBufferPosition, out token); + + //Keep on scanning until we've hit a balancing token that belongs to us + // each "if" found opens a new level of nesting + while (IsTokenIndexOfType(token, IfTokenBaseIndex) || IsTokenIndexOfType(token, IfTokenActionableBaseIndex)) + { + int open = 1; + while (open != 0) + { + seekSucceeded &= SeekToToken(processor, ref bufferLength, ref currentBufferPosition, out token); + + if (IsTokenIndexOfType(token, IfTokenBaseIndex) || IsTokenIndexOfType(token, IfTokenActionableBaseIndex)) + { + ++open; + } + else if (IsTokenIndexOfType(token, EndTokenBaseIndex)) + { + --open; + } + } + + seekSucceeded &= SeekToToken(processor, ref bufferLength, ref currentBufferPosition, out token); + } + + // this may be irrelevant. If it happens, the template is malformed (i think) + return seekSucceeded; + } + + // moves to the next token + // returns false if the end of the buffer was reached without finding a token. + private bool SeekToToken(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, out int token) + { + bool bufferAdvanceFailed = false; + ITokenTrieEvaluator evaluator = _trie.CreateEvaluator(); + + while (true) + { + for (; currentBufferPosition < bufferLength; ++currentBufferPosition) + { + if (evaluator.Accept(processor.CurrentBuffer[currentBufferPosition], ref currentBufferPosition, out token)) + { + if (bufferAdvanceFailed || (currentBufferPosition != bufferLength)) + { + return true; + } + } + } + + if (bufferAdvanceFailed) + { + if (evaluator.TryFinalizeMatchesInProgress(ref currentBufferPosition, out token)) + { + return true; + } + + break; + } + + bufferAdvanceFailed = !processor.AdvanceBuffer(bufferLength - evaluator.BytesToKeepInBuffer); + currentBufferPosition = evaluator.BytesToKeepInBuffer; + bufferLength = processor.CurrentBufferLength; + } + + //If we run out of places to look, assert that the end of the buffer is the end + token = EndTokenBaseIndex; + currentBufferPosition = bufferLength; + return false; // no terminator found + } + + private class EvaluationState + { + private readonly Implementation _implementation; + private bool _branchTaken; + + public EvaluationState(Implementation implementation) + { + _implementation = implementation; + ActionableOperationsEnabled = false; + } + + public bool BranchTaken + { + get => _branchTaken; + set => _branchTaken |= value; + } + + public bool ActionableOperationsEnabled { get; private set; } + + public void ToggleActionableOperations(bool enabled, IProcessorState processor) + { + ActionableOperationsEnabled = enabled; + + foreach (string otherOptionDisableFlag in _implementation._definition.Tokens.ActionableOperations) + { + processor.Config.Flags[otherOptionDisableFlag] = enabled; + } + } + + internal bool Evaluate(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition) + { + BranchTaken = _implementation._definition.Evaluator(processor, ref bufferLength, ref currentBufferPosition, out bool faulted); + return BranchTaken; + } + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ConditionalTokens.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ConditionalTokens.cs new file mode 100644 index 000000000000..edb6f6a76bdb --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ConditionalTokens.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class ConditionalTokens + { + public ConditionalTokens() + { + IfTokens = []; + ElseTokens = []; + ElseIfTokens = []; + EndIfTokens = []; + ActionableIfTokens = []; + ActionableElseTokens = []; + ActionableElseIfTokens = []; + ActionableOperations = []; + } + + public IReadOnlyList IfTokens { get; set; } + + public IReadOnlyList ElseTokens { get; set; } + + public IReadOnlyList ElseIfTokens { get; set; } + + public IReadOnlyList EndIfTokens { get; set; } + + public IReadOnlyList ActionableIfTokens { get; set; } + + public IReadOnlyList ActionableElseTokens { get; set; } + + public IReadOnlyList ActionableElseIfTokens { get; set; } + + public IReadOnlyList ActionableOperations { get; set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ExpandVariables.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ExpandVariables.cs new file mode 100644 index 000000000000..ac71b9434dfb --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ExpandVariables.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class ExpandVariables : IOperationProvider + { + public static readonly string OperationName = "expandvariables"; + + private readonly bool _initialState; + + public ExpandVariables(string? id, bool initialState) + { + Id = id; + _initialState = initialState; + } + + public string? Id { get; } + + public IOperation GetOperation(Encoding encoding, IProcessorState processor) + { + return new Implementation(processor, Id, _initialState); + } + + private class Implementation : IOperation + { + public Implementation(IProcessorState processor, string? id, bool initialState) + { + Tokens = processor.EncodingConfig.VariableKeys; + Id = id; + IsInitialStateOn = string.IsNullOrEmpty(id) || initialState; + } + + public IReadOnlyList Tokens { get; } + + public string? Id { get; } + + public bool IsInitialStateOn { get; } + + public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + if (processor.Config.Flags.TryGetValue("expandVariables", out bool flag) && !flag) + { + processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length); + return Tokens[token].Length; + } + + object result = processor.EncodingConfig[token]; + string output = result?.ToString() ?? "null"; + + byte[] outputBytes = processor.Encoding.GetBytes(output); + processor.WriteToTarget(outputBytes, 0, outputBytes.Length); + return outputBytes.Length; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Include.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Include.cs new file mode 100644 index 000000000000..c616a04dab8a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Include.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Util; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class Include : IOperationProvider + { + public static readonly string OperationName = "include"; + + private readonly bool _initialState; + + public Include(ITokenConfig startToken, ITokenConfig endToken, Func sourceStreamOpener, string? id, bool initialState) + { + SourceStreamOpener = sourceStreamOpener; + StartToken = startToken; + EndToken = endToken; + Id = id; + _initialState = initialState; + } + + public ITokenConfig EndToken { get; } + + public ITokenConfig StartToken { get; } + + public Func SourceStreamOpener { get; } + + public string? Id { get; } + + public IOperation GetOperation(Encoding encoding, IProcessorState processorState) + { + IToken tokenBytes = StartToken.ToToken(encoding); + IToken endTokenBytes = EndToken.ToToken(encoding); + TokenTrie endTokenMatcher = new TokenTrie(); + endTokenMatcher.AddToken(endTokenBytes); + return new Implementation(tokenBytes, endTokenMatcher, this, Id, _initialState); + } + + private class Implementation : IOperation + { + private readonly Include _source; + private readonly ITokenTrie _endTokenMatcher; + + public Implementation(IToken token, ITokenTrie endTokenMatcher, Include source, string? id, bool initialState) + { + Tokens = new[] { token }; + _source = source; + _endTokenMatcher = endTokenMatcher; + Id = id; + IsInitialStateOn = string.IsNullOrEmpty(id) || initialState; + } + + public IReadOnlyList Tokens { get; } + + public string? Id { get; } + + public bool IsInitialStateOn { get; } + + public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + if (processor.Config.Flags.TryGetValue(OperationName, out bool flag) && !flag) + { + processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length); + return Tokens[token].Length; + } + + List pathBytes = new List(); + while (!_endTokenMatcher.GetOperation(processor.CurrentBuffer, bufferLength, ref currentBufferPosition, out token)) + { + pathBytes.Add(processor.CurrentBuffer[currentBufferPosition++]); + if (bufferLength - currentBufferPosition < _endTokenMatcher.MinLength) + { + processor.AdvanceBuffer(currentBufferPosition); + bufferLength = processor.CurrentBufferLength; + currentBufferPosition = 0; + + if (bufferLength == 0) + { + break; + } + } + } + + byte[] pathBytesArray = pathBytes.ToArray(); + string sourceLocation = processor.Encoding.GetString(pathBytesArray).Trim(); + + const int pageSize = 65536; + //Start off with a 64K buffer, we'll keep adding chunks to this + byte[] composite = new byte[pageSize]; + int totalBytesRead = 0; + using (Stream? data = _source.SourceStreamOpener(sourceLocation)) + { + while (totalBytesRead < composite.Length) + { + int bytesRead = data!.Read(composite, totalBytesRead, composite.Length - totalBytesRead); + if (bytesRead == 0) + { + break; + } + totalBytesRead += bytesRead; + } + int bytesFromPage = totalBytesRead; + + //As long as we're reading whole pages, keep allocating more space ahead + while (bytesFromPage == pageSize) + { + byte[] newBuffer = new byte[composite.Length + pageSize]; + Buffer.BlockCopy(composite, 0, newBuffer, 0, composite.Length); + composite = newBuffer; + + bytesFromPage = 0; + while (totalBytesRead < composite.Length) + { + int bytesRead = data!.Read(composite, totalBytesRead, composite.Length - totalBytesRead); + if (bytesRead == 0) + { + break; + } + totalBytesRead += bytesRead; + bytesFromPage += bytesRead; + } + } + } + + int offset = 0; + int nBytesToWrite; + Encoding realEncoding = EncodingUtil.Detect(composite, totalBytesRead, out byte[] bom); + + if (!Equals(realEncoding, processor.Encoding)) + { + composite = Encoding.Convert(realEncoding, processor.Encoding, composite, bom.Length, totalBytesRead - bom.Length); + nBytesToWrite = composite.Length; + } + else + { + offset = bom.Length; + nBytesToWrite = totalBytesRead - bom.Length; + } + + processor.WriteToTarget(composite, offset, nBytesToWrite); + return nBytesToWrite; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/InlineMarkupConditional.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/InlineMarkupConditional.cs new file mode 100644 index 000000000000..754359de34c5 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/InlineMarkupConditional.cs @@ -0,0 +1,257 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Util; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class InlineMarkupConditional : IOperationProvider + { + private readonly bool _initialState; + + public InlineMarkupConditional(MarkupTokens tokens, bool wholeLine, bool trimWhitespace, ConditionEvaluator evaluator, string variableFormat, string? id, bool initialState) + { + Tokens = tokens; + Id = id; + Evaluator = evaluator; + WholeLine = wholeLine; + TrimWhitespace = trimWhitespace; + VariableFormat = variableFormat; + _initialState = initialState; + } + + public ConditionEvaluator Evaluator { get; } + + public string? Id { get; } + + public MarkupTokens Tokens { get; } + + public bool TrimWhitespace { get; } + + public string VariableFormat { get; } + + public bool WholeLine { get; } + + public IOperation GetOperation(Encoding encoding, IProcessorState processorState) + { + TokenTrie structureTrie = new TokenTrie(); + TokenTrie closeConditionTrie = new TokenTrie(); + TokenTrie scanBackTrie = new TokenTrie(); + + IToken openOpenElementTokenBytes = Tokens.OpenOpenElementToken.ToToken(processorState.Encoding); + scanBackTrie.AddToken(openOpenElementTokenBytes); + int openOpenElementToken = structureTrie.AddToken(openOpenElementTokenBytes); + int openCloseElementToken = structureTrie.AddToken(Tokens.OpenCloseElementToken.ToToken(processorState.Encoding)); + int closeCloseElementToken = structureTrie.AddToken(Tokens.CloseElementTagToken.ToToken(processorState.Encoding)); + int openCommentToken = structureTrie.AddToken(Tokens.OpenCommentToken.ToToken(processorState.Encoding)); + int closeCommentToken = structureTrie.AddToken(Tokens.CloseCommentToken.ToToken(processorState.Encoding)); + + int selfClosingElementEndToken = -1; + if (Tokens.SelfClosingElementEndToken != null) + { + selfClosingElementEndToken = structureTrie.AddToken(Tokens.SelfClosingElementEndToken.ToToken(processorState.Encoding)); + } + + closeConditionTrie.AddToken(Tokens.CloseConditionExpression.ToToken(processorState.Encoding)); + MarkupTokenMapping mapping = new MarkupTokenMapping( + openOpenElementToken, + openCloseElementToken, + closeCloseElementToken, + selfClosingElementEndToken, + openCommentToken, + closeCommentToken); + + IReadOnlyList start = new[] { Tokens.OpenConditionExpression.ToToken(processorState.Encoding) }; + return new Implementation(this, start, structureTrie, closeConditionTrie, scanBackTrie, mapping, Id, _initialState); + } + + private class Implementation : IOperation + { + private readonly ITokenTrie _closeConditionTrie; + private readonly InlineMarkupConditional _definition; + private readonly MarkupTokenMapping _mapping; + private readonly ITokenTrie _scanBackTrie; + private readonly ITokenTrie _structureTrie; + + internal Implementation(InlineMarkupConditional definition, IReadOnlyList tokens, ITokenTrie structureTrie, ITokenTrie closeConditionTrie, ITokenTrie scanBackTrie, MarkupTokenMapping mapping, string? id, bool initialState) + { + _definition = definition; + Id = id; + Tokens = tokens; + _mapping = mapping; + _structureTrie = structureTrie; + _scanBackTrie = scanBackTrie; + _closeConditionTrie = closeConditionTrie; + IsInitialStateOn = string.IsNullOrEmpty(id) || initialState; + } + + public string? Id { get; } + + public IReadOnlyList Tokens { get; } + + public bool IsInitialStateOn { get; } + + public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + if (processor.Config.Flags.TryGetValue(Conditional.OperationName, out bool flag) && !flag) + { + processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length); + return Tokens[token].Length; + } + + List conditionBytes = new List(); + ScanToCloseCondition(processor, conditionBytes, ref bufferLength, ref currentBufferPosition); + byte[] condition = conditionBytes.ToArray(); + EngineConfig adjustedConfig = new EngineConfig(processor.Config.Logger, processor.Config.Whitespaces, processor.Config.LineEndings, processor.Config.Variables, _definition.VariableFormat); + IProcessorState localState = new ProcessorState(new MemoryStream(condition), new MemoryStream(), conditionBytes.Count, int.MaxValue, adjustedConfig, []); + int pos = 0; + int len = conditionBytes.Count; + + bool value = _definition.Evaluator(localState, ref len, ref pos, out bool faulted); + + if (faulted) + { + processor.WriteToTarget(Tokens[0].Value, Tokens[0].Start, Tokens[0].Length); + MemoryStream fragment = new MemoryStream(); + fragment.Write(condition, 0, condition.Length); + fragment.Write(_closeConditionTrie.Tokens[0].Value, _closeConditionTrie.Tokens[0].Start, _closeConditionTrie.Tokens[0].Length); + fragment.Write(processor.CurrentBuffer, currentBufferPosition, bufferLength - currentBufferPosition); + fragment.Position = 0; + processor.Inject(fragment); + currentBufferPosition = processor.CurrentBufferPosition; + int written = Tokens[0].Length; + return written; + } + + if (value) + { + processor.WhitespaceHandler(ref bufferLength, ref currentBufferPosition, trimBackward: true); + return 0; + } + + processor.SeekTargetBackUntil(_scanBackTrie, true); + FindEnd(processor, ref bufferLength, ref currentBufferPosition); + processor.WhitespaceHandler(ref bufferLength, ref currentBufferPosition, _definition.WholeLine, _definition.TrimWhitespace); + return 0; + } + + private void FindEnd(IProcessorState processorState, ref int bufferLength, ref int currentBufferPosition) + { + int depth = 1; + bool inElement = true; + bool inComment = false; + + while (bufferLength >= _structureTrie.MinLength) + { + //Try to get at least the max length of the tree into the buffer + if (bufferLength - currentBufferPosition < _structureTrie.MaxLength) + { + processorState.AdvanceBuffer(currentBufferPosition); + currentBufferPosition = processorState.CurrentBufferPosition; + bufferLength = processorState.CurrentBufferLength; + } + + int sz = bufferLength == processorState.CurrentBuffer.Length ? _structureTrie.MaxLength : _structureTrie.MinLength; + + for (; currentBufferPosition < bufferLength - sz + 1; ++currentBufferPosition) + { + if (bufferLength == 0) + { + currentBufferPosition = 0; + return; + } + + if (_structureTrie.GetOperation(processorState.CurrentBuffer, bufferLength, ref currentBufferPosition, out int token)) + { + if (inComment) + { + // When in comment keep skipping everything else until comment closed + if (token == _mapping.CloseCommentToken) + { + inComment = false; + } + } + else if (token == _mapping.OpenCommentToken) + { + inComment = true; + } + else if (token == _mapping.OpenOpenElementToken) + { + ++depth; + inElement = true; + } + else if (token == _mapping.SelfClosingElementEndToken) + { + --depth; + inElement = false; + } + else if (token == _mapping.CloseElementTagToken) + { + if (inElement) + { + inElement = false; + } + else + { + --depth; + } + } + else if (token == _mapping.OpenCloseElementToken) + { + inElement = false; + } + + if (depth == 0) + { + return; + } + } + } + } + + //Ran out of places to check and haven't reached the actual match, consume all the way to the end + currentBufferPosition = bufferLength; + } + + private void ScanToCloseCondition(IProcessorState processorState, List conditionBytes, ref int bufferLength, ref int currentBufferPosition) + { + int previousPosition = currentBufferPosition; + + while (bufferLength >= _closeConditionTrie.MinLength) + { + //Try to get at least the max length of the tree into the buffer + if (bufferLength - currentBufferPosition < _closeConditionTrie.MaxLength) + { + conditionBytes.AddRange(processorState.CurrentBuffer.Skip(previousPosition).Take(currentBufferPosition - previousPosition)); + processorState.AdvanceBuffer(currentBufferPosition); + currentBufferPosition = processorState.CurrentBufferPosition; + bufferLength = processorState.CurrentBufferLength; + previousPosition = 0; + } + + int sz = bufferLength == processorState.CurrentBuffer.Length ? _closeConditionTrie.MaxLength : _closeConditionTrie.MinLength; + + for (; currentBufferPosition < bufferLength - sz + 1; ++currentBufferPosition) + { + if (bufferLength == 0) + { + currentBufferPosition = 0; + return; + } + + if (_closeConditionTrie.GetOperation(processorState.CurrentBuffer, bufferLength, ref currentBufferPosition, out int token)) + { + conditionBytes.AddRange(processorState.CurrentBuffer.Skip(previousPosition).Take(currentBufferPosition - previousPosition - _closeConditionTrie.Tokens[token].Length)); + return; + } + } + } + + //Ran out of places to check and haven't reached the actual match, consume all the way to the end + currentBufferPosition = bufferLength; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/MarkupTokenMapping.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/MarkupTokenMapping.cs new file mode 100644 index 000000000000..36fb42e7b6cf --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/MarkupTokenMapping.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Operations +{ + internal class MarkupTokenMapping + { + public MarkupTokenMapping( + int openOpenElementToken, + int openCloseElementToken, + int closeCloseElementToken, + int selfClosingElementEndToken, + int openCommentToken, + int closeCommentToken) + { + OpenOpenElementToken = openOpenElementToken; + OpenCloseElementToken = openCloseElementToken; + CloseElementTagToken = closeCloseElementToken; + SelfClosingElementEndToken = selfClosingElementEndToken; + OpenCommentToken = openCommentToken; + CloseCommentToken = closeCommentToken; + } + + public int CloseElementTagToken { get; } + + public int OpenCloseElementToken { get; } + + public int OpenOpenElementToken { get; } + + public int SelfClosingElementEndToken { get; } + + public int OpenCommentToken { get; } + + public int CloseCommentToken { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/MarkupTokens.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/MarkupTokens.cs new file mode 100644 index 000000000000..55a6fca4454c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/MarkupTokens.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class MarkupTokens + { + public MarkupTokens( + ITokenConfig openOpenElementToken, + ITokenConfig openCloseElementToken, + ITokenConfig closeElementTagToken, + ITokenConfig selfClosingElementEndToken, + ITokenConfig openConditionExpression, + ITokenConfig closeConditionExpression, + ITokenConfig openCommentToken, + ITokenConfig closeCommentToken) + { + OpenOpenElementToken = openOpenElementToken; + OpenCloseElementToken = openCloseElementToken; + CloseElementTagToken = closeElementTagToken; + SelfClosingElementEndToken = selfClosingElementEndToken; + OpenConditionExpression = openConditionExpression; + CloseConditionExpression = closeConditionExpression; + OpenCommentToken = openCommentToken; + CloseCommentToken = closeCommentToken; + } + + public ITokenConfig CloseConditionExpression { get; } + + public ITokenConfig CloseElementTagToken { get; } + + public ITokenConfig OpenCloseElementToken { get; } + + public ITokenConfig OpenConditionExpression { get; } + + public ITokenConfig OpenOpenElementToken { get; } + + public ITokenConfig SelfClosingElementEndToken { get; } + + public ITokenConfig OpenCommentToken { get; } + + public ITokenConfig CloseCommentToken { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Phase.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Phase.cs new file mode 100644 index 000000000000..745893880a53 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Phase.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class Phase + { + public Phase(ITokenConfig match, IReadOnlyList resetsWith) + : this(match, null, resetsWith) + { + } + + public Phase(ITokenConfig match, string? replacement, IReadOnlyList resetsWith) + { + Match = match; + Replacement = replacement; + ResetsWith = resetsWith; + Next = new List(); + } + + public ITokenConfig Match { get; } + + public List Next { get; } + + public string? Replacement { get; } + + public IReadOnlyList ResetsWith { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/PhasedOperation.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/PhasedOperation.cs new file mode 100644 index 000000000000..c6815c102fde --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/PhasedOperation.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class PhasedOperation : IOperationProvider + { + private readonly IReadOnlyList _config; + private readonly bool _initialState; + + public PhasedOperation(string? id, IReadOnlyList config, bool initialState) + { + Id = id; + _config = config; + _initialState = initialState; + } + + public string? Id { get; } + + public IOperation GetOperation(Encoding encoding, IProcessorState processorState) + { + Dictionary tokenMap = new Dictionary(); + List tokens = new List(); + + Stack> sourceParents = new Stack>(); + Stack> targetParents = new Stack>(); + IEnumerator? currentSource = _config.GetEnumerator(); + List currentTarget = new List(); + + while (sourceParents.Count > 0 || currentSource != null) + { + while (currentSource?.MoveNext() ?? false) + { + Phase c = currentSource.Current; + if (!tokenMap.TryGetValue(c.Match, out int existingMatchToken)) + { + IToken bytes = c.Match.ToToken(encoding); + existingMatchToken = tokenMap[c.Match] = tokens.Count; + tokens.Add(bytes); + } + + SpecializedPhase target = new SpecializedPhase + { + Replacement = c.Replacement != null ? encoding.GetBytes(c.Replacement) : tokens[existingMatchToken].Value, + Match = existingMatchToken, + }; + + foreach (ITokenConfig reset in c.ResetsWith) + { + if (!tokenMap.TryGetValue(reset, out int existingResetToken)) + { + IToken token = reset.ToToken(encoding); + existingResetToken = tokenMap[reset] = tokens.Count; + tokens.Add(token); + } + target.ResetsWith.Add(existingResetToken); + } + + currentTarget.Add(target); + + if (c.Next.Count > 0) + { + sourceParents.Push(currentSource); + currentSource = currentSource.Current.Next.GetEnumerator(); + + targetParents.Push(currentTarget); + currentTarget = new List(); + } + } + + currentSource?.Dispose(); + currentSource = null; + + if (sourceParents.Count > 0) + { + currentSource = sourceParents.Pop(); + List children = currentTarget; + currentTarget = targetParents.Pop(); + currentTarget[currentTarget.Count - 1].Next.AddRange(children); + } + } + + return new Implementation(this, tokens, currentTarget, _initialState); + } + + private class Implementation : IOperation + { + private readonly PhasedOperation _definition; + private readonly IReadOnlyList _entryPoints; + private SpecializedPhase? _currentPhase; + + public Implementation(PhasedOperation definition, IReadOnlyList config, IReadOnlyList entryPoints, bool initialState) + { + _definition = definition; + Tokens = config; + _entryPoints = entryPoints; + IsInitialStateOn = string.IsNullOrEmpty(_definition.Id) || initialState; + } + + public string? Id => _definition.Id; + + public IReadOnlyList Tokens { get; } + + public bool IsInitialStateOn { get; } + + public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + IReadOnlyList nextPhases = _currentPhase?.Next ?? _entryPoints; + SpecializedPhase? match = nextPhases.FirstOrDefault(x => x.Match == token); + + if (match != null) + { + _currentPhase = match.Next.Count > 0 ? match : null; + processor.WriteToTarget(match.Replacement!, 0, match.Replacement!.Length); + return match.Replacement.Length; + } + + if (_currentPhase != null && _currentPhase.ResetsWith.Contains(token)) + { + _currentPhase = null; + } + + processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length); + return Tokens[token].Length; + } + } + + private class SpecializedPhase + { + public SpecializedPhase() + { + Next = new List(); + ResetsWith = new List(); + } + + public int Match { get; set; } + + public List Next { get; } + + public byte[]? Replacement { get; set; } + + public List ResetsWith { get; } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Region.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Region.cs new file mode 100644 index 000000000000..d9a945f48784 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Region.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class Region : IOperationProvider + { + public static readonly string OperationName = "region"; + + private readonly ITokenConfig _end; + private readonly bool _include; + private readonly ITokenConfig _start; + private readonly bool _toggle; + private readonly bool _wholeLine; + private readonly bool _trimWhitespace; + private readonly bool _initialState; + + public Region(ITokenConfig start, ITokenConfig end, bool include, bool wholeLine, bool trimWhitespace, string? id, bool initialState) + { + _wholeLine = wholeLine; + _trimWhitespace = trimWhitespace; + _start = start; + _end = end; + _include = include; + _toggle = _start.Equals(_end); + Id = id; + _initialState = initialState; + } + + public string? Id { get; } + + public IOperation GetOperation(Encoding encoding, IProcessorState processorState) + { + IToken startToken = _start.ToToken(encoding); + IToken endToken = _end.ToToken(encoding); + return new Implementation(this, startToken, endToken, _include, _toggle, Id, _initialState); + } + + private class Implementation : IOperation + { + private readonly IToken _endToken; + private readonly bool _includeRegion; + private readonly bool _startAndEndAreSame; + private readonly Region _definition; + private bool _waitingForEnd; + + public Implementation(Region owner, IToken startToken, IToken endToken, bool include, bool toggle, string? id, bool initialState) + { + _definition = owner; + _endToken = endToken; + _includeRegion = include; + _startAndEndAreSame = toggle; + + Tokens = toggle ? new[] { startToken } : new[] { startToken, endToken }; + Id = id; + IsInitialStateOn = string.IsNullOrEmpty(id) || initialState; + } + + public IReadOnlyList Tokens { get; } + + public string? Id { get; } + + public bool IsInitialStateOn { get; } + + public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + if (processor.Config.Flags.TryGetValue(Region.OperationName, out bool flag) && !flag) + { + processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length); + return Tokens[token].Length; + } + + processor.WhitespaceHandler(ref bufferLength, ref currentBufferPosition, wholeLine: _definition._wholeLine, trim: _definition._trimWhitespace); + + if (_startAndEndAreSame) + { + token = _waitingForEnd ? 1 : 0; + } + + //If we're resuming from a region that has been included (we've found the end now) + // just process the end + if (_waitingForEnd && token == 1) + { + _waitingForEnd = false; + return 0; + } + + if (token != 0) + { + return 0; + } + + //If we're including the region, set that we're waiting for the end and return + // control to the processor + if (_includeRegion) + { + _waitingForEnd = true; + return 0; + } + + //If we've made it here, we're skipping stuff, skip all the way to the end of the + // end token + + int i = currentBufferPosition; + int j = 0; + + for (; j < _endToken.Length; ++j) + { + if (i + j == bufferLength) + { + processor.AdvanceBuffer(i + j); + bufferLength = processor.CurrentBufferLength; + i = -j; + } + + //TODO: This should be using one of the tries rather than looking for the byte run directly + if (processor.CurrentBuffer[i + j] != _endToken.Value[j]) + { + ++i; + j = -1; + } + } + + i += j; + + processor.WhitespaceHandler(ref bufferLength, ref i, wholeLine: _definition._wholeLine, trim: _definition._trimWhitespace); + + currentBufferPosition = i; + return 0; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Replacement.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Replacement.cs new file mode 100644 index 000000000000..eede3e6cb6cd --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/Replacement.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class Replacement : IOperationProvider + { + public static readonly string OperationName = "replacement"; + + private readonly ITokenConfig _match; + private readonly string? _replaceWith; + private readonly bool _initialState; + + public Replacement(ITokenConfig match, string? replaceWith, string? id, bool initialState) + { + _match = match; + _replaceWith = replaceWith; + Id = id; + _initialState = initialState; + } + + public string? Id { get; } + + public IOperation GetOperation(Encoding encoding, IProcessorState processorState) + { + IToken token = _match.ToToken(encoding); + byte[] replaceWith = encoding.GetBytes(_replaceWith); + + if (token.Value.Skip(token.Start).Take(token.Length).SequenceEqual(replaceWith)) + { + return null!; + } + + return new Implementation(token, replaceWith, Id, _initialState); + } + + private class Implementation : IOperation + { + private readonly byte[] _replacement; + + public Implementation(IToken token, byte[] replaceWith, string? id, bool initialState) + { + _replacement = replaceWith; + Id = id; + Tokens = new[] { token }; + IsInitialStateOn = string.IsNullOrEmpty(id) || initialState; + } + + public IReadOnlyList Tokens { get; } + + public string? Id { get; } + + public bool IsInitialStateOn { get; } + + public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + if (processor.Config.Flags.TryGetValue(OperationName, out bool flag) && !flag) + { + processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length); + return Tokens[token].Length; + } + + processor.WriteToTarget(_replacement, 0, _replacement.Length); + return _replacement.Length; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ReplacementTokens.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ReplacementTokens.cs new file mode 100644 index 000000000000..5df9fe940457 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/ReplacementTokens.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class ReplacementTokens : IReplacementTokens + { + public ReplacementTokens(string identity, ITokenConfig originalValue) + { + VariableName = identity; + OriginalValue = originalValue; + } + + public string VariableName { get; } + + public ITokenConfig OriginalValue { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/SetFlag.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/SetFlag.cs new file mode 100644 index 000000000000..301deea36c0a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Operations/SetFlag.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Operations +{ + public class SetFlag : IOperationProvider + { + public static readonly string OperationName = "flags"; + + private readonly bool _initialState; + + public SetFlag(string? name, ITokenConfig on, ITokenConfig off, ITokenConfig onNoEmit, ITokenConfig offNoEmit, string? id, bool initialState, bool? @default = null) + { + Name = name; + On = on; + Off = off; + OnNoEmit = onNoEmit; + OffNoEmit = offNoEmit; + Default = @default; + Id = id; + _initialState = initialState; + } + + public string? Id { get; } + + public string? Name { get; } + + public ITokenConfig On { get; } + + public ITokenConfig Off { get; } + + public bool? Default { get; } + + public ITokenConfig OnNoEmit { get; } + + public ITokenConfig OffNoEmit { get; } + + public IOperation GetOperation(Encoding encoding, IProcessorState processorState) + { + IToken[] tokens = new[] + { + On.ToToken(encoding), + Off.ToToken(encoding), + OnNoEmit.ToToken(encoding), + OffNoEmit.ToToken(encoding) + }; + + if (Default.HasValue) + { + processorState.Config.Flags[Name!] = Default.Value; + } + + return new Implementation(this, tokens, Id, _initialState); + } + + private class Implementation : IOperation + { + private readonly SetFlag _owner; + + public Implementation(SetFlag owner, IReadOnlyList tokens, string? id, bool initialState) + { + _owner = owner; + Tokens = tokens; + Id = id; + IsInitialStateOn = string.IsNullOrEmpty(id) || initialState; + } + + public IReadOnlyList Tokens { get; } + + public string? Id { get; } + + public bool IsInitialStateOn { get; } + + public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + if (!processor.Config.Flags.TryGetValue(OperationName, out bool flagsOn)) + { + flagsOn = true; + } + + bool emit = token < 2 || !flagsOn; + bool turnOn = (token % 2) == 0; + int written = 0; + + if (emit) + { + processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length); + written = Tokens[token].Length; + } + else + { + // consume the entire line when not emitting. Otherwise the newlines on the flag tokens get emitted + processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: true); + } + + //Only turn the flag in question back on if it's the "flags" flag. + // Yes, we still need to emit it as the common case is for this + // to be done in the template definition file + if (flagsOn) + { + processor.Config.Flags[_owner.Name!] = turnOn; + } + else if (_owner.Name == SetFlag.OperationName && turnOn) + { + processor.Config.Flags[SetFlag.OperationName] = true; + } + + return written; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/PublicAPI.Shipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.Core/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..e660de105413 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/PublicAPI.Shipped.txt @@ -0,0 +1,543 @@ +#nullable enable +abstract Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.DereferenceInLiterals.get -> bool +Microsoft.TemplateEngine.Core.CommonOperations +Microsoft.TemplateEngine.Core.Expressions.BinaryScope +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.IsFull.get -> bool +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.IsIndivisible.get -> bool +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.Operator.get -> TOperator +Microsoft.TemplateEngine.Core.Expressions.Converter +Microsoft.TemplateEngine.Core.Expressions.Cpp.CppStyleEvaluatorDefinition +Microsoft.TemplateEngine.Core.Expressions.Cpp.QuotedRegionKind +Microsoft.TemplateEngine.Core.Expressions.Cpp.QuotedRegionKind.DoubleQuoteRegion = 1 -> Microsoft.TemplateEngine.Core.Expressions.Cpp.QuotedRegionKind +Microsoft.TemplateEngine.Core.Expressions.Cpp.QuotedRegionKind.None = 0 -> Microsoft.TemplateEngine.Core.Expressions.Cpp.QuotedRegionKind +Microsoft.TemplateEngine.Core.Expressions.Cpp.QuotedRegionKind.SingleQuoteRegion = 2 -> Microsoft.TemplateEngine.Core.Expressions.Cpp.QuotedRegionKind +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Cpp2StyleEvaluatorDefinition() -> void +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.Add = 19 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.And = 0 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.BitwiseAnd = 23 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.BitwiseOr = 24 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.CloseBrace = 10 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.Divide = 22 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.DoubleQuote = 26 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.EqualTo = 7 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.GreaterThan = 3 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.GreaterThanOrEqualTo = 4 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.LeftShift = 17 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.LegacyMacEOL = 15 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.LessThan = 5 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.LessThanOrEqualTo = 6 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.Literal = 27 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.Multiply = 21 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.Not = 2 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.NotEqualTo = 8 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.OpenBrace = 9 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.Or = 1 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.Quote = 16 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.RightShift = 18 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.SingleQuote = 25 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.Space = 11 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.Subtract = 20 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.Tab = 12 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.UnixEOL = 14 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens.WindowsEOL = 13 -> Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.IEvaluable +Microsoft.TemplateEngine.Core.Expressions.IEvaluable.IsFull.get -> bool +Microsoft.TemplateEngine.Core.Expressions.IEvaluable.IsIndivisible.get -> bool +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.CloseGroupToken.get -> TToken +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.Identity.get -> TOperator +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.LiteralToken.get -> TToken +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.OpenGroupToken.get -> TToken +Microsoft.TemplateEngine.Core.Expressions.ITypeConverter +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.MSBuildStyleEvaluatorDefinition() -> void +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.And = 0 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.CloseBrace = 10 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.EqualTo = 7 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.GreaterThan = 3 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.GreaterThanOrEqualTo = 4 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.LegacyMacEOL = 15 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.LessThan = 5 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.LessThanOrEqualTo = 6 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.Literal = 18 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.Not = 2 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.NotEqualTo = 8 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.OpenBrace = 9 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.Or = 1 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.Quote = 16 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.Space = 11 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.Tab = 12 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.UnixEOL = 14 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.VariableStart = 17 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens.WindowsEOL = 13 -> Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.Tokens +Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.Add = 28 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.And = 41 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.BadSyntax = -1 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.BitwiseAnd = 38 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.BitwiseOr = 40 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.Divide = 26 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.EqualTo = 36 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.Exponentiate = 24 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.GreaterThan = 33 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.GreaterThanOrEqualTo = 35 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.Identity = 1 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.LeftShift = 30 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.LessThan = 32 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.LessThanOrEqualTo = 34 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.Modulus = 27 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.Multiply = 25 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.None = 0 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.Not = 2 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.NotEqualTo = 37 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.Or = 42 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.RightShift = 31 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.Subtract = 29 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.Operators.Xor = 39 -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.CloseGroupToken.get -> TToken +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.CustomTypeConverter +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.CustomTypeConverter.CustomTypeConverter() -> void +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Identity.get -> Microsoft.TemplateEngine.Core.Expressions.Operators +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.LiteralToken.get -> TToken +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.OpenGroupToken.get -> TToken +Microsoft.TemplateEngine.Core.Expressions.ScopeBuilder +Microsoft.TemplateEngine.Core.Expressions.ScopeBuilderHelper +Microsoft.TemplateEngine.Core.Expressions.Shared.CoreConverters +Microsoft.TemplateEngine.Core.Expressions.Shared.CppStyleConverters +Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.SharedEvaluatorDefinition() -> void +Microsoft.TemplateEngine.Core.Expressions.Shared.VisualBasicStyleConverters +Microsoft.TemplateEngine.Core.Expressions.Shared.XmlStyleConverters +Microsoft.TemplateEngine.Core.Expressions.Token +Microsoft.TemplateEngine.Core.Expressions.Token.Family.get -> TToken +Microsoft.TemplateEngine.Core.Expressions.TokenScope +Microsoft.TemplateEngine.Core.Expressions.TokenScope.IsFull.get -> bool +Microsoft.TemplateEngine.Core.Expressions.TokenScope.IsIndivisible.get -> bool +Microsoft.TemplateEngine.Core.Expressions.TokenScope.IsQuoted.get -> bool +Microsoft.TemplateEngine.Core.Expressions.TokenScope.IsQuoted.set -> void +Microsoft.TemplateEngine.Core.Expressions.TypeConverterDelegate +Microsoft.TemplateEngine.Core.Expressions.UnaryScope +Microsoft.TemplateEngine.Core.Expressions.UnaryScope.IsFull.get -> bool +Microsoft.TemplateEngine.Core.Expressions.UnaryScope.IsIndivisible.get -> bool +Microsoft.TemplateEngine.Core.Expressions.UnaryScope.Operator.get -> TOperator +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Add = 22 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.And = 0 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.AndAlso = 1 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.CloseBrace = 13 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Divide = 25 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.DoubleQuote = 27 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.EqualTo = 9 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Exponentiate = 26 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.GreaterThan = 5 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.GreaterThanOrEqualTo = 6 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.LeftShift = 20 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.LegacyMacEOL = 18 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.LessThan = 7 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.LessThanOrEqualTo = 8 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Literal = 28 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Multiply = 24 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Not = 4 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.NotEqualTo = 10 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.OpenBrace = 12 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Or = 2 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.OrElse = 3 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Quote = 19 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.RightShift = 21 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Space = 14 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Subtract = 23 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Tab = 15 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.UnixEOL = 17 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.WindowsEOL = 16 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens.Xor = 11 -> Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.Tokens +Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.VisualBasicStyleEvaluatorDefintion() -> void +Microsoft.TemplateEngine.Core.FileChange +Microsoft.TemplateEngine.Core.FileChange.ChangeKind.get -> Microsoft.TemplateEngine.Abstractions.ChangeKind +Microsoft.TemplateEngine.Core.FileChange.Contents.get -> byte[]! +Microsoft.TemplateEngine.Core.FileChange.FileChange(string! sourceRelativePath, string! targetRelativePath, Microsoft.TemplateEngine.Abstractions.ChangeKind changeKind, byte[]? contents = null) -> void +Microsoft.TemplateEngine.Core.FileChange.SourceRelativePath.get -> string! +Microsoft.TemplateEngine.Core.FileChange.TargetRelativePath.get -> string! +Microsoft.TemplateEngine.Core.KeysChangedEventArgs +Microsoft.TemplateEngine.Core.KeysChangedEventArgs.KeysChangedEventArgs() -> void +Microsoft.TemplateEngine.Core.Matching.OperationTerminal +Microsoft.TemplateEngine.Core.Matching.OperationTerminal.Token.get -> int +Microsoft.TemplateEngine.Core.Matching.TerminalBase +Microsoft.TemplateEngine.Core.Matching.TerminalBase.End.get -> int +Microsoft.TemplateEngine.Core.Matching.TerminalBase.End.set -> void +Microsoft.TemplateEngine.Core.Matching.TerminalBase.Length.get -> int +Microsoft.TemplateEngine.Core.Matching.TerminalBase.Start.get -> int +Microsoft.TemplateEngine.Core.Matching.TerminalBase.Start.set -> void +Microsoft.TemplateEngine.Core.Matching.TerminalBase.TerminalBase(int tokenLength, int start, int end) -> void +Microsoft.TemplateEngine.Core.Matching.TerminalLocation.Location.get -> int +Microsoft.TemplateEngine.Core.Matching.TerminalLocation.Location.set -> void +Microsoft.TemplateEngine.Core.Matching.Trie.MaxRemainingLength.get -> int +Microsoft.TemplateEngine.Core.Matching.Trie.Trie() -> void +Microsoft.TemplateEngine.Core.Matching.TrieEvaluator.MaxLength.get -> int +Microsoft.TemplateEngine.Core.Matching.TrieEvaluator.OldestRequiredSequenceNumber.get -> int +Microsoft.TemplateEngine.Core.Matching.TrieNode.IsTerminal.get -> bool +Microsoft.TemplateEngine.Core.Matching.TrieNode.Match.get -> byte +Microsoft.TemplateEngine.Core.Matching.TrieNode.TrieNode(byte match) -> void +Microsoft.TemplateEngine.Core.Operations.BalancedNesting +Microsoft.TemplateEngine.Core.Operations.Conditional +Microsoft.TemplateEngine.Core.Operations.Conditional.TrimWhitespace.get -> bool +Microsoft.TemplateEngine.Core.Operations.Conditional.WholeLine.get -> bool +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ConditionalTokens() -> void +Microsoft.TemplateEngine.Core.Operations.ConditionEvaluator +Microsoft.TemplateEngine.Core.Operations.ExpandVariables +Microsoft.TemplateEngine.Core.Operations.Include +Microsoft.TemplateEngine.Core.Operations.InlineMarkupConditional +Microsoft.TemplateEngine.Core.Operations.InlineMarkupConditional.TrimWhitespace.get -> bool +Microsoft.TemplateEngine.Core.Operations.InlineMarkupConditional.WholeLine.get -> bool +Microsoft.TemplateEngine.Core.Operations.MarkupTokens +Microsoft.TemplateEngine.Core.Operations.Phase +Microsoft.TemplateEngine.Core.Operations.PhasedOperation +Microsoft.TemplateEngine.Core.Operations.Region +Microsoft.TemplateEngine.Core.Operations.Replacement +Microsoft.TemplateEngine.Core.Operations.ReplacementTokens +Microsoft.TemplateEngine.Core.Operations.SetFlag +Microsoft.TemplateEngine.Core.Operations.SetFlag.Default.get -> bool? +Microsoft.TemplateEngine.Core.TokenConfig +Microsoft.TemplateEngine.Core.TokenConfigExtensions +Microsoft.TemplateEngine.Core.Util.EncodingConfig +Microsoft.TemplateEngine.Core.Util.EncodingUtil +Microsoft.TemplateEngine.Core.Util.EngineConfig +Microsoft.TemplateEngine.Core.Util.Orchestrator +Microsoft.TemplateEngine.Core.Util.Processor +Microsoft.TemplateEngine.Core.Util.ProcessorState +Microsoft.TemplateEngine.Core.Util.ProcessorState.AdvanceBuffer(int bufferPosition) -> bool +Microsoft.TemplateEngine.Core.Util.ProcessorState.CurrentBufferLength.get -> int +Microsoft.TemplateEngine.Core.Util.ProcessorState.CurrentBufferPosition.get -> int +Microsoft.TemplateEngine.Core.Util.ProcessorState.CurrentSequenceNumber.get -> int +Microsoft.TemplateEngine.Core.Util.ProcessorState.Run() -> bool +Microsoft.TemplateEngine.Core.Util.Token +Microsoft.TemplateEngine.Core.Util.Token.Index.get -> int +Microsoft.TemplateEngine.Core.Util.TokenTrie +Microsoft.TemplateEngine.Core.Util.TokenTrie.Count.get -> int +Microsoft.TemplateEngine.Core.Util.TokenTrie.MaxLength.get -> int +Microsoft.TemplateEngine.Core.Util.TokenTrie.MinLength.get -> int +Microsoft.TemplateEngine.Core.Util.TokenTrie.TokenTrie() -> void +Microsoft.TemplateEngine.Core.Util.TokenTrieEvaluator +Microsoft.TemplateEngine.Core.Util.TokenTrieEvaluator.Accept(byte data, ref int bufferPosition, out int token) -> bool +Microsoft.TemplateEngine.Core.Util.TokenTrieEvaluator.BytesToKeepInBuffer.get -> int +Microsoft.TemplateEngine.Core.Util.TokenTrieEvaluator.TryFinalizeMatchesInProgress(ref int bufferPosition, out int token) -> bool +Microsoft.TemplateEngine.Core.ValueReadEventArgs +Microsoft.TemplateEngine.Core.VariableCollection +Microsoft.TemplateEngine.Core.VariableCollection.Clear() -> void +Microsoft.TemplateEngine.Core.VariableCollection.Count.get -> int +Microsoft.TemplateEngine.Core.VariableCollection.IsReadOnly.get -> bool +Microsoft.TemplateEngine.Core.VariableCollection.VariableCollection() -> void +override Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.DereferenceInLiterals.get -> bool +override Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.DereferenceInLiterals.get -> bool +override Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.DereferenceInLiterals.get -> bool +override Microsoft.TemplateEngine.Core.TokenConfig.GetHashCode() -> int +abstract Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.GenerateMap() -> Microsoft.TemplateEngine.Core.Expressions.IOperatorMap! +abstract Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.GetSymbols(Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor) -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +abstract Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.NullTokenValue.get -> string! +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.BinaryScope(Microsoft.TemplateEngine.Core.Expressions.IEvaluable? parent, TOperator operator, System.Func! evaluate) -> void +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.Evaluate() -> object! +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.Left.get -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable? +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.Left.set -> void +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.Parent.get -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable? +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.Parent.set -> void +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.Right.get -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable? +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.Right.set -> void +Microsoft.TemplateEngine.Core.Expressions.BinaryScope.TryAccept(Microsoft.TemplateEngine.Core.Expressions.IEvaluable? child) -> bool +Microsoft.TemplateEngine.Core.Expressions.IEvaluable.Evaluate() -> object? +Microsoft.TemplateEngine.Core.Expressions.IEvaluable.Parent.get -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable? +Microsoft.TemplateEngine.Core.Expressions.IEvaluable.Parent.set -> void +Microsoft.TemplateEngine.Core.Expressions.IEvaluable.TryAccept(Microsoft.TemplateEngine.Core.Expressions.IEvaluable? child) -> bool +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.BadSyntaxTokens.get -> System.Collections.Generic.ISet! +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.Decode(string! value) -> string! +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.Encode(string! value) -> string! +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.LiteralSequenceBoundsMarkers.get -> System.Collections.Generic.ISet! +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.NoOpTokens.get -> System.Collections.Generic.ISet! +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.OperatorScopeLookupFactory.get -> System.Collections.Generic.IReadOnlyDictionary!>! +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.Terminators.get -> System.Collections.Generic.ISet! +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.TokensToOperatorsMap.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Core.Expressions.IOperatorMap.TryConvert(object? source, out T? result) -> bool +Microsoft.TemplateEngine.Core.Expressions.ITypeConverter.Register(Microsoft.TemplateEngine.Core.Expressions.TypeConverterDelegate! converter) -> Microsoft.TemplateEngine.Core.Expressions.ITypeConverter! +Microsoft.TemplateEngine.Core.Expressions.ITypeConverter.TryConvert(object? source, out T? result) -> bool +Microsoft.TemplateEngine.Core.Expressions.ITypeConverter.TryCoreConvert(object? source, out T? result) -> bool +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Add(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.And(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.BadSyntax(params TToken[]! token) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.BadSyntaxTokens.get -> System.Collections.Generic.ISet! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.BitwiseAnd(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.BitwiseOr(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.CloseGroup(TToken token) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.CustomTypeConverter.Register(Microsoft.TemplateEngine.Core.Expressions.TypeConverterDelegate! converter) -> Microsoft.TemplateEngine.Core.Expressions.ITypeConverter! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.CustomTypeConverter.ScopeType.get -> System.Type! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.CustomTypeConverter.TryConvert(object? source, out T? result) -> bool +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.CustomTypeConverter.TryCoreConvert(object? source, out T? result) -> bool +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Decode(string! value) -> string! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Divide(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Encode(string! value) -> string! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.EqualTo(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Exponentiate(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.GreaterThan(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.GreaterThanOrEqualTo(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Ignore(params TToken[]! token) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.LeftShift(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.LessThan(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.LessThanOrEqualTo(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Literal(TToken token) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.LiteralBoundsMarkers(params TToken[]! token) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.LiteralSequenceBoundsMarkers.get -> System.Collections.Generic.ISet! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Multiply(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.NoOpTokens.get -> System.Collections.Generic.ISet! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Not(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.NotEqualTo(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.OpenGroup(TToken token) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.OperatorScopeLookupFactory.get -> System.Collections.Generic.IReadOnlyDictionary!>! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.OperatorSetBuilder(System.Func! encoder, System.Func! decoder) -> void +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Or(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Other(Microsoft.TemplateEngine.Core.Expressions.Operators operator, TToken token, System.Func! nodeFactory) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.RightShift(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Subtract(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.TerminateWith(params TToken[]! token) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Terminators.get -> System.Collections.Generic.ISet! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.TokensToOperatorsMap.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.TryConvert(object? sender, out T? result) -> bool +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.TypeConverter(System.Action! configureConverter) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder.Xor(TToken token, System.Func? precedesOperator = null, System.Func? evaluate = null) -> Microsoft.TemplateEngine.Core.Expressions.OperatorSetBuilder! +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.TokenScope.Evaluate() -> object? +Microsoft.TemplateEngine.Core.Expressions.TokenScope.Parent.get -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable? +Microsoft.TemplateEngine.Core.Expressions.TokenScope.Parent.set -> void +Microsoft.TemplateEngine.Core.Expressions.TokenScope.Token.get -> Microsoft.TemplateEngine.Core.Expressions.Token! +Microsoft.TemplateEngine.Core.Expressions.TokenScope.TokenScope(Microsoft.TemplateEngine.Core.Expressions.IEvaluable! parent, Microsoft.TemplateEngine.Core.Expressions.Token! token) -> void +Microsoft.TemplateEngine.Core.Expressions.TokenScope.TryAccept(Microsoft.TemplateEngine.Core.Expressions.IEvaluable? child) -> bool +Microsoft.TemplateEngine.Core.Expressions.UnaryScope.Evaluate() -> object? +Microsoft.TemplateEngine.Core.Expressions.UnaryScope.Operand.get -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable? +Microsoft.TemplateEngine.Core.Expressions.UnaryScope.Operand.set -> void +Microsoft.TemplateEngine.Core.Expressions.UnaryScope.Parent.get -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable? +Microsoft.TemplateEngine.Core.Expressions.UnaryScope.Parent.set -> void +Microsoft.TemplateEngine.Core.Expressions.UnaryScope.TryAccept(Microsoft.TemplateEngine.Core.Expressions.IEvaluable? child) -> bool +Microsoft.TemplateEngine.Core.Expressions.UnaryScope.UnaryScope(Microsoft.TemplateEngine.Core.Expressions.IEvaluable? parent, TOperator operator, System.Func! evaluate) -> void +Microsoft.TemplateEngine.Core.Matching.Trie +Microsoft.TemplateEngine.Core.Matching.Trie.AddPath(byte[]! path, T! terminal) -> void +Microsoft.TemplateEngine.Core.Matching.Trie.NextNodes.get -> System.Collections.Generic.Dictionary!>! +Microsoft.TemplateEngine.Core.Matching.TrieEvaluationDriver +Microsoft.TemplateEngine.Core.Matching.TrieEvaluationDriver.Evaluate(byte[]! buffer, int bufferLength, bool isFinalBuffer, int lastNetBufferEffect, ref int bufferPosition) -> Microsoft.TemplateEngine.Core.Matching.TerminalLocation? +Microsoft.TemplateEngine.Core.Matching.TrieEvaluationDriver.TrieEvaluationDriver(Microsoft.TemplateEngine.Core.Matching.TrieEvaluator! trie) -> void +Microsoft.TemplateEngine.Core.Matching.TrieEvaluator +Microsoft.TemplateEngine.Core.Matching.TrieEvaluator.Accept(byte data, ref int sequenceNumber, out Microsoft.TemplateEngine.Core.Matching.TerminalLocation? terminal) -> bool +Microsoft.TemplateEngine.Core.Matching.TrieEvaluator.FinalizeMatchesInProgress(ref int sequenceNumber, out Microsoft.TemplateEngine.Core.Matching.TerminalLocation? terminals) -> void +Microsoft.TemplateEngine.Core.Matching.TrieEvaluator.TrieEvaluator(Microsoft.TemplateEngine.Core.Matching.Trie! trie) -> void +Microsoft.TemplateEngine.Core.Matching.TrieEvaluator.TryGetNext(bool isFinal, ref int sequenceNumber, out Microsoft.TemplateEngine.Core.Matching.TerminalLocation? terminalLocation) -> bool +Microsoft.TemplateEngine.Core.Matching.TrieNode +Microsoft.TemplateEngine.Core.Matching.TrieNode.Terminals.get -> System.Collections.Generic.List? +Microsoft.TemplateEngine.Core.Matching.TrieNode.Terminals.set -> void +Microsoft.TemplateEngine.Core.Operations.BalancedNesting.BalancedNesting(Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! startToken, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! realEndToken, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! pseudoEndToken, string? id, string? resetFlag, bool initialState) -> void +Microsoft.TemplateEngine.Core.Operations.BalancedNesting.GetOperation(System.Text.Encoding! encoding, Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processorState) -> Microsoft.TemplateEngine.Core.Contracts.IOperation! +Microsoft.TemplateEngine.Core.Operations.BalancedNesting.Id.get -> string? +Microsoft.TemplateEngine.Core.Operations.Conditional.Conditional(Microsoft.TemplateEngine.Core.Operations.ConditionalTokens! tokenVariants, bool wholeLine, bool trimWhitespace, Microsoft.TemplateEngine.Core.Operations.ConditionEvaluator! evaluator, string? id, bool initialState) -> void +Microsoft.TemplateEngine.Core.Operations.Conditional.Evaluator.get -> Microsoft.TemplateEngine.Core.Operations.ConditionEvaluator! +Microsoft.TemplateEngine.Core.Operations.Conditional.GetOperation(System.Text.Encoding! encoding, Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processorState) -> Microsoft.TemplateEngine.Core.Contracts.IOperation! +Microsoft.TemplateEngine.Core.Operations.Conditional.Id.get -> string? +Microsoft.TemplateEngine.Core.Operations.Conditional.Tokens.get -> Microsoft.TemplateEngine.Core.Operations.ConditionalTokens! +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ActionableElseIfTokens.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ActionableElseIfTokens.set -> void +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ActionableElseTokens.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ActionableElseTokens.set -> void +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ActionableIfTokens.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ActionableIfTokens.set -> void +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ActionableOperations.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ActionableOperations.set -> void +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ElseIfTokens.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ElseIfTokens.set -> void +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ElseTokens.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.ElseTokens.set -> void +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.EndIfTokens.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.EndIfTokens.set -> void +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.IfTokens.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Operations.ConditionalTokens.IfTokens.set -> void +Microsoft.TemplateEngine.Core.Operations.ExpandVariables.ExpandVariables(string? id, bool initialState) -> void +Microsoft.TemplateEngine.Core.Operations.ExpandVariables.GetOperation(System.Text.Encoding! encoding, Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor) -> Microsoft.TemplateEngine.Core.Contracts.IOperation! +Microsoft.TemplateEngine.Core.Operations.ExpandVariables.Id.get -> string? +Microsoft.TemplateEngine.Core.Operations.Include.EndToken.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.Include.GetOperation(System.Text.Encoding! encoding, Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processorState) -> Microsoft.TemplateEngine.Core.Contracts.IOperation! +Microsoft.TemplateEngine.Core.Operations.Include.Id.get -> string? +Microsoft.TemplateEngine.Core.Operations.Include.Include(Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! startToken, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! endToken, System.Func! sourceStreamOpener, string? id, bool initialState) -> void +Microsoft.TemplateEngine.Core.Operations.Include.SourceStreamOpener.get -> System.Func! +Microsoft.TemplateEngine.Core.Operations.Include.StartToken.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.InlineMarkupConditional.Evaluator.get -> Microsoft.TemplateEngine.Core.Operations.ConditionEvaluator! +Microsoft.TemplateEngine.Core.Operations.InlineMarkupConditional.GetOperation(System.Text.Encoding! encoding, Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processorState) -> Microsoft.TemplateEngine.Core.Contracts.IOperation! +Microsoft.TemplateEngine.Core.Operations.InlineMarkupConditional.Id.get -> string? +Microsoft.TemplateEngine.Core.Operations.InlineMarkupConditional.InlineMarkupConditional(Microsoft.TemplateEngine.Core.Operations.MarkupTokens! tokens, bool wholeLine, bool trimWhitespace, Microsoft.TemplateEngine.Core.Operations.ConditionEvaluator! evaluator, string! variableFormat, string? id, bool initialState) -> void +Microsoft.TemplateEngine.Core.Operations.InlineMarkupConditional.Tokens.get -> Microsoft.TemplateEngine.Core.Operations.MarkupTokens! +Microsoft.TemplateEngine.Core.Operations.InlineMarkupConditional.VariableFormat.get -> string! +Microsoft.TemplateEngine.Core.Operations.MarkupTokens.CloseConditionExpression.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.MarkupTokens.CloseElementTagToken.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.MarkupTokens.OpenCloseElementToken.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.MarkupTokens.OpenCommentToken.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.MarkupTokens.OpenConditionExpression.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.MarkupTokens.OpenOpenElementToken.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +~Microsoft.TemplateEngine.Core.Operations.MarkupTokens.SelfClosingElementEndToken.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig +Microsoft.TemplateEngine.Core.Operations.Phase.Match.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.Phase.Next.get -> System.Collections.Generic.List! +Microsoft.TemplateEngine.Core.Operations.Phase.Phase(Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! match, string? replacement, System.Collections.Generic.IReadOnlyList! resetsWith) -> void +Microsoft.TemplateEngine.Core.Operations.Phase.Phase(Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! match, System.Collections.Generic.IReadOnlyList! resetsWith) -> void +Microsoft.TemplateEngine.Core.Operations.Phase.Replacement.get -> string? +Microsoft.TemplateEngine.Core.Operations.Phase.ResetsWith.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Operations.PhasedOperation.GetOperation(System.Text.Encoding! encoding, Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processorState) -> Microsoft.TemplateEngine.Core.Contracts.IOperation! +Microsoft.TemplateEngine.Core.Operations.PhasedOperation.Id.get -> string? +Microsoft.TemplateEngine.Core.Operations.PhasedOperation.PhasedOperation(string? id, System.Collections.Generic.IReadOnlyList! config, bool initialState) -> void +Microsoft.TemplateEngine.Core.Operations.Region.GetOperation(System.Text.Encoding! encoding, Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processorState) -> Microsoft.TemplateEngine.Core.Contracts.IOperation! +Microsoft.TemplateEngine.Core.Operations.Region.Id.get -> string? +Microsoft.TemplateEngine.Core.Operations.Region.Region(Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! start, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! end, bool include, bool wholeLine, bool trimWhitespace, string? id, bool initialState) -> void +Microsoft.TemplateEngine.Core.Operations.Replacement.GetOperation(System.Text.Encoding! encoding, Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processorState) -> Microsoft.TemplateEngine.Core.Contracts.IOperation! +Microsoft.TemplateEngine.Core.Operations.Replacement.Id.get -> string? +Microsoft.TemplateEngine.Core.Operations.Replacement.Replacement(Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! match, string? replaceWith, string? id, bool initialState) -> void +Microsoft.TemplateEngine.Core.Operations.ReplacementTokens.OriginalValue.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.ReplacementTokens.ReplacementTokens(string! identity, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! originalValue) -> void +Microsoft.TemplateEngine.Core.Operations.ReplacementTokens.VariableName.get -> string! +Microsoft.TemplateEngine.Core.Operations.SetFlag.GetOperation(System.Text.Encoding! encoding, Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processorState) -> Microsoft.TemplateEngine.Core.Contracts.IOperation! +Microsoft.TemplateEngine.Core.Operations.SetFlag.Id.get -> string? +Microsoft.TemplateEngine.Core.Operations.SetFlag.Name.get -> string? +Microsoft.TemplateEngine.Core.Operations.SetFlag.Off.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.SetFlag.OffNoEmit.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.SetFlag.On.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.SetFlag.OnNoEmit.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.SetFlag.SetFlag(string? name, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! on, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! off, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! onNoEmit, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! offNoEmit, string? id, bool initialState, bool? default = null) -> void +Microsoft.TemplateEngine.Core.TokenConfig.After.get -> string? +Microsoft.TemplateEngine.Core.TokenConfig.Before.get -> string? +Microsoft.TemplateEngine.Core.TokenConfig.Equals(Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! other) -> bool +Microsoft.TemplateEngine.Core.TokenConfig.OnlyIfAfter(string? prefix) -> Microsoft.TemplateEngine.Core.TokenConfig! +Microsoft.TemplateEngine.Core.TokenConfig.OnlyIfBefore(string? suffix) -> Microsoft.TemplateEngine.Core.TokenConfig! +Microsoft.TemplateEngine.Core.TokenConfig.ToToken(System.Text.Encoding! encoding) -> Microsoft.TemplateEngine.Core.Contracts.IToken! +Microsoft.TemplateEngine.Core.TokenConfig.Value.get -> string? +Microsoft.TemplateEngine.Core.Util.EncodingConfig.Encoding.get -> System.Text.Encoding! +Microsoft.TemplateEngine.Core.Util.EncodingConfig.EncodingConfig(Microsoft.TemplateEngine.Core.Contracts.IEngineConfig! config, System.Text.Encoding! encoding) -> void +Microsoft.TemplateEngine.Core.Util.EncodingConfig.LineEndings.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +Microsoft.TemplateEngine.Core.Util.EncodingConfig.this[int index].get -> object! +Microsoft.TemplateEngine.Core.Util.EncodingConfig.VariableKeys.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Util.EncodingConfig.Variables.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +Microsoft.TemplateEngine.Core.Util.EncodingConfig.VariableValues.get -> System.Collections.Generic.IReadOnlyList!>! +Microsoft.TemplateEngine.Core.Util.EncodingConfig.Whitespace.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +Microsoft.TemplateEngine.Core.Util.EncodingConfig.WhitespaceOrLineEnding.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +Microsoft.TemplateEngine.Core.Util.EngineConfig.Flags.get -> System.Collections.Generic.IDictionary! +Microsoft.TemplateEngine.Core.Util.EngineConfig.LineEndings.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Util.EngineConfig.Logger.get -> Microsoft.Extensions.Logging.ILogger! +Microsoft.TemplateEngine.Core.Util.EngineConfig.VariableFormatString.get -> string! +Microsoft.TemplateEngine.Core.Util.EngineConfig.Whitespaces.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Util.Processor.CloneAndAppendOperations(System.Collections.Generic.IReadOnlyList! tempOperations) -> Microsoft.TemplateEngine.Core.Contracts.IProcessor! +Microsoft.TemplateEngine.Core.Util.Processor.Config.get -> Microsoft.TemplateEngine.Core.Util.EngineConfig! +Microsoft.TemplateEngine.Core.Util.Processor.Run(System.IO.Stream! source, System.IO.Stream! target) -> bool +Microsoft.TemplateEngine.Core.Util.Processor.Run(System.IO.Stream! source, System.IO.Stream! target, int bufferSize) -> bool +Microsoft.TemplateEngine.Core.Util.Processor.Run(System.IO.Stream! source, System.IO.Stream! target, int bufferSize, int flushThreshold) -> bool +Microsoft.TemplateEngine.Core.Util.ProcessorState.Config.get -> Microsoft.TemplateEngine.Core.Contracts.IEngineConfig! +Microsoft.TemplateEngine.Core.Util.ProcessorState.CurrentBuffer.get -> byte[]! +Microsoft.TemplateEngine.Core.Util.ProcessorState.Encoding.get -> System.Text.Encoding! +Microsoft.TemplateEngine.Core.Util.ProcessorState.EncodingConfig.get -> Microsoft.TemplateEngine.Core.Contracts.IEncodingConfig! +Microsoft.TemplateEngine.Core.Util.ProcessorState.Inject(System.IO.Stream! staged) -> void +Microsoft.TemplateEngine.Core.Util.ProcessorState.ProcessorState(System.IO.Stream! source, System.IO.Stream! target, int bufferSize, int flushThreshold, Microsoft.TemplateEngine.Core.Contracts.IEngineConfig! config, System.Collections.Generic.IReadOnlyList! operationProviders) -> void +Microsoft.TemplateEngine.Core.Util.Token.Token(byte[]! token, int index, int start = 0, int end = -1) -> void +Microsoft.TemplateEngine.Core.Util.Token.Value.get -> byte[]! +Microsoft.TemplateEngine.Core.Util.TokenTrie.AddToken(byte[]! literalToken) -> int +Microsoft.TemplateEngine.Core.Util.TokenTrie.AddToken(byte[]! literalToken, int index) -> void +Microsoft.TemplateEngine.Core.Util.TokenTrie.AddToken(Microsoft.TemplateEngine.Core.Contracts.IToken! token) -> int +Microsoft.TemplateEngine.Core.Util.TokenTrie.AddToken(Microsoft.TemplateEngine.Core.Contracts.IToken? token, int index) -> void +Microsoft.TemplateEngine.Core.Util.TokenTrie.Append(Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! trie) -> void +Microsoft.TemplateEngine.Core.Util.TokenTrie.CreateEvaluator() -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrieEvaluator! +Microsoft.TemplateEngine.Core.Util.TokenTrie.GetOperation(byte[]! buffer, int bufferLength, ref int currentBufferPosition, bool mustMatchPosition, out int token) -> bool +Microsoft.TemplateEngine.Core.Util.TokenTrie.GetOperation(byte[]! buffer, int bufferLength, ref int currentBufferPosition, out int token) -> bool +Microsoft.TemplateEngine.Core.Util.TokenTrie.TokenLength.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Util.TokenTrie.Tokens.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Util.TokenTrieEvaluator.TokenTrieEvaluator(Microsoft.TemplateEngine.Core.Matching.Trie! trie) -> void +override Microsoft.TemplateEngine.Core.Expressions.BinaryScope.ToString() -> string! +override Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.GenerateMap() -> Microsoft.TemplateEngine.Core.Expressions.IOperatorMap! +override Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.GetSymbols(Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor) -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +override Microsoft.TemplateEngine.Core.Expressions.Cpp2.Cpp2StyleEvaluatorDefinition.NullTokenValue.get -> string! +override Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.GenerateMap() -> Microsoft.TemplateEngine.Core.Expressions.IOperatorMap! +override Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.GetSymbols(Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor) -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +override Microsoft.TemplateEngine.Core.Expressions.MSBuild.MSBuildStyleEvaluatorDefinition.NullTokenValue.get -> string! +override Microsoft.TemplateEngine.Core.Expressions.TokenScope.ToString() -> string! +override Microsoft.TemplateEngine.Core.Expressions.UnaryScope.ToString() -> string! +override Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.GenerateMap() -> Microsoft.TemplateEngine.Core.Expressions.IOperatorMap! +override Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.GetSymbols(Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor) -> Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! +override Microsoft.TemplateEngine.Core.Expressions.VisualBasic.VisualBasicStyleEvaluatorDefintion.NullTokenValue.get -> string! +override Microsoft.TemplateEngine.Core.TokenConfig.Equals(object! obj) -> bool +static Microsoft.TemplateEngine.Core.Expressions.Converter.TryConvert(object? source, out T? result) -> bool +~static Microsoft.TemplateEngine.Core.Expressions.Cpp.CppStyleEvaluatorDefinition.Evaluate(Microsoft.TemplateEngine.Core.Contracts.IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, out bool faulted) -> bool +static Microsoft.TemplateEngine.Core.Expressions.ScopeBuilderHelper.ScopeBuilder(this Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor, Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! tokens, Microsoft.TemplateEngine.Core.Expressions.IOperatorMap! operatorMap, bool dereferenceInLiterals = false) -> Microsoft.TemplateEngine.Core.Expressions.ScopeBuilder! +static Microsoft.TemplateEngine.Core.Expressions.Shared.CoreConverters.TryHexConvert(string! prefix, Microsoft.TemplateEngine.Core.Expressions.ITypeConverter! obj, object? source, out int result) -> bool +static Microsoft.TemplateEngine.Core.Expressions.Shared.CoreConverters.TryHexConvert(string! prefix, Microsoft.TemplateEngine.Core.Expressions.ITypeConverter! obj, object? source, out long result) -> bool +static Microsoft.TemplateEngine.Core.Expressions.Shared.CppStyleConverters.ConfigureConverters(Microsoft.TemplateEngine.Core.Expressions.ITypeConverter! obj) -> void +static Microsoft.TemplateEngine.Core.Expressions.Shared.CppStyleConverters.Decode(string! arg) -> string! +static Microsoft.TemplateEngine.Core.Expressions.Shared.CppStyleConverters.Encode(string! arg) -> string! +static Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.Compare(object? left, object? right) -> int +static Microsoft.TemplateEngine.Core.Expressions.Shared.VisualBasicStyleConverters.ConfigureConverters(Microsoft.TemplateEngine.Core.Expressions.ITypeConverter! obj) -> void +static Microsoft.TemplateEngine.Core.Expressions.Shared.XmlStyleConverters.XmlDecode(string! arg) -> string! +static Microsoft.TemplateEngine.Core.Expressions.Shared.XmlStyleConverters.XmlEncode(string! arg) -> string! +static Microsoft.TemplateEngine.Core.KeysChangedEventArgs.Default.get -> Microsoft.TemplateEngine.Core.KeysChangedEventArgs! +static Microsoft.TemplateEngine.Core.TokenConfig.FromValue(string? value) -> Microsoft.TemplateEngine.Core.TokenConfig! +static Microsoft.TemplateEngine.Core.TokenConfig.LiteralToken(byte[]! data, int start = 0, int end = -1) -> Microsoft.TemplateEngine.Core.Contracts.IToken! +static Microsoft.TemplateEngine.Core.TokenConfigExtensions.LiteralToken(this byte[]! b, int start = 0, int end = -1) -> Microsoft.TemplateEngine.Core.Contracts.IToken! +static Microsoft.TemplateEngine.Core.TokenConfigExtensions.Token(this string! s, System.Text.Encoding! e) -> Microsoft.TemplateEngine.Core.Contracts.IToken! +static Microsoft.TemplateEngine.Core.TokenConfigExtensions.TokenConfig(this string? s) -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +static Microsoft.TemplateEngine.Core.TokenConfigExtensions.TokenConfigBuilder(this string? s) -> Microsoft.TemplateEngine.Core.TokenConfig! +static Microsoft.TemplateEngine.Core.TokenConfigExtensions.TokenConfigs(this System.Collections.Generic.IEnumerable! s) -> System.Collections.Generic.IReadOnlyList! +static Microsoft.TemplateEngine.Core.Util.EncodingUtil.Detect(byte[]! buffer, int currentBufferLength, out byte[]! bom) -> System.Text.Encoding! +~static Microsoft.TemplateEngine.Core.Util.EngineConfig.DefaultLineEndings.get -> System.Collections.Generic.IReadOnlyList +~static Microsoft.TemplateEngine.Core.Util.EngineConfig.DefaultWhitespaces.get -> System.Collections.Generic.IReadOnlyList +static Microsoft.TemplateEngine.Core.Util.Processor.Create(Microsoft.TemplateEngine.Core.Util.EngineConfig! config, params Microsoft.TemplateEngine.Core.Contracts.IOperationProvider![]! operations) -> Microsoft.TemplateEngine.Core.Contracts.IProcessor! +static Microsoft.TemplateEngine.Core.Util.Processor.Create(Microsoft.TemplateEngine.Core.Util.EngineConfig! config, System.Collections.Generic.IReadOnlyList! operations) -> Microsoft.TemplateEngine.Core.Contracts.IProcessor! +static readonly Microsoft.TemplateEngine.Core.Operations.BalancedNesting.OperationName -> string! +static readonly Microsoft.TemplateEngine.Core.Operations.Conditional.OperationName -> string! +static readonly Microsoft.TemplateEngine.Core.Operations.ExpandVariables.OperationName -> string! +static readonly Microsoft.TemplateEngine.Core.Operations.Include.OperationName -> string! +static readonly Microsoft.TemplateEngine.Core.Operations.Region.OperationName -> string! +static readonly Microsoft.TemplateEngine.Core.Operations.Replacement.OperationName -> string! +static readonly Microsoft.TemplateEngine.Core.Operations.SetFlag.OperationName -> string! +~virtual Microsoft.TemplateEngine.Core.Util.Orchestrator.RunSpecLoader(System.IO.Stream runSpec) -> Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec +Microsoft.TemplateEngine.Core.Util.Orchestrator.GetFileChanges(string! runSpecPath, Microsoft.TemplateEngine.Abstractions.Mount.IDirectory! sourceDir, string! targetDir) -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Util.Orchestrator.GetFileChanges(Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec! spec, Microsoft.TemplateEngine.Abstractions.Mount.IDirectory! sourceDir, string! targetDir) -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Core.Util.Orchestrator.Run(string! runSpecPath, Microsoft.TemplateEngine.Abstractions.Mount.IDirectory! sourceDir, string! targetDir) -> void +Microsoft.TemplateEngine.Core.Util.Orchestrator.Orchestrator(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem! fileSystem) -> void +Microsoft.TemplateEngine.Core.Util.Orchestrator.Run(Microsoft.TemplateEngine.Core.Contracts.IGlobalRunSpec! spec, Microsoft.TemplateEngine.Abstractions.Mount.IDirectory! sourceDir, string! targetDir) -> void +Microsoft.TemplateEngine.Core.ValueReadEventArgs.Key.get -> string! +Microsoft.TemplateEngine.Core.ValueReadEventArgs.Value.get -> object! +Microsoft.TemplateEngine.Core.ValueReadEventArgs.ValueReadEventArgs(string! key, object! value) -> void +Microsoft.TemplateEngine.Core.VariableCollection.Add(string! key, object! value) -> void +Microsoft.TemplateEngine.Core.VariableCollection.Add(System.Collections.Generic.KeyValuePair item) -> void +Microsoft.TemplateEngine.Core.VariableCollection.Contains(System.Collections.Generic.KeyValuePair item) -> bool +Microsoft.TemplateEngine.Core.VariableCollection.ContainsKey(string! key) -> bool +Microsoft.TemplateEngine.Core.VariableCollection.CopyTo(System.Collections.Generic.KeyValuePair[]! array, int arrayIndex) -> void +Microsoft.TemplateEngine.Core.VariableCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator>! +Microsoft.TemplateEngine.Core.VariableCollection.Keys.get -> System.Collections.Generic.ICollection! +Microsoft.TemplateEngine.Core.VariableCollection.KeysChanged -> Microsoft.TemplateEngine.Core.Contracts.KeysChangedEventHander? +Microsoft.TemplateEngine.Core.VariableCollection.Remove(string! key) -> bool +Microsoft.TemplateEngine.Core.VariableCollection.Remove(System.Collections.Generic.KeyValuePair item) -> bool +Microsoft.TemplateEngine.Core.VariableCollection.this[string! key].get -> object! +Microsoft.TemplateEngine.Core.VariableCollection.this[string! key].set -> void +Microsoft.TemplateEngine.Core.VariableCollection.TryGetValue(string! key, out object! value) -> bool +Microsoft.TemplateEngine.Core.VariableCollection.ValueRead -> Microsoft.TemplateEngine.Core.Contracts.ValueReadEventHander? +Microsoft.TemplateEngine.Core.VariableCollection.Values.get -> System.Collections.Generic.ICollection! +Microsoft.TemplateEngine.Core.VariableCollection.VariableCollection(Microsoft.TemplateEngine.Core.VariableCollection? parent) -> void +static Microsoft.TemplateEngine.Core.Expressions.Cpp.CppStyleEvaluatorDefinition.Evaluate(Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor, ref int bufferLength, ref int currentBufferPosition, out bool faulted) -> bool +static Microsoft.TemplateEngine.Core.Util.EngineConfig.DefaultLineEndings.get -> System.Collections.Generic.IReadOnlyList! +static Microsoft.TemplateEngine.Core.Util.EngineConfig.DefaultLineEndings.set -> void +static Microsoft.TemplateEngine.Core.Util.EngineConfig.DefaultWhitespaces.get -> System.Collections.Generic.IReadOnlyList! +static Microsoft.TemplateEngine.Core.Util.EngineConfig.DefaultWhitespaces.set -> void +static Microsoft.TemplateEngine.Core.VariableCollection.Root() -> Microsoft.TemplateEngine.Core.VariableCollection! +static Microsoft.TemplateEngine.Core.VariableCollection.Root(System.Collections.Generic.IDictionary! values) -> Microsoft.TemplateEngine.Core.VariableCollection! +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.ScopeBuilder.Build(ref int bufferLength, ref int bufferPosition, System.Action!>! onFault, System.Collections.Generic.HashSet? referencedVariablesKeys = null) -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable? +Microsoft.TemplateEngine.Core.Expressions.Token.Token(TToken family, object? value) -> void +Microsoft.TemplateEngine.Core.Expressions.Token.Value.get -> object? +Microsoft.TemplateEngine.Core.Operations.MarkupTokens.CloseCommentToken.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +Microsoft.TemplateEngine.Core.Operations.MarkupTokens.MarkupTokens(Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! openOpenElementToken, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! openCloseElementToken, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! closeElementTagToken, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! selfClosingElementEndToken, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! openConditionExpression, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! closeConditionExpression, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! openCommentToken, Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! closeCommentToken) -> void +Microsoft.TemplateEngine.Core.Operations.MarkupTokens.SelfClosingElementEndToken.get -> Microsoft.TemplateEngine.Core.Contracts.ITokenConfig! +static Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.Evaluate(Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor, ref int bufferLength, ref int currentBufferPosition, out bool faulted) -> bool +static Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.EvaluateFromString(Microsoft.Extensions.Logging.ILogger! logger, string! text, Microsoft.TemplateEngine.Abstractions.IVariableCollection! variables) -> bool +static Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.EvaluateFromString(Microsoft.Extensions.Logging.ILogger! logger, string! text, Microsoft.TemplateEngine.Abstractions.IVariableCollection! variables, out string? faultedMessage, System.Collections.Generic.HashSet? referencedVariablesKeys = null) -> bool +static Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.GetEvaluableExpression(Microsoft.Extensions.Logging.ILogger! logger, string! text, Microsoft.TemplateEngine.Abstractions.IVariableCollection! variables, out string? evaluableExpressionError, System.Collections.Generic.HashSet! referencedVariablesKeys) -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable? diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/PublicAPI.Unshipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.Core/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..dc58b37f7751 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/PublicAPI.Unshipped.txt @@ -0,0 +1,24 @@ +Microsoft.TemplateEngine.Core.Matching.OperationTerminal.Operation.get -> Microsoft.TemplateEngine.Core.Contracts.IOperation! +Microsoft.TemplateEngine.Core.Matching.OperationTerminal.OperationTerminal(Microsoft.TemplateEngine.Core.Contracts.IOperation! operation, int token, int tokenLength, int start = 0, int end = -1) -> void +Microsoft.TemplateEngine.Core.Matching.TerminalLocation +Microsoft.TemplateEngine.Core.Matching.TerminalLocation.Terminal.get -> T! +Microsoft.TemplateEngine.Core.Matching.TerminalLocation.TerminalLocation(T! terminal, int location) -> void +Microsoft.TemplateEngine.Core.Util.EngineConfig.EngineConfig(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.TemplateEngine.Abstractions.IVariableCollection! variables) -> void +Microsoft.TemplateEngine.Core.Util.EngineConfig.EngineConfig(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.TemplateEngine.Abstractions.IVariableCollection! variables, string! variableFormatString) -> void +Microsoft.TemplateEngine.Core.Util.EngineConfig.EngineConfig(Microsoft.Extensions.Logging.ILogger! logger, System.Collections.Generic.IReadOnlyList! whitespaces, System.Collections.Generic.IReadOnlyList! lineEndings, Microsoft.TemplateEngine.Abstractions.IVariableCollection! variables, string! variableFormatString = "{0}") -> void +Microsoft.TemplateEngine.Core.Util.EngineConfig.Variables.get -> Microsoft.TemplateEngine.Abstractions.IVariableCollection! +Microsoft.TemplateEngine.Core.Util.ProcessorState.SeekSourceForwardUntil(Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! match, ref int bufferLength, ref int currentBufferPosition, bool consumeToken = false) -> void +Microsoft.TemplateEngine.Core.Util.ProcessorState.SeekSourceForwardWhile(Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! trie, ref int bufferLength, ref int currentBufferPosition) -> void +Microsoft.TemplateEngine.Core.Util.ProcessorState.SeekTargetBackUntil(Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! match, bool consume = false) -> void +Microsoft.TemplateEngine.Core.Util.ProcessorState.SeekTargetBackWhile(Microsoft.TemplateEngine.Core.Contracts.ITokenTrie! match) -> void +Microsoft.TemplateEngine.Core.Util.ProcessorState.WriteToTarget(byte[]! buffer, int offset, int count) -> void +Microsoft.TemplateEngine.Core.VariableCollection.Parent.get -> Microsoft.TemplateEngine.Abstractions.IVariableCollection? +Microsoft.TemplateEngine.Core.VariableCollection.Parent.set -> void +Microsoft.TemplateEngine.Core.VariableCollection.VariableCollection(Microsoft.TemplateEngine.Abstractions.IVariableCollection? parent, System.Collections.Generic.IDictionary! values) -> void +static Microsoft.TemplateEngine.Core.CommonOperations.ConsumeWholeLine(this Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor, ref int bufferLength, ref int currentBufferPosition) -> void +static Microsoft.TemplateEngine.Core.CommonOperations.TrimWhitespace(this Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor, bool forward, bool backward, ref int bufferLength, ref int currentBufferPosition) -> void +static Microsoft.TemplateEngine.Core.CommonOperations.WhitespaceHandler(this Microsoft.TemplateEngine.Core.Contracts.IProcessorState! processor, ref int bufferLength, ref int currentBufferPosition, bool wholeLine = false, bool trim = false, bool trimForward = false, bool trimBackward = false) -> void +static Microsoft.TemplateEngine.Core.Expressions.Cpp.CppStyleEvaluatorDefinition.EvaluateFromString(Microsoft.Extensions.Logging.ILogger! logger, string! text, Microsoft.TemplateEngine.Abstractions.IVariableCollection! variables) -> bool +~static Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.EvaluateFromString(Microsoft.Extensions.Logging.ILogger logger, string text, Microsoft.TemplateEngine.Abstractions.IVariableCollection variables) -> bool +~static Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.EvaluateFromString(Microsoft.Extensions.Logging.ILogger logger, string text, Microsoft.TemplateEngine.Abstractions.IVariableCollection variables, out string faultedMessage, System.Collections.Generic.HashSet referencedVariablesKeys = null) -> bool +~static Microsoft.TemplateEngine.Core.Expressions.Shared.SharedEvaluatorDefinition.GetEvaluableExpression(Microsoft.Extensions.Logging.ILogger logger, string text, Microsoft.TemplateEngine.Abstractions.IVariableCollection variables, out string evaluableExpressionError, System.Collections.Generic.HashSet referencedVariablesKeys) -> Microsoft.TemplateEngine.Core.Expressions.IEvaluable \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/TokenConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/TokenConfig.cs new file mode 100644 index 000000000000..d2917c5b2ba5 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/TokenConfig.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core +{ + public class TokenConfig : ITokenConfig + { + private TokenConfig(string? after, string? value, string? before) + { + After = after; + Value = value; + Before = before; + } + + public string? After { get; } + + public string? Before { get; } + + public string? Value { get; } + + public static TokenConfig FromValue(string? value) + { + return new TokenConfig(null, value, null); + } + + public static IToken LiteralToken(byte[] data, int start = 0, int end = -1) + { + int realEnd = end != -1 ? end : (data.Length - start - 1); + return new Token(data, start, realEnd); + } + + public TokenConfig OnlyIfAfter(string? prefix) + { + return new TokenConfig(prefix, Value, Before); + } + + public TokenConfig OnlyIfBefore(string? suffix) + { + return new TokenConfig(After, Value, suffix); + } + + public IToken ToToken(Encoding encoding) + { + byte[] pre = string.IsNullOrEmpty(After) ? [] : encoding.GetBytes(After); + byte[] post = string.IsNullOrEmpty(Before) ? [] : encoding.GetBytes(Before); + byte[] core = string.IsNullOrEmpty(Value) ? [] : encoding.GetBytes(Value); + + byte[] buffer = new byte[pre.Length + core.Length + post.Length]; + + if (pre.Length > 0) + { + Buffer.BlockCopy(pre, 0, buffer, 0, pre.Length); + } + + if (core.Length > 0) + { + Buffer.BlockCopy(core, 0, buffer, pre.Length, core.Length); + } + + if (post.Length > 0) + { + Buffer.BlockCopy(post, 0, buffer, pre.Length + core.Length, post.Length); + } + + return new Token(buffer, pre.Length, buffer.Length - post.Length - 1); + } + + public override bool Equals(object obj) + { + return Equals((obj as ITokenConfig)!); + } + + public override int GetHashCode() + { + return (Before?.GetHashCode() ?? 0) ^ (After?.GetHashCode() ?? 0) ^ (Value?.GetHashCode() ?? 0); + } + + public bool Equals(ITokenConfig other) + { + return other != null && string.Equals(other.Before, Before, StringComparison.Ordinal) && string.Equals(other.After, After, StringComparison.Ordinal) && string.Equals(other.Value, Value, StringComparison.Ordinal); + } + + private class Token : IToken + { + public Token(byte[] value, int start, int end) + { + Value = value; + Start = start; + End = end; + Length = End - Start + 1; + } + + public byte[] Value { get; } + + public int Start { get; } + + public int End { get; } + + public int Length { get; } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/TokenConfigExtensions.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/TokenConfigExtensions.cs new file mode 100644 index 000000000000..06d4aec60b02 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/TokenConfigExtensions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core +{ + public static class TokenConfigExtensions + { + public static ITokenConfig TokenConfig(this string? s) + { + return Core.TokenConfig.FromValue(s); + } + + public static TokenConfig TokenConfigBuilder(this string? s) + { + return Core.TokenConfig.FromValue(s); + } + + public static IToken LiteralToken(this byte[] b, int start = 0, int end = -1) + { + return Core.TokenConfig.LiteralToken(b, start, end); + } + + public static IToken Token(this string s, Encoding e) + { + return s.TokenConfig().ToToken(e); + } + + public static IReadOnlyList TokenConfigs(this IEnumerable s) + { + List configs = new List(); + + foreach (string x in s) + { + configs.Add(x.TokenConfig()); + } + + return configs; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/CombinedStream.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/CombinedStream.cs new file mode 100644 index 000000000000..c6831aac35e9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/CombinedStream.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Util +{ + internal class CombinedStream : Stream + { + private readonly Stream _stream1; + private readonly Stream _stream2; + private readonly Action _reassign; + private bool _isStream1Depleted; + private bool _isReassigned; + + public CombinedStream(Stream stream1, Stream stream2, Action reassign) + { + _stream1 = stream1; + _stream2 = stream2; + _reassign = reassign; + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotImplementedException(); + + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (count + offset > buffer.Length) + { + //cannot read more than available buffer length + count = buffer.Length - offset; + } + + int totalBytesRead = 0; + if (!_isStream1Depleted) + { + while (totalBytesRead < count) + { + int bytesRead = _stream1.Read(buffer, offset + totalBytesRead, count - totalBytesRead); + if (bytesRead == 0) + { + break; + } + totalBytesRead += bytesRead; + } + if (totalBytesRead == count) + { + return totalBytesRead; + } + + _isStream1Depleted = true; + _stream1.Dispose(); + _reassign(_stream2); + _isReassigned = true; + } + + while (totalBytesRead < count) + { + int bytesRead = _stream2.Read(buffer, offset + totalBytesRead, count - totalBytesRead); + if (bytesRead == 0) + { + break; + } + totalBytesRead += bytesRead; + } + return totalBytesRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!_isReassigned) + { + _stream1.Dispose(); + _stream2.Dispose(); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/EncodingConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/EncodingConfig.cs new file mode 100644 index 000000000000..cac1f60a6827 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/EncodingConfig.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Util +{ + public class EncodingConfig : IEncodingConfig + { + private readonly Func[] _values; + private readonly List _variableKeys; + + public EncodingConfig(IEngineConfig config, Encoding encoding) + { + _variableKeys = new List(); + Encoding = encoding; + LineEndings = new TokenTrie(); + Whitespace = new TokenTrie(); + WhitespaceOrLineEnding = new TokenTrie(); + Variables = new TokenTrie(); + + foreach (string token in config.LineEndings) + { + IToken tokenBytes = token.Token(encoding); + LineEndings.AddToken(tokenBytes); + WhitespaceOrLineEnding.AddToken(tokenBytes); + } + + foreach (string token in config.Whitespaces) + { + IToken tokenBytes = token.Token(encoding); + Whitespace.AddToken(tokenBytes); + WhitespaceOrLineEnding.AddToken(tokenBytes); + } + + _values = new Func[config.Variables.Count]; + ExpandVariables(config, encoding); + } + + public Encoding Encoding { get; } + + public ITokenTrie LineEndings { get; } + + public IReadOnlyList VariableKeys => _variableKeys; + + public IReadOnlyList> VariableValues => _values; + + public ITokenTrie Variables { get; } + + public ITokenTrie Whitespace { get; } + + public ITokenTrie WhitespaceOrLineEnding { get; } + + public object this[int index] => _values[index](); + + private void ExpandVariables(IEngineConfig config, Encoding encoding) + { + foreach (var item in config.Variables) + { + string formattedKey = string.Format(config.VariableFormatString, item.Key); + IToken keyBytes = formattedKey.Token(encoding); + _variableKeys.Add(keyBytes); + _values[Variables.AddToken(keyBytes)] = () => item.Value; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/EncodingUtil.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/EncodingUtil.cs new file mode 100644 index 000000000000..8dada0419681 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/EncodingUtil.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.TemplateEngine.Core.Util +{ + public static class EncodingUtil + { + /// + /// Detects encoding of the stream. + /// + /// http://www.unicode.org/faq/utf_bom.html. + public static Encoding Detect(byte[] buffer, int currentBufferLength, out byte[] bom) + { + if (currentBufferLength == 0) + { + //File is zero length - pick something + bom = []; + return Encoding.UTF8; + } + + if (currentBufferLength >= 4) + { + if (buffer[0] == 0x00 && buffer[1] == 0x00 && buffer[2] == 0xFE && buffer[3] == 0xFF) + { + //Big endian UTF-32 + bom = new byte[] { 0x00, 0x00, 0xFE, 0xFF }; + return Encoding.GetEncoding(12001); + } + + if (buffer[0] == 0xFF && buffer[1] == 0xFE && buffer[2] == 0x00 && buffer[3] == 0x00) + { + //Little endian UTF-32 + bom = new byte[] { 0xFF, 0xFE, 0x00, 0x00 }; + return Encoding.UTF32; + } + } + + if (currentBufferLength >= 3) + { + if (buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF) + { + //UTF-8 + bom = new byte[] { 0xEF, 0xBB, 0xBF }; + return Encoding.UTF8; + } + } + + if (currentBufferLength >= 2) + { + if (buffer[0] == 0xFE && buffer[1] == 0xFF) + { + //Big endian UTF-16 + bom = new byte[] { 0xFE, 0xFF }; + return Encoding.BigEndianUnicode; + } + + if (buffer[0] == 0xFF && buffer[1] == 0xFE) + { + //Little endian UTF-16 + bom = new byte[] { 0xFF, 0xFE }; + return Encoding.Unicode; + } + } + + //Fallback to UTF-8 + bom = []; + return Encoding.UTF8; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/EngineConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/EngineConfig.cs new file mode 100644 index 000000000000..45683700d18a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/EngineConfig.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core.Util +{ + public class EngineConfig : IEngineConfig + { + public EngineConfig(ILogger logger, IVariableCollection variables) + : this(logger, DefaultWhitespaces, DefaultLineEndings, variables) + { + } + + public EngineConfig(ILogger logger, IVariableCollection variables, string variableFormatString) + : this(logger, DefaultWhitespaces, DefaultLineEndings, variables, variableFormatString) + { + } + + public EngineConfig(ILogger logger, IReadOnlyList whitespaces, IReadOnlyList lineEndings, IVariableCollection variables, string variableFormatString = "{0}") + { + Logger = logger; + Whitespaces = whitespaces; + LineEndings = lineEndings; + Variables = variables; + VariableFormatString = variableFormatString; + Flags = new Dictionary(); + } + + public static IReadOnlyList DefaultLineEndings { get; set; } = new[] { "\r", "\n", "\r\n" }; + + public static IReadOnlyList DefaultWhitespaces { get; set; } = new[] { " ", "\t" }; + + public IReadOnlyList LineEndings { get; } + + public string VariableFormatString { get; } + + public IVariableCollection Variables { get; } + + public IReadOnlyList Whitespaces { get; } + + public IDictionary Flags { get; } + + public ILogger Logger { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/Orchestrator.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/Orchestrator.cs new file mode 100644 index 000000000000..09afaae2647c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/Orchestrator.cs @@ -0,0 +1,300 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Core.Util +{ + public class Orchestrator : IOrchestrator + { + private readonly ILogger _logger; + private readonly IPhysicalFileSystem _fileSystem; + + public Orchestrator(ILogger logger, IPhysicalFileSystem fileSystem) + { + _logger = logger; + _fileSystem = fileSystem; + } + + public void Run(string runSpecPath, IDirectory sourceDir, string targetDir) + { + IGlobalRunSpec spec; + using (Stream stream = _fileSystem.OpenRead(runSpecPath)) + { + spec = RunSpecLoader(stream); + EngineConfig config = new EngineConfig(_logger, EngineConfig.DefaultWhitespaces, EngineConfig.DefaultLineEndings, spec.RootVariableCollection); + IProcessor processor = Processor.Create(config, spec.Operations); + stream.Position = 0; + using MemoryStream ms = new MemoryStream(); + processor.Run(stream, ms); + ms.Position = 0; + spec = RunSpecLoader(ms); + } + + RunInternal(sourceDir, targetDir, spec); + } + + public IReadOnlyList GetFileChanges(string runSpecPath, IDirectory sourceDir, string targetDir) + { + IGlobalRunSpec spec; + using (Stream stream = _fileSystem.OpenRead(runSpecPath)) + { + spec = RunSpecLoader(stream); + EngineConfig config = new EngineConfig(_logger, EngineConfig.DefaultWhitespaces, EngineConfig.DefaultLineEndings, spec.RootVariableCollection); + IProcessor processor = Processor.Create(config, spec.Operations); + stream.Position = 0; + using MemoryStream ms = new MemoryStream(); + processor.Run(stream, ms); + ms.Position = 0; + spec = RunSpecLoader(ms); + } + + return GetFileChangesInternal(sourceDir, targetDir, spec); + } + + public void Run(IGlobalRunSpec spec, IDirectory sourceDir, string targetDir) + { + RunInternal(sourceDir, targetDir, spec); + } + + public IReadOnlyList GetFileChanges(IGlobalRunSpec spec, IDirectory sourceDir, string targetDir) + { + return GetFileChangesInternal(sourceDir, targetDir, spec); + } + + protected virtual IGlobalRunSpec RunSpecLoader(Stream runSpec) + { + throw new NotImplementedException(); + } + + protected virtual bool TryGetBufferSize(IFile sourceFile, out int bufferSize) + { + bufferSize = -1; + return false; + } + + protected virtual bool TryGetFlushThreshold(IFile sourceFile, out int threshold) + { + threshold = -1; + return false; + } + + private static List> CreateFileGlobProcessors(ILogger logger, IGlobalRunSpec spec) + { + List> processorList = new List>(); + + if (spec.Special != null) + { + foreach (KeyValuePair runSpec in spec.Special) + { + IReadOnlyList operations = runSpec.Value.GetOperations(spec.Operations); + EngineConfig config = new EngineConfig(logger, EngineConfig.DefaultWhitespaces, EngineConfig.DefaultLineEndings, spec.RootVariableCollection, runSpec.Value.VariableFormatString); + IProcessor processor = Processor.Create(config, operations); + + processorList.Add(new KeyValuePair(runSpec.Key, processor)); + } + } + + return processorList; + } + + private static string CreateTargetDir(IPhysicalFileSystem fileSystem, string sourceRel, string targetDir, IGlobalRunSpec spec) + { + if (!spec.TryGetTargetRelPath(sourceRel, out string targetRel)) + { + targetRel = sourceRel; + } + + string targetPath = Path.Combine(targetDir, targetRel); + string fullTargetDir = Path.GetDirectoryName(targetPath); + fileSystem.CreateDirectory(fullTargetDir); + + return targetPath; + } + + private IReadOnlyList GetFileChangesInternal(IDirectory sourceDir, string targetDir, IGlobalRunSpec spec) + { + List changes = new List(); + foreach (IFile file in sourceDir.EnumerateFiles("*", SearchOption.AllDirectories)) + { + string sourceRel = file.PathRelativeTo(sourceDir); + string fileName = Path.GetFileName(sourceRel); + + // The placeholder file should never get copied / created / processed. It just causes the dir to get created if needed. + // The change checking / reporting is different, setting this variable tracks it. + bool checkingDirWithPlaceholderFile = spec.IgnoreFileNames.Contains(fileName); + + foreach (IPathMatcher include in spec.Include) + { + if (include.IsMatch(sourceRel)) + { + bool excluded = false; + foreach (IPathMatcher exclude in spec.Exclude) + { + if (exclude.IsMatch(sourceRel)) + { + excluded = true; + break; + } + } + + if (!excluded) + { + if (!spec.TryGetTargetRelPath(sourceRel, out string targetRel)) + { + targetRel = sourceRel; + } + + string targetPath = Path.Combine(targetDir, targetRel); + + if (checkingDirWithPlaceholderFile) + { + targetPath = Path.GetDirectoryName(targetPath); + targetRel = Path.GetDirectoryName(targetRel); + + if (_fileSystem.DirectoryExists(targetPath)) + { + changes.Add(new FileChange(sourceRel, targetRel, ChangeKind.Overwrite)); + } + else + { + changes.Add(new FileChange(sourceRel, targetRel, ChangeKind.Create)); + } + } + else if (_fileSystem.FileExists(targetPath)) + { + changes.Add(new FileChange(sourceRel, targetRel, ChangeKind.Overwrite)); + } + else + { + changes.Add(new FileChange(sourceRel, targetRel, ChangeKind.Create)); + } + } + + break; + } + } + } + + return changes; + } + + private void RunInternal(IDirectory sourceDir, string targetDir, IGlobalRunSpec spec) + { + EngineConfig cfg = new EngineConfig(_logger, EngineConfig.DefaultWhitespaces, EngineConfig.DefaultLineEndings, spec.RootVariableCollection); + IProcessor fallback = Processor.Create(cfg, spec.Operations); + + List> fileGlobProcessors = CreateFileGlobProcessors(_logger, spec); + + foreach (IFile file in sourceDir.EnumerateFiles("*", SearchOption.AllDirectories)) + { + string sourceRel = file.PathRelativeTo(sourceDir); + string fileName = Path.GetFileName(sourceRel); + + // The placeholder file should never get copied / created / processed. It just causes the dir to get created if needed. + // The change checking / reporting is different, setting this variable tracks it. + bool checkingDirWithPlaceholderFile = spec.IgnoreFileNames.Contains(fileName); + + foreach (IPathMatcher include in spec.Include) + { + if (include.IsMatch(sourceRel)) + { + bool excluded = false; + foreach (IPathMatcher exclude in spec.Exclude) + { + if (exclude.IsMatch(sourceRel)) + { + excluded = true; + break; + } + } + + if (!excluded) + { + bool copy = false; + foreach (IPathMatcher copyOnly in spec.CopyOnly) + { + if (copyOnly.IsMatch(sourceRel)) + { + copy = true; + break; + } + } + + if (checkingDirWithPlaceholderFile) + { + CreateTargetDir(_fileSystem, sourceRel, targetDir, spec); + } + else if (!copy) + { + ProcessFile(file, sourceRel, targetDir, spec, fallback, fileGlobProcessors); + } + else + { + string targetPath = CreateTargetDir(_fileSystem, sourceRel, targetDir, spec); + + using Stream sourceStream = file.OpenRead(); + using Stream targetStream = _fileSystem.CreateFile(targetPath); + sourceStream.CopyTo(targetStream); + } + } + + break; + } + } + } + } + + private void ProcessFile(IFile sourceFile, string sourceRel, string targetDir, IGlobalRunSpec spec, IProcessor fallback, IEnumerable> fileGlobProcessors) + { + IProcessor runner = (fileGlobProcessors.FirstOrDefault(x => x.Key.IsMatch(sourceRel)).Value ?? fallback) + ?? throw new InvalidOperationException("At least one of [runner] or [fallback] cannot be null"); + if (!spec.TryGetTargetRelPath(sourceRel, out string targetRel)) + { + targetRel = sourceRel; + } + + string targetPath = Path.Combine(targetDir, targetRel); + //TODO: Update context with the current file & such here + + bool customBufferSize = TryGetBufferSize(sourceFile, out int bufferSize); + bool customFlushThreshold = TryGetFlushThreshold(sourceFile, out int flushThreshold); + string fullTargetDir = Path.GetDirectoryName(targetPath); + _fileSystem.CreateDirectory(fullTargetDir); + + try + { + using Stream source = sourceFile.OpenRead(); + using Stream target = _fileSystem.CreateFile(targetPath); + if (!customBufferSize) + { + runner.Run(source, target); + } + else + { + if (!customFlushThreshold) + { + runner.Run(source, target, bufferSize); + } + else + { + runner.Run(source, target, bufferSize, flushThreshold); + } + } + } + catch (TemplateAuthoringException ex) + { + throw new TemplateAuthoringException($"Template authoring error encountered while processing file {sourceFile.FullPath}: {ex.Message}", ex.ConfigItem, ex); + } + catch (Exception ex) + { + throw new ContentGenerationException($"Error while processing file {sourceFile.FullPath}", ex); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/Processor.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/Processor.cs new file mode 100644 index 000000000000..312462926f19 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/Processor.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Core.Util +{ + public class Processor : IProcessor + { + private const int DefaultBufferSize = 8 * 1024 * 1024; + private const int DefaultFlushThreshold = 8 * 1024 * 1024; + private readonly IReadOnlyList _operations; + + private Processor(EngineConfig config, IReadOnlyList operations) + { + Config = config; + _operations = operations; + } + + public EngineConfig Config { get; } + + public static IProcessor Create(EngineConfig config, params IOperationProvider[] operations) + { + return new Processor(config, operations); + } + + public static IProcessor Create(EngineConfig config, IReadOnlyList operations) + { + return new Processor(config, operations); + } + + public IProcessor CloneAndAppendOperations(IReadOnlyList tempOperations) + { + return new Processor(Config, new CombinedList(_operations, tempOperations)); + } + + public bool Run(Stream source, Stream target) => Run(source, target, DefaultBufferSize); + + public bool Run(Stream source, Stream target, int bufferSize) => Run(source, target, bufferSize, DefaultFlushThreshold); + + public bool Run(Stream source, Stream target, int bufferSize, int flushThreshold) + { + ProcessorState state = new ProcessorState(source, target, bufferSize, flushThreshold, Config, _operations); + return state.Run(); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/ProcessorState.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/ProcessorState.cs new file mode 100644 index 000000000000..7a6371d9b310 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/ProcessorState.cs @@ -0,0 +1,550 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Matching; + +namespace Microsoft.TemplateEngine.Core.Util +{ + public class ProcessorState : IProcessorState + { + private static readonly ConcurrentDictionary, ConcurrentDictionary>> TrieLookup = new(); + private static readonly ConcurrentDictionary, List> OperationsToExplicitlySetOnByDefault = new(); + private readonly StreamProxy _target; + private readonly TrieEvaluator _trie; + private readonly int _flushThreshold; + private readonly int _bomSize; + private Stream _source; + + public ProcessorState(Stream source, Stream target, int bufferSize, int flushThreshold, IEngineConfig config, IReadOnlyList operationProviders) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (operationProviders == null) + { + throw new ArgumentNullException(nameof(operationProviders)); + } + + if (source.CanSeek) + { + try + { + if (source.Length < bufferSize) + { + bufferSize = (int)source.Length; + } + } + catch + { + //The stream may not support getting the length property (in NetworkStream for instance, which throw a NotSupportedException), suppress any errors in + // accessing the property and continue with the specified buffer size + } + } + //Buffer has to be at least as large as the largest BOM we could expect + else if (bufferSize < 4) + { + bufferSize = 4; + } + + _source = source; + _target = new StreamProxy(target, bufferSize); + Config = config ?? throw new ArgumentNullException(nameof(config)); + _flushThreshold = flushThreshold; + CurrentBuffer = new byte[bufferSize]; + CurrentBufferLength = ReadExactBytes(source, CurrentBuffer, 0, CurrentBuffer.Length); + + Encoding encoding = EncodingUtil.Detect(CurrentBuffer, CurrentBufferLength, out byte[] bom); + EncodingConfig = new EncodingConfig(Config, encoding); + _bomSize = bom.Length; + CurrentBufferPosition = _bomSize; + CurrentSequenceNumber = _bomSize; + WriteToTarget(bom, 0, _bomSize); + + bool explicitOnConfigurationRequired = false; + ConcurrentDictionary> byEncoding = TrieLookup.GetOrAdd(operationProviders, x => new()); + List turnOnByDefault = OperationsToExplicitlySetOnByDefault.GetOrAdd(operationProviders, x => + { + explicitOnConfigurationRequired = true; + return new List(); + }); + + if (!byEncoding.TryGetValue(encoding, out Trie trie)) + { + trie = new Trie(); + + for (int i = 0; i < operationProviders.Count; ++i) + { + IOperation op = operationProviders[i].GetOperation(encoding, this); + + if (op != null) + { + for (int j = 0; j < op.Tokens.Count; ++j) + { + if (op.Tokens[j] != null) + { + trie.AddPath(op.Tokens[j]!.Value, new OperationTerminal(op, j, op.Tokens[j]!.Length, op.Tokens[j]!.Start, op.Tokens[j]!.End)); + } + } + + if (explicitOnConfigurationRequired && op.IsInitialStateOn && !string.IsNullOrEmpty(op.Id)) + { + turnOnByDefault.Add(op.Id!); + } + } + } + + byEncoding.TryAdd(encoding, trie); + } + + foreach (string state in turnOnByDefault) + { + config.Flags[state] = true; + } + + _trie = new TrieEvaluator(trie); + + if (bufferSize < _trie.MaxLength + 1) + { + byte[] tmp = new byte[_trie.MaxLength + 1]; + Buffer.BlockCopy(CurrentBuffer, CurrentBufferPosition, tmp, 0, CurrentBufferLength - CurrentBufferPosition); + int nRead = ReadExactBytes(_source, tmp, CurrentBufferLength - CurrentBufferPosition, tmp.Length - CurrentBufferLength); + CurrentBuffer = tmp; + CurrentBufferLength += nRead - _bomSize; + CurrentBufferPosition = 0; + CurrentSequenceNumber = 0; + } + } + + public IEngineConfig Config { get; } + + public byte[] CurrentBuffer { get; } + + public int CurrentBufferLength { get; private set; } + + public int CurrentBufferPosition { get; private set; } + + public int CurrentSequenceNumber { get; private set; } + + public IEncodingConfig EncodingConfig { get; } + + public Encoding Encoding => EncodingConfig.Encoding; + + public bool AdvanceBuffer(int bufferPosition) + { + if (CurrentBufferLength == 0 || bufferPosition == 0) + { + return false; + } + + //The number of bytes away from the current buffer position being + // retargeted to the buffer head + int netMove = bufferPosition - CurrentBufferPosition; + //Since the CurrentSequenceNumber and CurrentBufferPosition are + // different mappings over the same value, the same net move + // applies to the current sequence number + CurrentSequenceNumber += netMove; + //Calculate the number of bytes at the end of the buffer that + // should be preserved + int bytesToPreserveInBuffer = CurrentBufferLength - bufferPosition; + + if (CurrentBufferLength < CurrentBuffer.Length && bytesToPreserveInBuffer == 0) + { + CurrentBufferLength = 0; + CurrentBufferPosition = 0; + return false; + } + + //If we actually have to preserve any data, shift it to the start + if (bytesToPreserveInBuffer > 0) + { + //Shift the relevant number of bytes back to the head of the buffer + Buffer.BlockCopy(CurrentBuffer, bufferPosition, CurrentBuffer, 0, bytesToPreserveInBuffer); + } + + //Fill the remaining spaces in the buffer with new data, save how + // many we've read for recalculating the new effective buffer size + int nRead = ReadExactBytes(_source, CurrentBuffer, bytesToPreserveInBuffer, CurrentBufferLength - bytesToPreserveInBuffer); + CurrentBufferLength = bytesToPreserveInBuffer + nRead; + + //The new buffer position is set to point at the byte that buffer + // position pointed at (which is now at the head of the buffer) + CurrentBufferPosition = 0; + + return true; + } + + public bool Run() + { + int nextSequenceNumberThatCouldBeWritten = CurrentSequenceNumber; + int bytesWrittenSinceLastFlush = 0; + bool anyOperationsExecuted = false; + + while (true) + { + //Loop until we run out of data in the buffer + while (CurrentBufferPosition < CurrentBufferLength) + { + int posedPosition = CurrentSequenceNumber; + bool skipAdvanceBuffer = false; + if (_trie.Accept(CurrentBuffer[CurrentBufferPosition], ref posedPosition, out TerminalLocation? terminal)) + { + IOperation operation = terminal!.Terminal.Operation; + int matchLength = terminal.Terminal.End - terminal.Terminal.Start + 1; + int handoffBufferPosition = CurrentBufferPosition + matchLength - (CurrentSequenceNumber - terminal.Location); + + if (terminal.Location > nextSequenceNumberThatCouldBeWritten) + { + int toWrite = terminal.Location - nextSequenceNumberThatCouldBeWritten; + //Console.WriteLine("UnmatchedBlock"); + //string text = System.Text.Encoding.UTF8.GetString(CurrentBuffer, handoffBufferPosition - toWrite - matchLength, toWrite).Replace("\0", "\\0"); + //Console.WriteLine(text); + _target.Write(CurrentBuffer, handoffBufferPosition - toWrite - matchLength, toWrite); + bytesWrittenSinceLastFlush += toWrite; + nextSequenceNumberThatCouldBeWritten = posedPosition - matchLength + 1; + } + + if (operation.Id == null || (Config.Flags.TryGetValue(operation.Id, out bool opEnabledFlag) && opEnabledFlag)) + { + CurrentSequenceNumber += handoffBufferPosition - CurrentBufferPosition; + CurrentBufferPosition = handoffBufferPosition; + posedPosition = handoffBufferPosition; + int bytesWritten = operation.HandleMatch(this, CurrentBufferLength, ref posedPosition, terminal.Terminal.Token); + bytesWrittenSinceLastFlush += bytesWritten; + + CurrentSequenceNumber += posedPosition - CurrentBufferPosition; + CurrentBufferPosition = posedPosition; + nextSequenceNumberThatCouldBeWritten = CurrentSequenceNumber; + skipAdvanceBuffer = true; + anyOperationsExecuted = true; + } + else + { + int oldSequenceNumber = CurrentSequenceNumber; + CurrentSequenceNumber = terminal.Location + terminal.Terminal.End + 1; + CurrentBufferPosition += CurrentSequenceNumber - oldSequenceNumber; + } + + if (bytesWrittenSinceLastFlush >= _flushThreshold) + { + _target.Flush(); + bytesWrittenSinceLastFlush = 0; + } + } + + if (!skipAdvanceBuffer) + { + ++CurrentSequenceNumber; + ++CurrentBufferPosition; + } + } + + //Calculate the sequence number at the head of the buffer + int headSequenceNumber = CurrentSequenceNumber - CurrentBufferPosition; + + int bufferPositionToAdvanceTo; + if (headSequenceNumber > _trie.OldestRequiredSequenceNumber) + { + // if headSequenceNumber is higher than _trie.OldestRequiredSequenceNumber + // the window is already missed + // we won't be able to continue with current tries anyway + // advance to new chunk of the buffer. + bufferPositionToAdvanceTo = CurrentBufferLength; + } + else + { + bufferPositionToAdvanceTo = _trie.OldestRequiredSequenceNumber - headSequenceNumber; + } + int numberOfUncommittedBytesBeforeThePositionToAdvanceTo = _trie.OldestRequiredSequenceNumber - nextSequenceNumberThatCouldBeWritten; + + //If we'd advance data out of the buffer that hasn't been + // handled already, write it out + if (numberOfUncommittedBytesBeforeThePositionToAdvanceTo > 0) + { + int toWrite = numberOfUncommittedBytesBeforeThePositionToAdvanceTo; + // Console.WriteLine("AdvancePreserve"); + // Console.WriteLine($"nextSequenceNumberThatCouldBeWritten {nextSequenceNumberThatCouldBeWritten}"); + // Console.WriteLine($"headSequenceNumber {headSequenceNumber}"); + // Console.WriteLine($"bufferPositionToAdvanceTo {bufferPositionToAdvanceTo}"); + // Console.WriteLine($"numberOfUncommittedBytesBeforeThePositionToAdvanceTo {numberOfUncommittedBytesBeforeThePositionToAdvanceTo}"); + // Console.WriteLine($"CurrentBufferPosition {CurrentBufferPosition}"); + // Console.WriteLine($"CurrentBufferLength {CurrentBufferLength}"); + // Console.WriteLine($"CurrentBuffer.Length {CurrentBuffer.Length}"); + // string text = System.Text.Encoding.UTF8.GetString(CurrentBuffer, bufferPositionToAdvanceTo - toWrite, toWrite).Replace("\0", "\\0"); + // Console.WriteLine(text); + _target.Write(CurrentBuffer, bufferPositionToAdvanceTo - toWrite, toWrite); + bytesWrittenSinceLastFlush += toWrite; + nextSequenceNumberThatCouldBeWritten = _trie.OldestRequiredSequenceNumber; + } + + //We ran out of data in the buffer, so attempt to advance + // if we fail, + if (!AdvanceBuffer(bufferPositionToAdvanceTo)) + { + int posedPosition = CurrentSequenceNumber; + _trie.FinalizeMatchesInProgress(ref posedPosition, out TerminalLocation? terminal); + + while (terminal != null) + { + IOperation operation = terminal.Terminal.Operation; + int matchLength = terminal.Terminal.End - terminal.Terminal.Start + 1; + int handoffBufferPosition = CurrentBufferPosition + matchLength - (CurrentSequenceNumber - terminal.Location); + + if (terminal.Location > nextSequenceNumberThatCouldBeWritten) + { + int toWrite = terminal.Location - nextSequenceNumberThatCouldBeWritten; + // Console.WriteLine("TailUnmatchedBlock"); + // string text = System.Text.Encoding.UTF8.GetString(CurrentBuffer, handoffBufferPosition - toWrite - matchLength, toWrite).Replace("\0", "\\0"); + // Console.WriteLine(text); + _target.Write(CurrentBuffer, handoffBufferPosition - toWrite - matchLength, toWrite); + bytesWrittenSinceLastFlush += toWrite; + nextSequenceNumberThatCouldBeWritten = terminal.Location; + } + + if (operation.Id == null || (Config.Flags.TryGetValue(operation.Id, out bool opEnabledFlag) && opEnabledFlag)) + { + CurrentSequenceNumber += handoffBufferPosition - CurrentBufferPosition; + CurrentBufferPosition = handoffBufferPosition; + posedPosition = handoffBufferPosition; + int bytesWritten = operation.HandleMatch(this, CurrentBufferLength, ref posedPosition, terminal.Terminal.Token); + bytesWrittenSinceLastFlush += bytesWritten; + + CurrentSequenceNumber += posedPosition - CurrentBufferPosition; + CurrentBufferPosition = posedPosition; + nextSequenceNumberThatCouldBeWritten = CurrentSequenceNumber; + anyOperationsExecuted = true; + } + else + { + int oldSequenceNumber = CurrentSequenceNumber; + CurrentSequenceNumber = terminal.Location + terminal.Terminal.End + 1; + CurrentBufferPosition += CurrentSequenceNumber - oldSequenceNumber; + } + + _trie.FinalizeMatchesInProgress(ref posedPosition, out terminal); + } + + break; + } + } + + int endSequenceNumber = CurrentSequenceNumber - CurrentBufferPosition + CurrentBufferLength; + if (endSequenceNumber > nextSequenceNumberThatCouldBeWritten) + { + int toWrite = endSequenceNumber - nextSequenceNumberThatCouldBeWritten; + // Console.WriteLine("LastBlock"); + // string text = System.Text.Encoding.UTF8.GetString(CurrentBuffer, CurrentBufferLength - toWrite, toWrite).Replace("\0", "\\0"); + // Console.WriteLine(text); + _target.Write(CurrentBuffer, CurrentBufferLength - toWrite, toWrite); + } + + _target.FlushToTarget(); + return anyOperationsExecuted; + } + + public void SeekTargetBackUntil(ITokenTrie match, bool consume = false) + { + byte[] buffer = new byte[match.MaxLength]; + while (_target.Position > _bomSize) + { + if (_target.Position - _bomSize < buffer.Length) + { + _target.Position = _bomSize; + } + else + { + _target.Position -= buffer.Length; + } + + int nRead = ReadExactBytes(_target, buffer, 0, buffer.Length); + + int best = -1; + int bestPos = -1; + for (int i = nRead - match.MinLength; i >= 0; --i) + { + int ic = i; + if (match.GetOperation(buffer, nRead, ref ic, out int token) && ic >= bestPos) + { + bestPos = ic; + best = token; + } + } + + if (best != -1) + { + _target.Position -= nRead - bestPos + (consume ? match.TokenLength[best] : 0); + _target.SetLength(_target.Position); + return; + } + + //Back up the amount we already read to get a new window of data in + if (_target.Position - _bomSize < buffer.Length) + { + _target.Position = _bomSize; + } + else + { + _target.Position -= buffer.Length; + } + } + + if (_target.Position == _bomSize) + { + _target.SetLength(_bomSize); + } + } + + public void SeekTargetBackWhile(ITokenTrie match) + { + byte[] buffer = new byte[match.MaxLength]; + while (_target.Position > _bomSize) + { + if (_target.Position - _bomSize < buffer.Length) + { + _target.Position = _bomSize; + } + else + { + _target.Position -= buffer.Length; + } + + int nRead = ReadExactBytes(_target, buffer, 0, buffer.Length); + bool anyMatch = false; + int token = -1; + int i = nRead - match.MinLength; + + for (; i >= 0; --i) + { + if (match.GetOperation(buffer, nRead, ref i, out token)) + { + i -= match.TokenLength[token]; + anyMatch = true; + break; + } + } + + if (!anyMatch || (token != -1 && i + match.TokenLength[token] != nRead)) + { + _target.SetLength(_target.Position); + return; + } + + //Back up the amount we already read to get a new window of data in + if (_target.Position - _bomSize < buffer.Length) + { + _target.Position = _bomSize; + } + else + { + _target.Position -= buffer.Length; + } + } + + if (_target.Position == _bomSize) + { + _target.SetLength(_bomSize); + } + } + + public void WriteToTarget(byte[] buffer, int offset, int count) => _target.Write(buffer, offset, count); + + public void SeekSourceForwardUntil(ITokenTrie match, ref int bufferLength, ref int currentBufferPosition, bool consumeToken = false) + { + while (bufferLength >= match.MinLength) + { + //Try to get at least the max length of the tree into the buffer + if (bufferLength - currentBufferPosition < match.MaxLength) + { + AdvanceBuffer(currentBufferPosition); + currentBufferPosition = CurrentBufferPosition; + bufferLength = CurrentBufferLength; + } + + int sz = bufferLength == CurrentBuffer.Length ? match.MaxLength : match.MinLength; + + for (; currentBufferPosition < bufferLength - sz + 1; ++currentBufferPosition) + { + if (bufferLength == 0) + { + currentBufferPosition = 0; + return; + } + + if (match.GetOperation(CurrentBuffer, bufferLength, ref currentBufferPosition, false, out int token)) + { + if (!consumeToken) + { + currentBufferPosition -= match.Tokens[token].Length; + } + + return; + } + } + } + + //Ran out of places to check and haven't reached the actual match, consume all the way to the end + currentBufferPosition = bufferLength; + } + + public void SeekSourceForwardWhile(ITokenTrie trie, ref int bufferLength, ref int currentBufferPosition) + { + while (bufferLength > trie.MinLength) + { + while (currentBufferPosition < bufferLength - trie.MinLength + 1) + { + if (bufferLength == 0) + { + currentBufferPosition = 0; + return; + } + + if (!trie.GetOperation(CurrentBuffer, bufferLength, ref currentBufferPosition, out _)) + { + return; + } + } + + AdvanceBuffer(currentBufferPosition); + currentBufferPosition = CurrentBufferPosition; + bufferLength = CurrentBufferLength; + } + } + + public void Inject(Stream staged) + { + _source = new CombinedStream(staged, _source, inner => _source = inner); + CurrentBufferLength = ReadExactBytes(_source, CurrentBuffer, 0, CurrentBufferLength); + CurrentBufferPosition = 0; + } + + private int ReadExactBytes(Stream stream, byte[] buffer, int offset, int count) + { + if (count + offset > buffer.Length) + { + //cannot read more than available buffer length + count = buffer.Length - offset; + } + int totalRead = 0; + while (totalRead < count) + { + int bytesRead = stream.Read(buffer, totalRead + offset, count - totalRead); + if (bytesRead == 0) + { + return totalRead; + } + totalRead += bytesRead; + } + return totalRead; + } + } +} + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/StreamProxy.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/StreamProxy.cs new file mode 100644 index 000000000000..44c68e1513e1 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/StreamProxy.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.Util +{ + /// + /// Simple implementation of stream proxy to be used in case where destination stream is being re-read and adjusted in place multiple times. + /// Direct I/O usage is unnecessarily costly in those situations (even with BCL buffered streams). + /// This implementation flips to destination stream as soon as the cumulative size of the stream exceeds fixed threshold. + /// + internal class StreamProxy : Stream + { + /// + /// Upper limit of a sane size of a source file of good factored new codebase. + /// + public const int MaxRecommendedBufferedFileSize = 100 * 1024; + + private readonly Stream _targetStream; + private readonly Stream? _memoryStream; + private readonly int _maximumMemoryWindowSize; + private Stream _currentTargetStream; + + public StreamProxy(Stream underlyingStream, int initialSize) + : this(underlyingStream, initialSize, MaxRecommendedBufferedFileSize) + { } + + public StreamProxy(Stream underlyingStream, int initialSize, int maximumMemoryWindowSize) + { + this._maximumMemoryWindowSize = maximumMemoryWindowSize; + this._targetStream = underlyingStream; + + if (initialSize > maximumMemoryWindowSize) + { + _currentTargetStream = _targetStream; + } + else + { + _memoryStream = new MemoryStream(initialSize); + _currentTargetStream = _memoryStream; + } + } + + public override bool CanRead => _currentTargetStream.CanRead; + + public override bool CanSeek => _currentTargetStream.CanSeek; + + public override bool CanWrite => _currentTargetStream.CanWrite; + + public override long Length => _currentTargetStream.Length; + + public override long Position + { + get => _currentTargetStream.Position; + + set => _currentTargetStream.Position = value; + } + + public override void Flush() => _currentTargetStream.Flush(); + + public override long Seek(long offset, SeekOrigin origin) => _currentTargetStream.Seek(offset, origin); + + public override void SetLength(long value) + { + CheckStreamExpectedSize(value); + + _currentTargetStream.SetLength(value); + } + + public override int Read(byte[] buffer, int offset, int count) => _currentTargetStream.Read(buffer, offset, count); + + public override void Write(byte[] buffer, int offset, int count) + { + CheckStreamExpectedSize(this.Position + count); + + _currentTargetStream.Write(buffer, offset, count); + } + + public void FlushToTarget() + { + CopyMemoryToTargetStream(); + + _currentTargetStream.Flush(); + } + + private void CheckStreamExpectedSize(long newExpectedSize) + { + if (newExpectedSize > _maximumMemoryWindowSize && _currentTargetStream == _memoryStream) + { + CopyMemoryToTargetStream(); + } + } + + private void CopyMemoryToTargetStream() + { + if (_currentTargetStream != _memoryStream) + { + return; + } + + long tempPosition = _memoryStream.Position; + _memoryStream.Flush(); + _memoryStream.Position = 0; + _targetStream.Position = 0; + _memoryStream.CopyTo(_targetStream); + _targetStream.Position = tempPosition; + _memoryStream.Dispose(); + + _currentTargetStream = _targetStream; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/Token.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/Token.cs new file mode 100644 index 000000000000..82866222457f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/Token.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Matching; + +namespace Microsoft.TemplateEngine.Core.Util +{ + public class Token : TerminalBase + { + public Token(byte[] token, int index, int start = 0, int end = -1) + : base(token.Length, start, end) + { + Value = token; + Index = index; + } + + public byte[] Value { get; } + + public int Index { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/TokenTrie.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/TokenTrie.cs new file mode 100644 index 000000000000..34874ff3afe6 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/TokenTrie.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Matching; + +namespace Microsoft.TemplateEngine.Core.Util +{ + public class TokenTrie : Trie, ITokenTrie + { + private readonly List _tokens = new List(); + private readonly List _lengths = new List(); + + public int Count => _tokens.Count; + + public int MaxLength { get; private set; } + + public int MinLength { get; private set; } = int.MaxValue; + + public IReadOnlyList TokenLength => _lengths; + + public IReadOnlyList Tokens => _tokens; + + public int AddToken(byte[] literalToken) + { + return AddToken(TokenConfig.LiteralToken(literalToken)); + } + + public void AddToken(byte[] literalToken, int index) + { + AddToken(TokenConfig.LiteralToken(literalToken), index); + } + + public int AddToken(IToken token) + { + int count = _tokens.Count; + AddToken(token, count); + return count; + } + + public void AddToken(IToken? token, int index) + { + _tokens.Add(token!); + _lengths.Add(token!.Length); + Token t = new Token(token.Value, index, token.Start, token.End); + AddPath(token.Value, t); + + if (token.Value.Length > MaxLength) + { + MaxLength = token.Value.Length; + } + + if (token.Value.Length < MinLength) + { + MinLength = token.Value.Length; + } + } + + public void Append(ITokenTrie trie) + { + foreach (IToken token in trie.Tokens) + { + AddToken(token); + } + } + + public ITokenTrieEvaluator CreateEvaluator() + { + return new TokenTrieEvaluator(this); + } + + public bool GetOperation(byte[] buffer, int bufferLength, ref int currentBufferPosition, out int token) + { + return GetOperation(buffer, bufferLength, ref currentBufferPosition, true, out token); + } + + public bool GetOperation(byte[] buffer, int bufferLength, ref int currentBufferPosition, bool mustMatchPosition, out int token) + { + int originalPosition = currentBufferPosition; + TrieEvaluator evaluator = new TrieEvaluator(this); + TrieEvaluationDriver driver = new TrieEvaluationDriver(evaluator); + + if (mustMatchPosition) + { + bufferLength = Math.Min(bufferLength, currentBufferPosition + MaxLength); + } + + TerminalLocation? location = driver.Evaluate(buffer, bufferLength, true, 0, ref currentBufferPosition); + + if (location != null && (!mustMatchPosition || (currentBufferPosition - location.Terminal.Length == originalPosition))) + { + token = location.Terminal.Index; + currentBufferPosition = location.Location + location.Terminal.End - location.Terminal.Start + 1; + return true; + } + + currentBufferPosition = mustMatchPosition ? originalPosition : bufferLength - MaxLength + 1; + + token = -1; + return false; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/TokenTrieEvaluator.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/TokenTrieEvaluator.cs new file mode 100644 index 000000000000..b47c367e9474 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/Util/TokenTrieEvaluator.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Matching; + +namespace Microsoft.TemplateEngine.Core.Util +{ + public class TokenTrieEvaluator : TrieEvaluator, ITokenTrieEvaluator + { + private int _currentSequenceNumber; + + public TokenTrieEvaluator(Trie trie) + : base(trie) + { + } + + public int BytesToKeepInBuffer => _currentSequenceNumber - OldestRequiredSequenceNumber + 1; + + public bool Accept(byte data, ref int bufferPosition, out int token) + { + ++_currentSequenceNumber; + if (Accept(data, ref _currentSequenceNumber, out TerminalLocation? terminal)) + { + token = terminal!.Terminal.Index; + bufferPosition += _currentSequenceNumber - terminal.Location - terminal.Terminal.End; + return true; + } + + token = -1; + return false; + } + + public bool TryFinalizeMatchesInProgress(ref int bufferPosition, out int token) + { + FinalizeMatchesInProgress(ref _currentSequenceNumber, out TerminalLocation? terminal); + + if (terminal != null) + { + token = terminal.Terminal.Index; + bufferPosition += _currentSequenceNumber - terminal.Location - terminal.Terminal.End; + return true; + } + + token = -1; + return false; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/ValueReadEventArgs.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/ValueReadEventArgs.cs new file mode 100644 index 000000000000..2eaefe844258 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/ValueReadEventArgs.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core +{ + public class ValueReadEventArgs : EventArgs, IValueReadEventArgs + { + public ValueReadEventArgs(string key, object value) + { + Key = key; + Value = value; + } + + public string Key { get; } + + public object Value { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/VariableCollection.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Core/VariableCollection.cs new file mode 100644 index 000000000000..f05c12e6a15e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/VariableCollection.cs @@ -0,0 +1,216 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Core +{ + public class VariableCollection : IVariableCollection, IMonitoredVariableCollection + { + private static readonly IEnumerable NoKeys = []; + private readonly IDictionary _values; + private IVariableCollection? _parent; + + public VariableCollection() + : this(null) + { + } + + public VariableCollection(VariableCollection? parent) + : this(parent, new Dictionary()) + { + } + + public VariableCollection(IVariableCollection? parent, IDictionary values) + { + if (values != null) + { + if (values.Values.Any(o => o is null)) + { + throw new ArgumentException($"The {nameof(values)} should not contain null.", nameof(values)); + } + } + + _parent = parent; + _values = values ?? new Dictionary(); + + if (_parent is not null and IMonitoredVariableCollection monitored) + { + monitored.KeysChanged += RelayKeysChanged; + } + } + + public event KeysChangedEventHander? KeysChanged; + + public event ValueReadEventHander? ValueRead; + + public int Count => Keys.Count; + + public bool IsReadOnly => false; + + public ICollection Keys => _values.Keys.Union(_parent?.Keys ?? NoKeys).ToList(); + + public IVariableCollection? Parent + { + get => _parent; + + set + { + _parent = value; + OnKeysChanged(); + } + } + + public ICollection Values => Keys.Select(x => this[x]).ToList(); + + public object this[string key] + { + get + { + if (_values.TryGetValue(key, out object result)) + { + ValueReadEventArgs args = new(key, result); + OnValueRead(args); + return result; + } + + if (_parent?.TryGetValue(key, out result) ?? false) + { + return result; + } + + throw new KeyNotFoundException($"No entry was found for key: {key}"); + } + + set + { + bool changing = !_values.ContainsKey(key); + _values[key] = value ?? throw new ArgumentNullException(nameof(value)); + if (changing) + { + OnKeysChanged(); + } + } + } + + public static VariableCollection Root() => Root(new Dictionary()); + + public static VariableCollection Root(IDictionary values) => new(null, values); + + public void Add(KeyValuePair item) + { + if (_parent?.ContainsKey(item.Key) ?? false) + { + throw new InvalidOperationException("Key already added"); + } + + if (item.Value is null) + { + throw new ArgumentException($"The value of key-value pair {nameof(item)} should not be null.", nameof(item)); + } + + _values.Add(item); + OnKeysChanged(); + } + + public void Add(string key, object value) + { + if (_parent?.ContainsKey(key) ?? false) + { + throw new InvalidOperationException("Key already added"); + } + + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + _values.Add(key, value); + OnKeysChanged(); + } + + public void Clear() + { + if (_values.Count > 0) + { + _values.Clear(); + OnKeysChanged(); + } + } + + public bool Contains(KeyValuePair item) + { + return _values.Contains(item) || (_parent?.Contains(item) ?? false); + } + + public bool ContainsKey(string key) => _values.ContainsKey(key) || (_parent?.ContainsKey(key) ?? false); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + int index = arrayIndex; + foreach (string key in Keys) + { + if (index >= array.Length) + { + break; + } + + array[index++] = new KeyValuePair(key, this[key]); + } + } + + public IEnumerator> GetEnumerator() => Keys.Select(x => new KeyValuePair(x, this[x])).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator(); + + public bool Remove(KeyValuePair item) => Remove(item.Key); + + public bool Remove(string key) + { + if (_values.Remove(key)) + { + if (!(_parent?.ContainsKey(key) ?? false)) + { + OnKeysChanged(); + } + + return true; + } + + return false; + } + + public bool TryGetValue(string key, out object value) + { + if (_values.TryGetValue(key, out value)) + { + OnValueRead(key, value); + return true; + } + + return _parent?.TryGetValue(key, out value) ?? false; + } + + private void OnKeysChanged() + { + KeysChanged?.Invoke(this, KeysChangedEventArgs.Default); + } + + private void OnValueRead(string key, object value) + { + OnValueRead(new ValueReadEventArgs(key, value)); + } + + private void OnValueRead(IValueReadEventArgs args) + { + ValueRead?.Invoke(this, args); + } + + private void RelayKeysChanged(object sender, IKeysChangedEventArgs args) + { + OnKeysChanged(); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.cs.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.cs.xlf new file mode 100644 index 000000000000..6c730d12b925 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.cs.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + Při analýze a vyhodnocování výrazu došlo k následující chybě: + + + + Unexpected substring after parsed expression: + Neočekávaný podřetězec po parsovaném výrazu: + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.de.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.de.xlf new file mode 100644 index 000000000000..42536fb09896 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.de.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + Der folgende Fehler ist beim Analysieren und Auswerten des Ausdrucks aufgetreten: + + + + Unexpected substring after parsed expression: + Unerwartete Teilzeichenfolge nach analysierten Ausdrücken: + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.es.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.es.xlf new file mode 100644 index 000000000000..7b36c075e0cf --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.es.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + Se encontró el siguiente error al analizar y evaluar la expresión: + + + + Unexpected substring after parsed expression: + Subcadena inesperada después de la expresión analizada: + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.fr.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.fr.xlf new file mode 100644 index 000000000000..18b9d931a93f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.fr.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + Erreur suivante rencontrée lors de l’analyse et de l’évaluation de l’expression : + + + + Unexpected substring after parsed expression: + Substring inattendue après analyse de l’expression : + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.it.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.it.xlf new file mode 100644 index 000000000000..f8d2bde206fb --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.it.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + Si è verificato l'errore seguente durante l'analisi e la valutazione dell'espressione: + + + + Unexpected substring after parsed expression: + Sottostringa imprevista dopo l'espressione analizzata: + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.ja.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.ja.xlf new file mode 100644 index 000000000000..911b8598aacc --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.ja.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + 式の解析および評価中に次のエラーが発生しました: + + + + Unexpected substring after parsed expression: + 解析された式の後に予期しない部分文字列があります: + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.ko.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.ko.xlf new file mode 100644 index 000000000000..8d56a8de55af --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.ko.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + 식을 구문 분석하고 평가하는 동안 다음 오류가 발생했습니다. + + + + Unexpected substring after parsed expression: + 구문 분석된 식 다음에 예기치 않은 하위 문자열이 있습니다. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.pl.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.pl.xlf new file mode 100644 index 000000000000..4fb3a5814e00 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.pl.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + Napotkano następujący błąd podczas analizowania i oceniania wyrażenia: + + + + Unexpected substring after parsed expression: + Nieoczekiwane podciągi po przeanalizowanym wyrażeniu: + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.pt-BR.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.pt-BR.xlf new file mode 100644 index 000000000000..12c5d572c794 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.pt-BR.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + Erro encontrado ao analisar e avaliar a expressão: + + + + Unexpected substring after parsed expression: + Substring inesperada após a expressão analisada: + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.ru.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.ru.xlf new file mode 100644 index 000000000000..575c7c34243f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.ru.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + При синтаксическом анализе и вычислении выражения произошла следующая ошибка: + + + + Unexpected substring after parsed expression: + Непредвиденная подстрока после синтаксического анализа выражения: + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.tr.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.tr.xlf new file mode 100644 index 000000000000..09d273581906 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.tr.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + İfade ayrıştırılır ve değerlendirilirken aşağıdaki hatayla karşılaşıldı: + + + + Unexpected substring after parsed expression: + Ayrıştırılan ifadeden sonra beklenmeyen alt dize: + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.zh-Hans.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.zh-Hans.xlf new file mode 100644 index 000000000000..d52422dc0788 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.zh-Hans.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + 分析和计算表达式时遇到以下错误: + + + + Unexpected substring after parsed expression: + 分析表达式后出现意外的子字符串: + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.zh-Hant.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.zh-Hant.xlf new file mode 100644 index 000000000000..90b3010379de --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Core/xlf/LocalizableStrings.zh-Hant.xlf @@ -0,0 +1,17 @@ + + + + + + Encountered following error when parsing and evaluating expression: + 剖析和評估運算式時遇到以下錯誤: + + + + Unexpected substring after parsed expression: + 剖析運算式後出現未預期的子字串: + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettings.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettings.cs new file mode 100644 index 000000000000..a2266f218c75 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettings.cs @@ -0,0 +1,226 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging; + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Edge.Settings; + +namespace Microsoft.TemplateEngine.Edge.BuiltInManagedProvider +{ + internal sealed class GlobalSettings : IGlobalSettings, IDisposable + { + private const int FileReadWriteRetries = 20; + private const int MillisecondsInterval = 20; + private static readonly TimeSpan MaxNotificationDelayOnWriterLock = TimeSpan.FromSeconds(1); + private readonly IEngineEnvironmentSettings _environmentSettings; + private readonly string _globalSettingsFile; + private IDisposable? _watcher; + private volatile bool _disposed; + private volatile AsyncMutex? _mutex; + private int _waitingInstances; + + public GlobalSettings(IEngineEnvironmentSettings environmentSettings, string globalSettingsFile) + { + _environmentSettings = environmentSettings ?? throw new ArgumentNullException(nameof(environmentSettings)); + _globalSettingsFile = globalSettingsFile ?? throw new ArgumentNullException(nameof(globalSettingsFile)); + environmentSettings.Host.FileSystem.CreateDirectory(Path.GetDirectoryName(_globalSettingsFile)); + _watcher = CreateWatcherIfRequested(); + } + + public event Action? SettingsChanged; + + public async Task LockAsync(CancellationToken token) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(GlobalSettings)); + } + token.ThrowIfCancellationRequested(); + if (_mutex?.IsLocked ?? false) + { + throw new InvalidOperationException("Lock is already taken."); + } + // We must use Mutex because we want to lock across different processes that might want to modify this settings file + var escapedFilename = _globalSettingsFile.Replace("\\", "_").Replace("/", "_"); + var mutex = await AsyncMutex.WaitAsync($"Global\\812CA7F3-7CD8-44B4-B3F0-0159355C0BD5{escapedFilename}", token).ConfigureAwait(false); + _mutex = mutex; + return mutex; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + _disposed = true; + _watcher?.Dispose(); + _watcher = null; + } + + public async Task> GetInstalledTemplatePackagesAsync(CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(GlobalSettings)); + } + + if (!_environmentSettings.Host.FileSystem.FileExists(_globalSettingsFile)) + { + return []; + } + + for (int i = 0; i < FileReadWriteRetries; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var jObject = _environmentSettings.Host.FileSystem.ReadObject(_globalSettingsFile); + var packages = new List(); + + foreach (var package in jObject.Get(nameof(GlobalSettingsData.Packages)) ?? new JsonArray()) + { + packages.Add(new TemplatePackageData( + package!.ToGuid(nameof(TemplatePackageData.InstallerId)), + package.ToString(nameof(TemplatePackageData.MountPointUri)) ?? string.Empty, + package![nameof(TemplatePackageData.LastChangeTime)]?.GetValue() ?? default, + package.ToStringDictionary(propertyName: nameof(TemplatePackageData.Details)))); + } + + return packages; + } + catch (JsonException ex) + { + var wrappedEx = new JsonException(string.Format(LocalizableStrings.GlobalSettings_Error_CorruptedSettings, _globalSettingsFile, ex.Message), ex.Path, ex.LineNumber, ex.BytePositionInLine, ex); + throw wrappedEx; + } + catch (Exception) + { + if (i == (FileReadWriteRetries - 1)) + { + throw; + } + } + await Task.Delay(MillisecondsInterval, cancellationToken).ConfigureAwait(false); + } + throw new InvalidOperationException(); + } + + public async Task SetInstalledTemplatePackagesAsync(IReadOnlyList packages, CancellationToken cancellationToken) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(GlobalSettings)); + } + + if (!(_mutex?.IsLocked ?? false)) + { + throw new InvalidOperationException($"Before calling {nameof(SetInstalledTemplatePackagesAsync)}, {nameof(LockAsync)} must be called."); + } + + var globalSettingsData = new GlobalSettingsData(packages); + + for (int i = 0; i < FileReadWriteRetries; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + // Ignore FSW notifications received during writing changes (we'll notify synchronously) + _watcher?.Dispose(); + _environmentSettings.Host.FileSystem.WriteObject(_globalSettingsFile, globalSettingsData, GlobalSettingsJsonSerializerContext.Default.GlobalSettingsData); + // We are ready for new notifications now + _watcher = CreateWatcherIfRequested(); + SettingsChanged?.Invoke(); + return; + } + catch (Exception) + { + if (i == (FileReadWriteRetries - 1)) + { + throw; + } + } + await Task.Delay(MillisecondsInterval, cancellationToken).ConfigureAwait(false); + } + throw new InvalidOperationException(); + } + + private IDisposable? CreateWatcherIfRequested() + { + if (_environmentSettings.Environment.GetEnvironmentVariable("TEMPLATE_ENGINE_DISABLE_FILEWATCHER") != "1") + { + return _environmentSettings.Host.FileSystem.WatchFileChanges(_globalSettingsFile, FileChanged); + } + + return null; + } + + // This method is called whenever there is a change in global settings. Since the handlers of SettingsChanged event + // first grab the lock (LockAsync) and then read the whole content of GlobalSettings folder - we are here making sure + // to skip unwanted extra calls - all concurrent calls while handler is waiting for a lock leads to duplicate reprocessing + // of a whole global settings folder. + // To prevent this - we try to wait for a lock on behalf of the handler and refuse all concurrent file change notifications in the meantime + private async void FileChanged(object sender, FileSystemEventArgs e) + { + // FileSystemWatcher fires callbacks on threadpool threads that can race with Dispose(). + // This pre-lock check handles the common case where the callback fires after _disposed is set. + if (_disposed) + { + return; + } + + // Make sure the waiting happens only for one notification at the time - as we do not care about other notifications + // until the SettingsChanged is called + // if multiple concurrent call(s) get here, while there is already other caller inside waiting for the lock + // those concurrent callers will just return (as counter is 1 already). + if (Interlocked.Increment(ref _waitingInstances) > 1) + { + return; + } + + await TryWaitForLock().ConfigureAwait(false); + + // Re-check after lock wait: the object may have been disposed while we were waiting + // for the lock. Without this guard, SettingsChanged subscribers would call back into + // disposed state. Stress testing confirms this fires in ~99% of disposal-during-callback races. + if (_disposed) + { + return; + } + + // We are ready for new notifications now - indicate so by clearing the counter + Interlocked.Exchange(ref _waitingInstances, 0); + + SettingsChanged?.Invoke(); + } + + private async Task TryWaitForLock() + { + CancellationTokenSource cts = new(); + try + { + cts.CancelAfter(MaxNotificationDelayOnWriterLock); + if (!(_mutex?.IsLocked ?? false)) + { + using (await LockAsync(cts.Token).ConfigureAwait(false)) + { } + } + } + catch (Exception e) + { + _environmentSettings.Host.Logger.LogDebug( + "Failed to wait for GlobalSettings lock to be freed, before notifying about new changes. {error}", + e.Message); + return false; + } + + return true; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsData.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsData.cs new file mode 100644 index 000000000000..4e55c4643c03 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsData.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using Microsoft.TemplateEngine.Abstractions.Installer; + +namespace Microsoft.TemplateEngine.Edge.BuiltInManagedProvider +{ + /// + /// Used just to serialize/deserialize data to/from settings.json file. + /// + internal sealed class GlobalSettingsData + { + internal GlobalSettingsData(IReadOnlyList packages) + { + Packages = packages; + } + + [JsonInclude] + internal IReadOnlyList Packages { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsJsonSerializerContext.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsJsonSerializerContext.cs new file mode 100644 index 000000000000..bd3353872b2f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsJsonSerializerContext.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Microsoft.TemplateEngine.Edge.BuiltInManagedProvider +{ + [JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + [JsonSerializable(typeof(GlobalSettingsData))] + internal partial class GlobalSettingsJsonSerializerContext : JsonSerializerContext; +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsTemplatePackageProvider.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsTemplatePackageProvider.cs new file mode 100644 index 000000000000..a705bf5f2d21 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsTemplatePackageProvider.cs @@ -0,0 +1,367 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; + +namespace Microsoft.TemplateEngine.Edge.BuiltInManagedProvider +{ + internal class GlobalSettingsTemplatePackageProvider : IManagedTemplatePackageProvider, IDisposable + { + private const string DebugLogCategory = "Installer"; + + private readonly ILogger _logger; + private readonly Dictionary _installersByGuid = new Dictionary(); + private readonly Dictionary _installersByName = new Dictionary(); + private readonly GlobalSettings _globalSettings; + private volatile bool _disposed; + + public GlobalSettingsTemplatePackageProvider(GlobalSettingsTemplatePackageProviderFactory factory, IEngineEnvironmentSettings settings) + { + Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + IEngineEnvironmentSettings environmentSettings = settings ?? throw new ArgumentNullException(nameof(settings)); + _logger = settings.Host.LoggerFactory.CreateLogger(); + + string packagesFolder = Path.Combine(settings.Paths.GlobalSettingsDir, "packages"); + if (!settings.Host.FileSystem.DirectoryExists(packagesFolder)) + { + settings.Host.FileSystem.CreateDirectory(packagesFolder); + } + foreach (var installerFactory in settings.Components.OfType()) + { + var installer = installerFactory.CreateInstaller(settings, packagesFolder); + + //this provider cannot work with installers that do not implement ISerializableInstaller + if (installer is ISerializableInstaller) + { + _installersByName[installerFactory.Name] = installer; + _installersByGuid[installerFactory.Id] = installer; + } + } + + string globalSettingsFilePath = Path.Combine(environmentSettings.Paths.GlobalSettingsDir, "packages.json"); + _globalSettings = new GlobalSettings(environmentSettings, globalSettingsFilePath); + // We can't just add "SettingsChanged+=TemplatePackagesChanged", because TemplatePackagesChanged is null at this time. + _globalSettings.SettingsChanged += OnGlobalSettingsChanged; + } + + public event Action? TemplatePackagesChanged; + + public ITemplatePackageProviderFactory Factory { get; } + + public async Task> GetAllTemplatePackagesAsync(CancellationToken cancellationToken) + { + var list = new List(); + foreach (TemplatePackageData entry in await _globalSettings.GetInstalledTemplatePackagesAsync(cancellationToken).ConfigureAwait(false)) + { + if (_installersByGuid.TryGetValue(entry.InstallerId, out var installer)) + { + try + { + list.Add(((ISerializableInstaller)installer).Deserialize(this, entry)); + } + catch (Exception e) + { + _logger.LogDebug($"[{Factory.DisplayName}] Failed to deserialize template package data entry {entry.MountPointUri}, details: {e}.", DebugLogCategory); + //adding template package as non-managed + list.Add(new TemplatePackage(this, entry.MountPointUri, entry.LastChangeTime)); + } + } + else + { + list.Add(new TemplatePackage(this, entry.MountPointUri, entry.LastChangeTime)); + } + } + return list; + } + + public async Task> GetLatestVersionsAsync(IEnumerable packages, CancellationToken cancellationToken) + { + _ = packages ?? throw new ArgumentNullException(nameof(packages)); + + var tasks = new List>>(); + foreach (var packagesGroupedByInstaller in packages.GroupBy(s => s.Installer)) + { + tasks.Add(packagesGroupedByInstaller.Key.GetLatestVersionAsync(packagesGroupedByInstaller, this, cancellationToken)); + } + await Task.WhenAll(tasks).ConfigureAwait(false); + + var result = new List(); + foreach (var task in tasks) + { + result.AddRange(task.Result); + } + + await UpdateTemplatePackagesMetadataAsync(result.Select(r => r.TemplatePackage), cancellationToken).ConfigureAwait(false); + + return result; + } + + /// + /// when has duplicate identifiers for given installer. + public async Task> InstallAsync(IEnumerable installRequests, CancellationToken cancellationToken) + { + _ = installRequests ?? throw new ArgumentNullException(nameof(installRequests)); + if (!installRequests.Any()) + { + return new List(); + } + + //validate that install requests are different - install requests should have unique identifier for given installer + HashSet uniqueInstallRequests = new HashSet(new InstallRequestEqualityComparer()); + foreach (InstallRequest installRequest in installRequests) + { + if (uniqueInstallRequests.Add(installRequest)) + { + continue; + } + throw new ArgumentException($"{nameof(installRequests)} has duplicate install requests", nameof(installRequest)); + } + + using var disposable = await _globalSettings.LockAsync(cancellationToken).ConfigureAwait(false); + var packages = new List(await _globalSettings.GetInstalledTemplatePackagesAsync(cancellationToken).ConfigureAwait(false)); + var results = await Task.WhenAll(installRequests.Select(async installRequest => + { + var installersThatCanInstall = new List(); + foreach (var install in _installersByName.Values) + { + if (await install.CanInstallAsync(installRequest, cancellationToken).ConfigureAwait(false)) + { + installersThatCanInstall.Add(install); + } + } + if (installersThatCanInstall.Count == 0) + { + return InstallResult.CreateFailure( + installRequest, + InstallerErrorCode.UnsupportedRequest, + string.Format(LocalizableStrings.GlobalSettingsTemplatePackageProvider_InstallResult_Error_PackageCannotBeInstalled, installRequest.PackageIdentifier), + []); + } + if (installersThatCanInstall.Count > 1) + { + return InstallResult.CreateFailure( + installRequest, + InstallerErrorCode.UnsupportedRequest, + string.Format(LocalizableStrings.GlobalSettingsTemplatePackageProvider_InstallResult_Error_MultipleInstallersCanBeUsed, installRequest.PackageIdentifier), + []); + } + + IInstaller installer = installersThatCanInstall[0]; + return await InstallAsync(packages, installRequest, installer, cancellationToken).ConfigureAwait(false); + })).ConfigureAwait(false); + await _globalSettings.SetInstalledTemplatePackagesAsync(packages, cancellationToken).ConfigureAwait(false); + return results; + } + + public async Task> UninstallAsync(IEnumerable packages, CancellationToken cancellationToken) + { + _ = packages ?? throw new ArgumentNullException(nameof(packages)); + if (!packages.Any()) + { + return new List(); + } + + using var disposable = await _globalSettings.LockAsync(cancellationToken).ConfigureAwait(false); + + var packagesInSettings = new List(await _globalSettings.GetInstalledTemplatePackagesAsync(cancellationToken).ConfigureAwait(false)); + var results = await Task.WhenAll(packages.Select(async package => + { + UninstallResult result = await package.Installer.UninstallAsync(package, this, cancellationToken).ConfigureAwait(false); + if (result.Success) + { + lock (packagesInSettings) + { + packagesInSettings.RemoveAll(p => p.MountPointUri == package.MountPointUri); + } + } + return result; + })).ConfigureAwait(false); + await _globalSettings.SetInstalledTemplatePackagesAsync(packagesInSettings, cancellationToken).ConfigureAwait(false); + return results; + } + + public async Task> UpdateAsync(IEnumerable updateRequests, CancellationToken cancellationToken) + { + _ = updateRequests ?? throw new ArgumentNullException(nameof(updateRequests)); + IEnumerable updatesToApply = updateRequests.Where(request => request.Version != request.TemplatePackage.Version); + + using var disposable = await _globalSettings.LockAsync(cancellationToken).ConfigureAwait(false); + + var packages = new List(await _globalSettings.GetInstalledTemplatePackagesAsync(cancellationToken).ConfigureAwait(false)); + var results = await Task.WhenAll(updatesToApply.Select(updateRequest => UpdateAsync(packages, updateRequest, cancellationToken))).ConfigureAwait(false); + await _globalSettings.SetInstalledTemplatePackagesAsync(packages, cancellationToken).ConfigureAwait(false); + return results; + } + + public void Dispose() + { + _disposed = true; + _globalSettings.SettingsChanged -= OnGlobalSettingsChanged; + _globalSettings.Dispose(); + } + + private void OnGlobalSettingsChanged() + { + // Guard against SettingsChanged firing during cascading disposal: Dispose() sets _disposed + // then unsubscribes, but an in-flight callback may already be past the delegate check. + if (_disposed) + { + return; + } + TemplatePackagesChanged?.Invoke(); + } + + private async Task UpdateTemplatePackagesMetadataAsync(IEnumerable templatePackages, CancellationToken cancellationToken) + { + _ = templatePackages ?? throw new ArgumentNullException(nameof(templatePackages)); + var updatedPackages = new List(templatePackages.Select(tp => ((ISerializableInstaller)tp.Installer).Serialize(tp))); + var cachedPackages = await _globalSettings.GetInstalledTemplatePackagesAsync(cancellationToken).ConfigureAwait(false); + + var updatedCachePackages = cachedPackages.Select(cp => + { + var updatedPackage = updatedPackages.FirstOrDefault(up => up.MountPointUri == cp.MountPointUri); + return updatedPackage ?? cp; + }).ToArray(); + using var disposable = await _globalSettings.LockAsync(cancellationToken).ConfigureAwait(false); + await _globalSettings.SetInstalledTemplatePackagesAsync(updatedCachePackages, cancellationToken).ConfigureAwait(false); + } + + private async Task UpdateAsync(List packages, UpdateRequest updateRequest, CancellationToken cancellationToken) + { + (InstallerErrorCode result, string message) = await EnsureInstallPrerequisites(packages, updateRequest.TemplatePackage.Identifier, updateRequest.Version, updateRequest.TemplatePackage.Installer, cancellationToken, update: true).ConfigureAwait(false); + if (result != InstallerErrorCode.Success) + { + return UpdateResult.CreateFailure(updateRequest, result, message, []); + } + + UpdateResult updateResult = await updateRequest.TemplatePackage.Installer.UpdateAsync(updateRequest, provider: this, cancellationToken).ConfigureAwait(false); + if (!updateResult.Success) + { + return updateResult; + } + + if (updateResult.TemplatePackage is null) + { + throw new InvalidOperationException($"{nameof(updateResult.TemplatePackage)} cannot be null when {nameof(updateResult.Success)} is 'true'"); + } + + lock (packages) + { + packages.Add(((ISerializableInstaller)updateRequest.TemplatePackage.Installer).Serialize(updateResult.TemplatePackage)); + } + return updateResult; + } + + private async Task<(InstallerErrorCode, string)> EnsureInstallPrerequisites(List packagesInSettings, string identifier, string? version, IInstaller installer, CancellationToken cancellationToken, bool update = false, bool forceUpdate = false) + { + var packages = await GetAllTemplatePackagesAsync(cancellationToken).ConfigureAwait(false); + + //check if the package with same identifier is already installed + if (packages.OfType().FirstOrDefault(s => s.Identifier == identifier && s.Installer == installer) is IManagedTemplatePackage packageToBeUpdated) + { + //if same version is already installed - return + if (!forceUpdate && packageToBeUpdated.Version == version) + { + return (InstallerErrorCode.AlreadyInstalled, string.Format(LocalizableStrings.GlobalSettingsTemplatePackageProvider_InstallResult_Error_PackageAlreadyInstalled, packageToBeUpdated.DisplayName)); + } + if (!update) + { + _logger.LogInformation( + string.Format( + LocalizableStrings.GlobalSettingsTemplatePackagesProvider_Info_PackageAlreadyInstalled, + string.IsNullOrWhiteSpace(packageToBeUpdated.Version) + ? packageToBeUpdated.Identifier + : $"{packageToBeUpdated.Identifier} ({string.Format(LocalizableStrings.Generic_Version, packageToBeUpdated.Version)})", + string.IsNullOrWhiteSpace(version) ? + LocalizableStrings.Generic_LatestVersion : + string.Format(LocalizableStrings.Generic_Version, version))); + } + //uninstall previous version first + UninstallResult uninstallResult = await installer.UninstallAsync(packageToBeUpdated, this, cancellationToken).ConfigureAwait(false); + if (!uninstallResult.Success) + { + if (uninstallResult.ErrorMessage is null) + { + throw new InvalidOperationException($"{nameof(uninstallResult.ErrorMessage)} cannot be null when {nameof(uninstallResult.Success)} is 'true'"); + } + return (InstallerErrorCode.UpdateUninstallFailed, uninstallResult.ErrorMessage); + } + _logger.LogInformation( + string.Format( + LocalizableStrings.GlobalSettingsTemplatePackagesProvider_Info_PackageUninstalled, + packageToBeUpdated.DisplayName)); + + lock (packagesInSettings) + { + packagesInSettings.RemoveAll(p => p.MountPointUri == packageToBeUpdated.MountPointUri); + } + } + return (InstallerErrorCode.Success, string.Empty); + } + + private async Task InstallAsync(List packages, InstallRequest installRequest, IInstaller installer, CancellationToken cancellationToken) + { + _ = installRequest ?? throw new ArgumentNullException(nameof(installRequest)); + _ = installer ?? throw new ArgumentNullException(nameof(installer)); + + (InstallerErrorCode result, string message) = await EnsureInstallPrerequisites( + packages, + installRequest.PackageIdentifier, + installRequest.Version, + installer, + cancellationToken, + forceUpdate: installRequest.Force).ConfigureAwait(false); + if (result != InstallerErrorCode.Success) + { + return InstallResult.CreateFailure(installRequest, result, message, []); + } + + InstallResult installResult = await installer.InstallAsync(installRequest, this, cancellationToken).ConfigureAwait(false); + if (!installResult.Success) + { + return installResult; + } + if (installResult.TemplatePackage is null) + { + throw new InvalidOperationException($"{nameof(installResult.TemplatePackage)} cannot be null when {nameof(installResult.Success)} is 'true'"); + } + + lock (packages) + { + packages.Add(((ISerializableInstaller)installer).Serialize(installResult.TemplatePackage)); + } + return installResult; + } + + private class InstallRequestEqualityComparer : IEqualityComparer + { + public bool Equals(InstallRequest x, InstallRequest y) + { + if (!x.PackageIdentifier.Equals(y.PackageIdentifier, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (x.InstallerName != null + && y.InstallerName != null + && x.InstallerName.Equals(y.InstallerName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + } + + public int GetHashCode(InstallRequest obj) + { + if (obj == null) + { + return 0; + } + return new { a = obj.InstallerName?.ToLowerInvariant(), b = obj.PackageIdentifier.ToLowerInvariant() }.GetHashCode(); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsTemplatePackageProviderFactory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsTemplatePackageProviderFactory.cs new file mode 100644 index 000000000000..fcca4b2ee9a2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/GlobalSettingsTemplatePackageProviderFactory.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; + +namespace Microsoft.TemplateEngine.Edge.BuiltInManagedProvider +{ + public sealed class GlobalSettingsTemplatePackageProviderFactory : ITemplatePackageProviderFactory, IPrioritizedComponent + { + internal static readonly Guid FactoryId = new("{3AACE22E-E978-4BAF-8BC1-568B290A238C}"); + + Guid IIdentifiedComponent.Id => FactoryId; + + string ITemplatePackageProviderFactory.DisplayName => "Global Settings"; + + /// + /// We want to have higher priority than SDK/OptionalWorkload providers. + /// So user installed templates(from this provider) override those. + /// + int IPrioritizedComponent.Priority => 1000; + + ITemplatePackageProvider ITemplatePackageProviderFactory.CreateProvider(IEngineEnvironmentSettings settings) + { + return new GlobalSettingsTemplatePackageProvider(this, settings); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/IGlobalSettings.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/IGlobalSettings.cs new file mode 100644 index 000000000000..5b5ec7768a01 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/BuiltInManagedProvider/IGlobalSettings.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Installer; + +namespace Microsoft.TemplateEngine.Edge.BuiltInManagedProvider +{ + /// + /// Interface that represents loading/storing data into settings.json file. + /// That is shared between multiple different hosts of TemplateEngine. + /// + internal interface IGlobalSettings + { + /// + /// Triggered every time when settings change. + /// + event Action SettingsChanged; + + /// + /// Returns non-cached list of the template packages. + /// + Task> GetInstalledTemplatePackagesAsync(CancellationToken cancellationToken); + + /// + /// Stores list of the template packages. + /// + Task SetInstalledTemplatePackagesAsync(IReadOnlyList packages, CancellationToken cancellationToken); + + /// + /// This method must be called before making any modifications to settings to ensure other processes on system + /// don't override or lose changes done by this process. + /// + /// object that needs to be disposed once modifying of settings is finished. + Task LockAsync(CancellationToken token); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Components.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Components.cs new file mode 100644 index 000000000000..3b0d9e4c983d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Components.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Components; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; +using Microsoft.TemplateEngine.Edge.BuiltInManagedProvider; +using Microsoft.TemplateEngine.Edge.Constraints; +using Microsoft.TemplateEngine.Edge.Installers.Folder; +using Microsoft.TemplateEngine.Edge.Installers.NuGet; +using Microsoft.TemplateEngine.Edge.Mount.Archive; +using Microsoft.TemplateEngine.Edge.Mount.FileSystem; + +namespace Microsoft.TemplateEngine.Edge +{ + public static class Components + { + public static IReadOnlyList<(Type Type, IIdentifiedComponent Instance)> AllComponents { get; } = + new (Type Type, IIdentifiedComponent Instance)[] + { + (typeof(IMountPointFactory), new ZipFileMountPointFactory()), + (typeof(IMountPointFactory), new FileSystemMountPointFactory()), + (typeof(ITemplatePackageProviderFactory), new GlobalSettingsTemplatePackageProviderFactory()), + (typeof(IInstallerFactory), new FolderInstallerFactory()), + (typeof(IInstallerFactory), new NuGetInstallerFactory()), + (typeof(ITemplateConstraintFactory), new OSConstraintFactory()), + (typeof(ITemplateConstraintFactory), new HostConstraintFactory()), + (typeof(ITemplateConstraintFactory), new WorkloadConstraintFactory()), + (typeof(ITemplateConstraintFactory), new SdkVersionConstraintFactory()), + (typeof(IBindSymbolSource), new EnvironmentVariablesBindSource()), + (typeof(IBindSymbolSource), new HostParametersBindSource()), + }; + + public static IReadOnlyList<(Type Type, IIdentifiedComponent Instance)> MandatoryComponents { get; } = + new (Type Type, IIdentifiedComponent Instance)[] + { + (typeof(IMountPointFactory), new ZipFileMountPointFactory()), + (typeof(IMountPointFactory), new FileSystemMountPointFactory()), + (typeof(ITemplatePackageProviderFactory), new GlobalSettingsTemplatePackageProviderFactory()), + (typeof(IInstallerFactory), new FolderInstallerFactory()), + (typeof(IBindSymbolSource), new EnvironmentVariablesBindSource()), + (typeof(IBindSymbolSource), new HostParametersBindSource()), + }; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Components/EnvironmentVariablesBindSource.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Components/EnvironmentVariablesBindSource.cs new file mode 100644 index 000000000000..2c504224abe9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Components/EnvironmentVariablesBindSource.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Components; + +namespace Microsoft.TemplateEngine.Edge +{ + /// + /// The component allows to bind environment variables. + /// + public sealed class EnvironmentVariablesBindSource : IBindSymbolSource + { + int IPrioritizedComponent.Priority => 0; + + string IBindSymbolSource.SourcePrefix => "env"; + + bool IBindSymbolSource.RequiresPrefixMatch => false; + + Guid IIdentifiedComponent.Id => Guid.Parse("{8420EB0D-2FD7-49A7-966D-0914C86A14E4}"); + + string IBindSymbolSource.DisplayName => LocalizableStrings.EnvironmentVariablesBindSource_Name; + + Task IBindSymbolSource.GetBoundValueAsync(IEngineEnvironmentSettings settings, string bindName, CancellationToken cancellationToken) + { + settings.Host.Logger.LogDebug( + "[{0}]: Retrieving bound value for '{1}'.", + nameof(EnvironmentVariablesBindSource), + bindName); + + string? result = settings.Environment.GetEnvironmentVariable(bindName); + + settings.Host.Logger.LogDebug( + "[{0}]: Retrieved bound value for '{1}': '{2}'.", + nameof(EnvironmentVariablesBindSource), + bindName, + result ?? ""); + return Task.FromResult(result); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Components/HostParametersBindSource.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Components/HostParametersBindSource.cs new file mode 100644 index 000000000000..0c8c886b4472 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Components/HostParametersBindSource.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Components; + +namespace Microsoft.TemplateEngine.Edge +{ + /// + /// The component allows custom host parameters. + /// + public sealed class HostParametersBindSource : IBindSymbolSource + { + int IPrioritizedComponent.Priority => 100; + + string IBindSymbolSource.SourcePrefix => "host"; + + bool IBindSymbolSource.RequiresPrefixMatch => false; + + Guid IIdentifiedComponent.Id => Guid.Parse("{63AB8956-DBFA-4DA4-8089-93CC8272D7C5}"); + + string IBindSymbolSource.DisplayName => LocalizableStrings.HostParametersBindSource_Name; + + Task IBindSymbolSource.GetBoundValueAsync(IEngineEnvironmentSettings settings, string bindName, CancellationToken cancellationToken) + { + settings.Host.Logger.LogDebug( + "[{0}]: Retrieving bound value for '{1}'.", + nameof(HostParametersBindSource), + bindName); + + settings.Host.TryGetHostParamDefault(bindName, out string? newValue); + + settings.Host.Logger.LogDebug( + "[{0}]: Retrieved bound value for '{1}': '{2}'.", + nameof(HostParametersBindSource), + bindName, + newValue ?? ""); + + return Task.FromResult(newValue); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/ConfigurationException.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/ConfigurationException.cs new file mode 100644 index 000000000000..5ad19eed698f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/ConfigurationException.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Constraints; + +namespace Microsoft.TemplateEngine.Edge.Constraints +{ + /// + /// Exception occurred when parsing configuration for . + /// + internal class ConfigurationException : Exception + { + public ConfigurationException(string message) : base(message) + { + } + + public ConfigurationException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/ConstraintBase.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/ConstraintBase.cs new file mode 100644 index 000000000000..ba2c5186b1a0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/ConstraintBase.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; + +namespace Microsoft.TemplateEngine.Edge.Constraints +{ + internal abstract class ConstraintBase : ITemplateConstraint + { + internal ConstraintBase(IEngineEnvironmentSettings environmentSettings, ITemplateConstraintFactory factory) + { + EnvironmentSettings = environmentSettings; + Factory = factory; + } + + public string Type => Factory.Type; + + public abstract string DisplayName { get; } + + protected IEngineEnvironmentSettings EnvironmentSettings { get; } + + protected ITemplateConstraintFactory Factory { get; } + + public TemplateConstraintResult Evaluate(string? args) + { + try + { + return EvaluateInternal(args); + } + catch (ConfigurationException ce) + { + return TemplateConstraintResult.CreateEvaluationFailure(this, ce.Message, LocalizableStrings.Generic_Constraint_WrongConfigurationCTA); + } + } + + protected abstract TemplateConstraintResult EvaluateInternal(string? args); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/ConstraintsExtensions.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/ConstraintsExtensions.cs new file mode 100644 index 000000000000..308f4813f298 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/ConstraintsExtensions.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Constraints +{ + internal static class Extensions + { + /// + /// Attempts to parse input configuration string (presumably string or json array of strings) into enumeration of strings. + /// + /// Input configuration string. + /// Enumeration of parsed tokens. + /// Thrown on unexpected input - not a valid json string or array of string or an empty array. + public static IEnumerable ParseArrayOfConstraintStrings(this string? args) + { + JsonNode token = ParseConstraintJsonNode(args); + + if (token.GetValueKind() == JsonValueKind.String) + { + return new[] { token.GetValue() ?? throw new ConfigurationException(string.Format(LocalizableStrings.Constraint_Error_ArgumentHasEmptyString, args)) }; + } + + JsonArray array = token.ToConstraintsJsonArray(args, true); + + return array.Select(value => + { + if (value == null || value.GetValueKind() != JsonValueKind.String) + { + throw new ConfigurationException(string.Format(LocalizableStrings.Constraint_Error_ArgumentHasEmptyString, args)); + } + + string? strValue = value.GetValue(); + if (string.IsNullOrEmpty(strValue)) + { + throw new ConfigurationException(string.Format(LocalizableStrings.Constraint_Error_ArgumentHasEmptyString, args)); + } + + return strValue!; + }); + } + + /// + /// Attempts to parse input configuration string (presumably string or json array of strings) into enumeration of JObjects. + /// + /// Input configuration string. + /// Enumeration of parsed JObject tokens. + /// Thrown on unexpected input - not a valid json array or an empty array. + public static IEnumerable ParseArrayOfConstraintJObjects(this string? args) + { + JsonNode token = ParseConstraintJsonNode(args); + JsonArray array = token.ToConstraintsJsonArray(args, false); + + return array.Select(value => + { + if (value is not JsonObject jObj) + { + throw new ConfigurationException(string.Format(LocalizableStrings.Constraint_Error_InvalidJsonArray_Objects, args)); + } + + return jObj; + }); + } + + /// + /// Attempts to parse given string and return the version specification (throws if unsuccessful). + /// checks version in the following order: + /// NuGet exact version + /// NuGet floating version + /// NuGet version range + /// Legacy template engine exact version + /// Legacy template engine version range. + /// + /// Version string to be parsed. + /// IVersionSpecification instance representing the given string representation of the version. + /// Thrown if given string is not recognized as any valid version format. + public static IVersionSpecification ParseVersionSpecification(this string versionString) + { + IVersionSpecification? versionInstance = null; + + if (NuGetVersionSpecification.TryParse(versionString, out NuGetVersionSpecification? exactNuGetVersion)) + { + versionInstance = exactNuGetVersion; + } + else if (NuGetFloatRangeSpecification.TryParse(versionString, out NuGetFloatRangeSpecification? floatVersion)) + { + versionInstance = floatVersion; + } + else if (NuGetVersionRangeSpecification.TryParse(versionString, out NuGetVersionRangeSpecification? rangeNuGetVersion)) + { + versionInstance = rangeNuGetVersion; + } + else if (ExactVersionSpecification.TryParse(versionString, out IVersionSpecification? exactVersion)) + { + versionInstance = exactVersion; + } + else if (RangeVersionSpecification.TryParse(versionString, out IVersionSpecification? rangeVersion)) + { + versionInstance = rangeVersion; + } + + if (versionInstance == null) + { + throw new ConfigurationException(string.Format(LocalizableStrings.Constraint_Error_InvalidVersion, versionString)); + } + + return versionInstance; + } + + private static JsonNode ParseConstraintJsonNode(this string? args) + { + if (string.IsNullOrWhiteSpace(args)) + { + throw new ConfigurationException(LocalizableStrings.Constraint_Error_ArgumentsNotSpecified); + } + + JsonNode? token; + try + { + token = JsonNode.Parse(args!); + } + catch (Exception e) + { + throw new ConfigurationException(string.Format(LocalizableStrings.Constraint_Error_InvalidJson, args), e); + } + + return token ?? throw new ConfigurationException(string.Format(LocalizableStrings.Constraint_Error_InvalidJson, args)); + } + + private static JsonArray ToConstraintsJsonArray(this JsonNode token, string? args, bool isStringTypeAllowed) + { + if (token is not JsonArray array) + { + throw new ConfigurationException(string.Format( + isStringTypeAllowed + ? LocalizableStrings.Constraint_Error_InvalidJsonType_StringOrArray + : LocalizableStrings.Constraint_Error_InvalidJsonType_Array, + args)); + } + + if (array.Count == 0) + { + throw new ConfigurationException(string.Format(LocalizableStrings.Constraint_Error_ArrayHasNoObjects, args)); + } + + return array; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/HostConstraint.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/HostConstraint.cs new file mode 100644 index 000000000000..bdbfb6ea1064 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/HostConstraint.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Constraints +{ + public sealed class HostConstraintFactory : ITemplateConstraintFactory + { + Guid IIdentifiedComponent.Id { get; } = Guid.Parse("{93721B30-6890-403F-BAE7-5925990865A2}"); + + string ITemplateConstraintFactory.Type => "host"; + + Task ITemplateConstraintFactory.CreateTemplateConstraintAsync(IEngineEnvironmentSettings environmentSettings, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult((ITemplateConstraint)new HostConstraint(environmentSettings, this)); + } + + internal class HostConstraint : ConstraintBase + { + internal HostConstraint(IEngineEnvironmentSettings environmentSettings, ITemplateConstraintFactory factory) + : base(environmentSettings, factory) + { } + + public override string DisplayName => LocalizableStrings.HostConstraint_Name; + + protected override TemplateConstraintResult EvaluateInternal(string? args) + { + IReadOnlyList supportedHosts = ParseArgs(args).ToList(); + + //check primary host name first + bool primaryHostNameMatch = false; + foreach (HostInformation hostInfo in supportedHosts.Where(h => h.HostName.Equals(EnvironmentSettings.Host.HostIdentifier, StringComparison.OrdinalIgnoreCase))) + { + primaryHostNameMatch = true; + if (hostInfo.Version == null || hostInfo.Version.CheckIfVersionIsValid(EnvironmentSettings.Host.Version)) + { + return TemplateConstraintResult.CreateAllowed(this); + } + } + if (!primaryHostNameMatch) + { + //if there is no primary host name, check fallback host names + foreach (HostInformation hostInfo in supportedHosts.Where(h => EnvironmentSettings.Host.FallbackHostTemplateConfigNames.Contains(h.HostName, StringComparer.OrdinalIgnoreCase))) + { + if (hostInfo.Version == null || hostInfo.Version.CheckIfVersionIsValid(EnvironmentSettings.Host.Version)) + { + return TemplateConstraintResult.CreateAllowed(this); + } + } + } + string errorMessage = string.Format(LocalizableStrings.HostConstraint_Message_Restricted, EnvironmentSettings.Host.HostIdentifier, EnvironmentSettings.Host.Version, supportedHosts.ToCsvString()); + return TemplateConstraintResult.CreateRestricted(this, errorMessage); + } + + // configuration examples + // "args": [ + // { + // "hostName": "dotnetcli", + // "version": "5.0.100" + // }, + // { + // "hostName": "ide", + // "version": "[16.0-*]" + // }] + private static IEnumerable ParseArgs(string? args) + { + List hostInformation = new List(); + + foreach (JsonObject jObj in args.ParseArrayOfConstraintJObjects()) + { + string? hostName = jObj.ToString("hostname"); + string? version = jObj.ToString("version"); + + if (string.IsNullOrWhiteSpace(hostName)) + { + throw new ConfigurationException(string.Format(LocalizableStrings.HostConstraint_Error_MissingMandatoryProperty, jObj, "hostname")); + } + if (string.IsNullOrWhiteSpace(version)) + { + hostInformation.Add(new HostInformation(hostName!)); + continue; + } + + hostInformation.Add(new HostInformation(hostName!, version!.ParseVersionSpecification())); + } + + return hostInformation; + } + + private class HostInformation + { + public HostInformation(string host, IVersionSpecification? version = null) + { + if (string.IsNullOrWhiteSpace(host)) + { + throw new ArgumentException($"'{nameof(host)}' cannot be null or whitespace.", nameof(host)); + } + + HostName = host; + Version = version; + } + + public string HostName { get; } + + public IVersionSpecification? Version { get; } + + public override string ToString() + { + return Version == null + ? HostName + : $"{HostName}({Version})"; + } + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/NuGetFloatRangeSpecification.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/NuGetFloatRangeSpecification.cs new file mode 100644 index 000000000000..3f51d7fc967a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/NuGetFloatRangeSpecification.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Utils; +using NuGet.Versioning; + +namespace Microsoft.TemplateEngine.Edge.Constraints +{ + internal class NuGetFloatRangeSpecification : IVersionSpecification + { + private readonly FloatRange _version; + + internal NuGetFloatRangeSpecification(FloatRange version) + { + _version = version; + } + + public bool CheckIfVersionIsValid(string versionToCheck) + { + if (NuGetVersion.TryParse(versionToCheck, out NuGetVersion? nuGetVersion2)) + { + return _version.Satisfies(nuGetVersion2); + } + return false; + } + + public override string ToString() => _version.ToString(); + + internal static bool TryParse(string value, out NuGetFloatRangeSpecification? version) + { + if (FloatRange.TryParse(value, out FloatRange? versionRange)) + { + version = new NuGetFloatRangeSpecification(versionRange!); + return true; + } + version = null; + return false; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/NuGetVersionRangeSpecification.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/NuGetVersionRangeSpecification.cs new file mode 100644 index 000000000000..df76d80688c2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/NuGetVersionRangeSpecification.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Utils; +using NuGet.Versioning; + +namespace Microsoft.TemplateEngine.Edge.Constraints +{ + internal class NuGetVersionRangeSpecification : IVersionSpecification + { + private readonly VersionRange _versionRange; + + internal NuGetVersionRangeSpecification(VersionRange versionRange) + { + _versionRange = versionRange; + } + + public bool CheckIfVersionIsValid(string versionToCheck) + { + if (NuGetVersion.TryParse(versionToCheck, out NuGetVersion? nuGetVersion2)) + { + return _versionRange.Satisfies(nuGetVersion2); + } + return false; + } + + public override string ToString() => _versionRange.ToString(); + + internal static bool TryParse(string value, out NuGetVersionRangeSpecification? version) + { + if (VersionRange.TryParse(value, out VersionRange? versionRange)) + { + version = new NuGetVersionRangeSpecification(versionRange!); + return true; + } + version = null; + return false; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/NuGetVersionSpecification.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/NuGetVersionSpecification.cs new file mode 100644 index 000000000000..a8a457746e19 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/NuGetVersionSpecification.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Utils; +using NuGet.Versioning; + +namespace Microsoft.TemplateEngine.Edge.Constraints +{ + internal class NuGetVersionSpecification : IVersionSpecification + { + private readonly NuGetVersion _version; + + internal NuGetVersionSpecification(NuGetVersion version) + { + _version = version; + } + + public bool CheckIfVersionIsValid(string versionToCheck) + { + if (NuGetVersion.TryParse(versionToCheck, out NuGetVersion? nuGetVersion2)) + { + return _version == nuGetVersion2; + } + return false; + } + + public override string ToString() => _version.ToString(); + + internal static bool TryParse(string value, out NuGetVersionSpecification? version) + { + if (NuGetVersion.TryParse(value, out NuGetVersion? nuGetVersion)) + { + version = new NuGetVersionSpecification(nuGetVersion!); + return true; + } + version = null; + return false; + } + + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/OSConstraint.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/OSConstraint.cs new file mode 100644 index 000000000000..0f6d69865742 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/OSConstraint.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; + +namespace Microsoft.TemplateEngine.Edge.Constraints +{ + public sealed class OSConstraintFactory : ITemplateConstraintFactory + { + private static readonly Dictionary PlatformMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Windows", OSPlatform.Windows }, + { "Linux", OSPlatform.Linux }, + { "OSX", OSPlatform.OSX } + }; + + Guid IIdentifiedComponent.Id { get; } = Guid.Parse("{73DE9788-264A-427B-A26F-2CA3911EE424}"); + + string ITemplateConstraintFactory.Type => "os"; + + Task ITemplateConstraintFactory.CreateTemplateConstraintAsync(IEngineEnvironmentSettings environmentSettings, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult((ITemplateConstraint)new OSConstraint(environmentSettings, this)); + } + + internal class OSConstraint : ConstraintBase + { + internal OSConstraint(IEngineEnvironmentSettings environmentSettings, ITemplateConstraintFactory factory) + : base(environmentSettings, factory) + { } + + public override string DisplayName => LocalizableStrings.OSConstraint_Name; + + protected override TemplateConstraintResult EvaluateInternal(string? args) + { + IEnumerable supportedOS = ParseArgs(args); + + foreach (OSPlatform platform in supportedOS) + { + if (RuntimeInformation.IsOSPlatform(platform)) + { + return TemplateConstraintResult.CreateAllowed(this); + } + } + return TemplateConstraintResult.CreateRestricted(this, string.Format(LocalizableStrings.OSConstraint_Message_Restricted, RuntimeInformation.OSDescription, string.Join(", ", supportedOS))); + } + + //supported configuration: + // "args": "Windows" + // "args": [ "Linux", "Windows" ] + private static IEnumerable ParseArgs(string? args) + { + string supportedValues = string.Join(", ", PlatformMap.Keys.Select(e => $"'{e}'")); + + return args.ParseArrayOfConstraintStrings().Select(Parse); + + OSPlatform Parse(string arg) + { + if (PlatformMap.TryGetValue(arg, out OSPlatform parsedValue)) + { + return parsedValue; + } + throw new ConfigurationException(string.Format(LocalizableStrings.OSConstraint_Error_InvalidOSName, arg, supportedValues)); + } + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/SdkVersionConstraintFactory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/SdkVersionConstraintFactory.cs new file mode 100644 index 000000000000..133b12e86be0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/SdkVersionConstraintFactory.cs @@ -0,0 +1,128 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Components; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Constraints +{ + public sealed class SdkVersionConstraintFactory : ITemplateConstraintFactory + { + Guid IIdentifiedComponent.Id { get; } = Guid.Parse("{4E9721EF-0C02-4C09-A5A4-56C3D29BFC8E}"); + + string ITemplateConstraintFactory.Type => "sdk-version"; + + async Task ITemplateConstraintFactory.CreateTemplateConstraintAsync(IEngineEnvironmentSettings environmentSettings, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + // need to await due to lack of covariance on Tasks + return await SdkVersionConstraint.CreateAsync(environmentSettings, this, cancellationToken).ConfigureAwait(false); + } + + internal class SdkVersionConstraint : ConstraintBase + { + private readonly NuGetVersionSpecification _currentSdkVersion; + private readonly IReadOnlyList _installedSdkVersion; + private readonly Func, IReadOnlyList, string> _remedySuggestionFactory; + + private SdkVersionConstraint( + IEngineEnvironmentSettings environmentSettings, + ITemplateConstraintFactory factory, + NuGetVersionSpecification currentSdkVersion, + IEnumerable installedSdkVersions, + Func, IReadOnlyList, string> remedySuggestionFactory) + : base(environmentSettings, factory) + { + _currentSdkVersion = currentSdkVersion; + _installedSdkVersion = installedSdkVersions.ToList(); + _remedySuggestionFactory = remedySuggestionFactory; + } + + public override string DisplayName => LocalizableStrings.SdkVersionConstraint_Name; + + internal static async Task CreateAsync(IEngineEnvironmentSettings environmentSettings, ITemplateConstraintFactory factory, CancellationToken cancellationToken) + { + (NuGetVersionSpecification currentSdkVersion, IEnumerable installedVersions, Func, IReadOnlyList, string> remedySuggestionFactory) = + await ExtractInstalledSdkVersionAsync( + environmentSettings.Components.OfType(), + cancellationToken).ConfigureAwait(false); + return new SdkVersionConstraint(environmentSettings, factory, currentSdkVersion, installedVersions, remedySuggestionFactory); + } + + protected override TemplateConstraintResult EvaluateInternal(string? args) + { + IReadOnlyList supportedSdks = ParseArgs(args).ToList(); + + foreach (IVersionSpecification supportedSdk in supportedSdks) + { + if (supportedSdk.CheckIfVersionIsValid(_currentSdkVersion.ToString())) + { + return TemplateConstraintResult.CreateAllowed(this); + } + } + + string cta = _remedySuggestionFactory( + VersionSpecificationsToStrings(supportedSdks), + VersionSpecificationsToStrings(_installedSdkVersion.Where(installed => + supportedSdks.Any(supported => supported.CheckIfVersionIsValid(installed.ToString()))))); + + return TemplateConstraintResult.CreateRestricted( + this, + string.Format(LocalizableStrings.SdkConstraint_Message_Restricted, _currentSdkVersion.ToString(), supportedSdks.ToCsvString()), + cta); + } + + private static IReadOnlyList VersionSpecificationsToStrings(IEnumerable versions) + { + return versions.Select(v => v.ToString()).ToList(); + } + + //supported configuration: + // "args": "[7-*]" // single semver nuget compatible version expression + // "args": [ "5.0.100", "6.0.100" ] // multiple version expression - all expressing supported versions + private static IEnumerable ParseArgs(string? args) + { + return args.ParseArrayOfConstraintStrings().Select(Extensions.ParseVersionSpecification); + } + + private static async + Task<(NuGetVersionSpecification CurrentSdkVersion, IEnumerable InstalledVersions, Func, IReadOnlyList, string> RemedySuggestionFactory)> + ExtractInstalledSdkVersionAsync(IEnumerable sdkInfoProviders, CancellationToken cancellationToken) + { + List providers = sdkInfoProviders.ToList(); + + if (providers.Count == 0) + { + throw new ConfigurationException(LocalizableStrings.SdkConstraint_Error_MissingProvider); + } + + if (providers.Count > 1) + { + throw new ConfigurationException( + string.Format(LocalizableStrings.SdkConstraint_Error_MismatchedProviders, providers.Select(p => p.Id).ToCsvString())); + } + + cancellationToken.ThrowIfCancellationRequested(); + string version = await providers[0].GetCurrentVersionAsync(cancellationToken).ConfigureAwait(false); + NuGetVersionSpecification currentSdkVersion = ParseVersion(version); + + cancellationToken.ThrowIfCancellationRequested(); + IEnumerable versions = (await providers[0].GetInstalledVersionsAsync(cancellationToken).ConfigureAwait(false)).Select(ParseVersion); + + return (currentSdkVersion, versions, providers[0].ProvideConstraintRemedySuggestion); + } + + private static NuGetVersionSpecification ParseVersion(string version) + { + if (!NuGetVersionSpecification.TryParse(version, out NuGetVersionSpecification? sdkVersion)) + { + throw new ConfigurationException(string.Format(LocalizableStrings.SdkConstraint_Error_InvalidVersion, version)); + } + + return sdkVersion!; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/WorkloadConstraintFactory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/WorkloadConstraintFactory.cs new file mode 100644 index 000000000000..2869ef6e77df --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Constraints/WorkloadConstraintFactory.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Components; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Constraints +{ + public sealed class WorkloadConstraintFactory : ITemplateConstraintFactory + { + private static readonly SemaphoreSlim Mutex = new(1); + + Guid IIdentifiedComponent.Id { get; } = Guid.Parse("{F8BA5B13-7BD6-47C8-838C-66626526817B}"); + + string ITemplateConstraintFactory.Type => "workload"; + + async Task ITemplateConstraintFactory.CreateTemplateConstraintAsync(IEngineEnvironmentSettings environmentSettings, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + // need to await due to lack of covariance on Tasks + return await WorkloadConstraint.CreateAsync(environmentSettings, this, cancellationToken).ConfigureAwait(false); + } + + internal class WorkloadConstraint : ConstraintBase + { + private readonly HashSet _installedWorkloads; + private readonly string _installedWorkloadsString; + private readonly Func, string> _remedySuggestionFactory; + + private WorkloadConstraint( + IEngineEnvironmentSettings environmentSettings, + ITemplateConstraintFactory factory, + IReadOnlyList workloads, + Func, string> remedySuggestionFactory) + : base(environmentSettings, factory) + { + _installedWorkloads = new HashSet(workloads.Select(w => w.Id), StringComparer.InvariantCultureIgnoreCase); + _installedWorkloadsString = workloads.Select(w => $"{w.Id} \"{w.Description}\"").ToCsvString(); + _remedySuggestionFactory = remedySuggestionFactory; + } + + public override string DisplayName => LocalizableStrings.WorkloadConstraint_Name; + + internal static async Task CreateAsync(IEngineEnvironmentSettings environmentSettings, ITemplateConstraintFactory factory, CancellationToken cancellationToken) + { + (IReadOnlyList workloads, Func, string> remedySuggestionFactory) = + await ExtractWorkloadInfoAsync( + environmentSettings.Components.OfType(), + environmentSettings.Host.Logger, + cancellationToken).ConfigureAwait(false); + return new WorkloadConstraint(environmentSettings, factory, workloads, remedySuggestionFactory); + } + + protected override TemplateConstraintResult EvaluateInternal(string? args) + { + IReadOnlyList supportedWorkloads = ParseArgs(args).ToList(); + + bool isSupportedWorkload = supportedWorkloads.Any(_installedWorkloads.Contains); + + if (isSupportedWorkload) + { + return TemplateConstraintResult.CreateAllowed(this); + } + + return TemplateConstraintResult.CreateRestricted( + this, + string.Format( + LocalizableStrings.WorkloadConstraint_Message_Restricted, + string.Join(", ", supportedWorkloads), + string.Join(", ", _installedWorkloadsString)), + _remedySuggestionFactory(supportedWorkloads)); + } + + //supported configuration: + // "args": "maui-mobile" // workload expected + // "args": [ "maui-mobile", "maui-desktop" ] // any of workloads expected + private static IEnumerable ParseArgs(string? args) + { + return args.ParseArrayOfConstraintStrings(); + } + + private static async Task<(IReadOnlyList Workloads, Func, string> RemedySuggestionFactory)> ExtractWorkloadInfoAsync(IEnumerable workloadsInfoProviders, ILogger logger, CancellationToken token) + { + List providers = workloadsInfoProviders.ToList(); + List? workloads = null; + + if (providers.Count == 0) + { + throw new ConfigurationException(LocalizableStrings.WorkloadConstraint_Error_MissingProvider); + } + + if (providers.Count > 1) + { + throw new ConfigurationException( + string.Format(LocalizableStrings.WorkloadConstraint_Error_MismatchedProviders, providers.Select(p => p.Id).ToCsvString())); + } + + token.ThrowIfCancellationRequested(); + + await Mutex.WaitAsync(token).ConfigureAwait(false); + try + { + IEnumerable currentProviderWorkloads = await providers[0].GetInstalledWorkloadsAsync(token).ConfigureAwait(false); + workloads = currentProviderWorkloads.ToList(); + } + finally + { + Mutex.Release(); + } + + if (workloads.Select(w => w.Id).HasDuplicates(StringComparer.InvariantCultureIgnoreCase)) + { + logger.LogWarning(string.Format( + LocalizableStrings.WorkloadConstraint_Warning_DuplicateWorkloads, + workloads.Select(w => w.Id).GetDuplicates(StringComparer.InvariantCultureIgnoreCase).ToCsvString())); + workloads = workloads + .GroupBy(w => w.Id, StringComparer.InvariantCultureIgnoreCase) + .Select(g => g.First()) + .ToList(); + } + + return (workloads, providers[0].ProvideConstraintRemedySuggestion); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/DefaultEnvironment.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/DefaultEnvironment.cs new file mode 100644 index 000000000000..55df8a7ae55a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/DefaultEnvironment.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Edge +{ + /// + /// Default implementation of . + /// Gets environment variables from . + /// + public class DefaultEnvironment : IEnvironment + { + private const int DefaultBufferWidth = 160; + private readonly IReadOnlyDictionary _environmentVariables; + + public DefaultEnvironment() : this(FetchEnvironmentVariables()) + { } + + protected DefaultEnvironment(IReadOnlyDictionary environmentVariables) + { + _environmentVariables = environmentVariables; + NewLine = Environment.NewLine; + } + + /// + public string NewLine { get; } + + /// + // Console.BufferWidth can throw if there's no console, such as when output is redirected, so + // first check if it is redirected, and fall back to a default value if needed. + public int ConsoleBufferWidth => Console.IsOutputRedirected ? DefaultBufferWidth : Console.BufferWidth; + + /// + public string ExpandEnvironmentVariables(string name) + { + return Environment.ExpandEnvironmentVariables(name); + } + + /// + public string? GetEnvironmentVariable(string name) + { + _environmentVariables.TryGetValue(name, out string? result); + return result; + } + + /// + public IReadOnlyDictionary GetEnvironmentVariables() + { + return _environmentVariables; + } + + protected static IReadOnlyDictionary FetchEnvironmentVariables() + { + Dictionary variables = new Dictionary(StringComparer.OrdinalIgnoreCase); + IDictionary env = Environment.GetEnvironmentVariables(); + + foreach (string key in env.Keys.Cast()) + { + variables[key] = (string)env[key]; + } + + return variables; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/DefaultPathInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/DefaultPathInfo.cs new file mode 100644 index 000000000000..232679a40fd7 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/DefaultPathInfo.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Edge +{ + /// + /// Default implementation of . If custom settings location are not passed, the following locations to be used:
+ /// - global settings: [user profile directory]/.templateengine
+ /// - host settings: [user profile directory]/.templateengine/[]
+ /// - host version settings: [user profile directory]/.templateengine/[]/[]. + ///
+ public sealed class DefaultPathInfo : IPathInfo + { + /// + /// Creates the instance. + /// + /// implementation to be used to get location of user profile directory. + /// implementation. + /// + /// If specified, the directory will be used for storing global settings. + /// Default location: [user profile directory]/.templateengine. + /// + /// + /// If specified, the directory will be used for storing host settings. + /// Default location: host settings: [user profile directory]/.templateengine/[]. + /// + /// + /// If specified, the directory will be used for storing host version settings. + /// Default location: host settings: [user profile directory]/.templateengine/[]/[]. + /// + public DefaultPathInfo( + IEnvironment environment, + ITemplateEngineHost host, + string? globalSettingsDir = null, + string? hostSettingsDir = null, + string? hostVersionSettingsDir = null) + { + UserProfileDir = GetUserProfileDir(environment); + + if (string.IsNullOrWhiteSpace(globalSettingsDir)) + { + globalSettingsDir = GetDefaultGlobalSettingsDir(UserProfileDir); + } + GlobalSettingsDir = globalSettingsDir!; + + if (string.IsNullOrWhiteSpace(hostSettingsDir)) + { + hostSettingsDir = GetDefaultHostSettingsDir(host, userDir: UserProfileDir); + } + HostSettingsDir = hostSettingsDir!; + + if (string.IsNullOrWhiteSpace(hostVersionSettingsDir)) + { + hostVersionSettingsDir = GetDefaultHostVersionSettingsDir(host, userDir: UserProfileDir); + } + HostVersionSettingsDir = hostVersionSettingsDir!; + } + + internal DefaultPathInfo( + IEngineEnvironmentSettings engineEnvironmentSettings, + string? settingsLocation) + { + UserProfileDir = GetUserProfileDir(engineEnvironmentSettings.Environment); + + GlobalSettingsDir = string.IsNullOrWhiteSpace(settingsLocation) ? GetDefaultGlobalSettingsDir(UserProfileDir) : settingsLocation!; + HostSettingsDir = GetDefaultHostSettingsDir(engineEnvironmentSettings.Host, globalDir: GlobalSettingsDir); + HostVersionSettingsDir = GetDefaultHostVersionSettingsDir(engineEnvironmentSettings.Host, globalDir: GlobalSettingsDir); + } + + /// + public string UserProfileDir { get; } + + /// + /// Gets global settings directory. + /// If not specified via constructor, the default location is []/.templateengine. + /// + public string GlobalSettingsDir { get; } + + /// + /// Gets host settings directory. + /// If not specified via constructor, the default location is []/[host identifier]. + /// + public string HostSettingsDir { get; } + + /// + /// Gets host version settings directory. + /// If not specified via constructor, the default location is []/[host identifier]/[host version]. + /// + public string HostVersionSettingsDir { get; } + + private static string GetUserProfileDir(IEnvironment environment) + { + bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + return environment.GetEnvironmentVariable(isWindows ? "USERPROFILE" : "HOME") + ?? throw new NotSupportedException("HOME or USERPROFILE environment variable is not defined, the environment is not supported"); + } + + private static string GetDefaultGlobalSettingsDir(string userDir) + { + return Path.Combine(userDir, ".templateengine"); + } + + private static string GetDefaultHostSettingsDir(ITemplateEngineHost host, string? userDir = null, string? globalDir = null) + { + if (string.IsNullOrWhiteSpace(host.HostIdentifier)) + { + throw new ArgumentException($"{nameof(host.HostIdentifier)} of {nameof(host)} cannot be null or whitespace.", nameof(host)); + } + if (string.IsNullOrWhiteSpace(userDir) && string.IsNullOrWhiteSpace(globalDir)) + { + throw new ArgumentException($"both {nameof(userDir)} and {nameof(globalDir)} cannot be null or whitespace.", nameof(userDir)); + } + return Path.Combine(globalDir ?? GetDefaultGlobalSettingsDir(userDir!), host.HostIdentifier); + } + + private static string GetDefaultHostVersionSettingsDir(ITemplateEngineHost host, string? userDir = null, string? globalDir = null) + { + if (string.IsNullOrWhiteSpace(host.HostIdentifier)) + { + throw new ArgumentException($"{nameof(host.HostIdentifier)} of {nameof(host)} cannot be null or whitespace.", nameof(host)); + } + if (string.IsNullOrWhiteSpace(host.Version)) + { + throw new ArgumentException($"{nameof(host.Version)} of {nameof(host)} cannot be null or whitespace.", nameof(host)); + } + if (string.IsNullOrWhiteSpace(userDir) && string.IsNullOrWhiteSpace(globalDir)) + { + throw new ArgumentException($"both {nameof(userDir)} and {nameof(globalDir)} cannot be null or whitespace.", nameof(userDir)); + } + return Path.Combine(globalDir ?? GetDefaultGlobalSettingsDir(userDir!), host.HostIdentifier, host.Version); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/DefaultTemplateEngineHost.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/DefaultTemplateEngineHost.cs new file mode 100644 index 000000000000..c2def792a6da --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/DefaultTemplateEngineHost.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge +{ + public class DefaultTemplateEngineHost : ITemplateEngineHost + { + private static readonly IReadOnlyList<(Type, IIdentifiedComponent)> NoComponents = []; + private readonly IReadOnlyDictionary _hostDefaults; + private readonly IReadOnlyList<(Type InterfaceType, IIdentifiedComponent Instance)> _hostBuiltInComponents; + + public DefaultTemplateEngineHost( + string hostIdentifier, + string version, + Dictionary? defaults = null, + IReadOnlyList<(Type InterfaceType, IIdentifiedComponent Instance)>? builtIns = null, + IReadOnlyList? fallbackHostTemplateConfigNames = null, + ILoggerFactory? loggerFactory = null) + { + HostIdentifier = hostIdentifier; + Version = version; + _hostDefaults = defaults ?? new Dictionary(); + FileSystem = new PhysicalFileSystem(); + _hostBuiltInComponents = builtIns ?? NoComponents; + FallbackHostTemplateConfigNames = fallbackHostTemplateConfigNames ?? new List(); + + loggerFactory ??= NullLoggerFactory.Instance; + LoggerFactory = loggerFactory; + Logger = LoggerFactory.CreateLogger("Template Engine"); + + WorkingDirectory = Environment.CurrentDirectory; + } + + public IPhysicalFileSystem FileSystem { get; private set; } + + public string HostIdentifier { get; } + + public string WorkingDirectory { get; } + + public IReadOnlyList FallbackHostTemplateConfigNames { get; } + + public string Version { get; } + + public virtual IReadOnlyList<(Type InterfaceType, IIdentifiedComponent Instance)> BuiltInComponents => _hostBuiltInComponents; + + public ILogger Logger { get; } + + public ILoggerFactory LoggerFactory { get; } + + // stub that will be built out soon. + public virtual bool TryGetHostParamDefault(string paramName, out string? value) + { + switch (paramName) + { + case "HostIdentifier": + value = HostIdentifier; + return true; + case "WorkingDirectory": + value = WorkingDirectory; + return true; + } + + return _hostDefaults.TryGetValue(paramName, out value); + } + + public void VirtualizeDirectory(string path) + { + FileSystem = new InMemoryFileSystem(path, FileSystem); + } + + public void Dispose() + { + LoggerFactory?.Dispose(); + } + + #region Obsoleted + [Obsolete] + bool ITemplateEngineHost.OnPotentiallyDestructiveChangesDetected(IReadOnlyList changes, IReadOnlyList destructiveChanges) + { + return true; + } + #endregion + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/EngineEnvironmentSettings.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/EngineEnvironmentSettings.cs new file mode 100644 index 000000000000..9a904a3bbffe --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/EngineEnvironmentSettings.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Edge.Settings; + +namespace Microsoft.TemplateEngine.Edge +{ + /// + /// Default implementation of . + /// + public sealed class EngineEnvironmentSettings : IEngineEnvironmentSettings + { + /// + /// Creates the instance. + /// + /// template engine host creating the instance. + /// if true, the settings directory will be virtualized, so the settings will be stored in memory only. + /// the base location of settings. If specified, the following settings paths will be used:
+ /// - - []
+ /// - - []/[]
+ /// - - []/[]/[].
+ /// If is specified, do not provide . + /// + /// implementation of to use. If not specified, will be used. + /// implementation of to use. If not specified, built-in implementation will be used. + /// implementation of to use. If not specified, will be used (if is used, settings location will be overridden as mentioned in description).
+ /// If is specified, do not provide . + /// + public EngineEnvironmentSettings( + ITemplateEngineHost host, + bool virtualizeSettings = false, + string? settingsLocation = null, + IEnvironment? environment = null, + IComponentManager? componentManager = null, + IPathInfo? pathInfo = null) + { + if (pathInfo != null && !string.IsNullOrWhiteSpace(settingsLocation)) + { + throw new ArgumentException($"{nameof(settingsLocation)} won't be used if {nameof(pathInfo)} is specified.", nameof(settingsLocation)); + } + + Host = host ?? throw new ArgumentNullException(nameof(host)); + Environment = environment ?? new DefaultEnvironment(); + Paths = pathInfo ?? new DefaultPathInfo(this, settingsLocation); + if (virtualizeSettings) + { + Host.VirtualizeDirectory(Paths.GlobalSettingsDir); + } + Components = componentManager ?? new ComponentManager(this); + // In past we created this folder as some file was created + // Checking if folder exists/create folder consumes time + is error prone(we could forget) + // Hence it should be done once, question is when and by who... + // It feels like this is sane place to do it + host.FileSystem.CreateDirectory(Paths.HostVersionSettingsDir); + } + + [Obsolete("ISettingsLoader is obsolete, see obsolete messages for individual properties/methods of ISettingsLoader for details.")] + public ISettingsLoader SettingsLoader => throw new NotSupportedException("ISettingsLoader is no longer supported, see Obsolete message for details."); + + public ITemplateEngineHost Host { get; } + + public IEnvironment Environment { get; } + + public IPathInfo Paths { get; } + + public IComponentManager Components { get; } + + public void Dispose() + { + Host?.Dispose(); + } + + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/FilterableTemplateInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/FilterableTemplateInfo.cs new file mode 100644 index 000000000000..9056525f2b05 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/FilterableTemplateInfo.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.Parameters; + +namespace Microsoft.TemplateEngine.Edge +{ + [Obsolete("This class is deprecated.")] + public class FilterableTemplateInfo : ITemplateInfo, IShortNameList + { + private readonly ITemplateInfo _source; + + public FilterableTemplateInfo() { } + + private FilterableTemplateInfo(ITemplateInfo source) + { + _source = source; + } + + public string Author { get; private set; } + + public string Description { get; private set; } + + public IReadOnlyList Classifications { get; private set; } + + public string DefaultName { get; private set; } + + public string Identity { get; private set; } + + public Guid GeneratorId { get; private set; } + + public string GroupIdentity { get; private set; } + + public int Precedence { get; private set; } + + public string Name { get; private set; } + + public string ShortName { get; private set; } + + public IReadOnlyList ShortNameList { get; set; } + + public IReadOnlyList GroupShortNameList { get; set; } + + public bool PreferDefaultName { get; private set; } + + public IReadOnlyDictionary Tags { get; private set; } + + public IReadOnlyDictionary CacheParameters { get; private set; } + + public IParameterDefinitionSet ParameterDefinitions { get; private set; } + + [Obsolete("Use ParameterDefinitionSet instead.")] + public IReadOnlyList Parameters => ParameterDefinitions; + + public string MountPointUri { get; private set; } + + public string ConfigPlace { get; private set; } + + public string LocaleConfigPlace { get; private set; } + + public string HostConfigPlace { get; private set; } + + public string ThirdPartyNotices { get; private set; } + + public IReadOnlyDictionary BaselineInfo { get; private set; } + + public bool HasScriptRunningPostActions { get; set; } + + public DateTime? ConfigTimestampUtc { get; set; } + + public IReadOnlyDictionary TagsCollection { get; private set; } + + IReadOnlyList ITemplateMetadata.PostActions => _source?.PostActions ?? []; + + IReadOnlyList ITemplateMetadata.Constraints => _source?.Constraints ?? []; + + public static FilterableTemplateInfo FromITemplateInfo(ITemplateInfo source) + { + FilterableTemplateInfo filterableTemplate = new FilterableTemplateInfo(source) + { + Author = source.Author, + Description = source.Description, + Classifications = source.Classifications, + DefaultName = source.DefaultName, + Identity = source.Identity, + GeneratorId = source.GeneratorId, + GroupIdentity = source.GroupIdentity, + Precedence = source.Precedence, + Name = source.Name, + ShortName = source.ShortName, + PreferDefaultName = source.PreferDefaultName, + Tags = source.Tags, + CacheParameters = source.CacheParameters, + ParameterDefinitions = source.ParameterDefinitions, + MountPointUri = source.MountPointUri, + ConfigPlace = source.ConfigPlace, + LocaleConfigPlace = source.LocaleConfigPlace, + HostConfigPlace = source.HostConfigPlace, + ThirdPartyNotices = source.ThirdPartyNotices, + BaselineInfo = source.BaselineInfo, + HasScriptRunningPostActions = source.HasScriptRunningPostActions, + ShortNameList = source.ShortNameList, + TagsCollection = source.TagsCollection, + }; + + return filterableTemplate; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/Folder/FolderInstaller.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/Folder/FolderInstaller.cs new file mode 100644 index 000000000000..bd366077c6d0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/Folder/FolderInstaller.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; + +namespace Microsoft.TemplateEngine.Edge.Installers.Folder +{ + internal class FolderInstaller : IInstaller, ISerializableInstaller + { + private readonly IEngineEnvironmentSettings _settings; + + public FolderInstaller(IEngineEnvironmentSettings settings, IInstallerFactory factory) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + public IInstallerFactory Factory { get; } + + public Task CanInstallAsync(InstallRequest installationRequest, CancellationToken cancellationToken) + { + _ = installationRequest ?? throw new ArgumentNullException(nameof(installationRequest)); + + return Task.FromResult(_settings.Host.FileSystem.DirectoryExists(installationRequest.PackageIdentifier)); + } + + public IManagedTemplatePackage Deserialize(IManagedTemplatePackageProvider provider, TemplatePackageData data) + { + _ = provider ?? throw new ArgumentNullException(nameof(provider)); + if (data.InstallerId != Factory.Id) + { + throw new ArgumentException($"{nameof(FolderInstaller)} can only deserialize packages with {nameof(data.InstallerId)} {Factory.Id}", nameof(data)); + } + + return new FolderManagedTemplatePackage(_settings, this, provider, data.MountPointUri, data.LastChangeTime); + } + + public Task> GetLatestVersionAsync(IEnumerable packages, IManagedTemplatePackageProvider provider, CancellationToken cancellationToken) + { + _ = packages ?? throw new ArgumentNullException(nameof(packages)); + + return Task.FromResult>(packages.Select(s => CheckUpdateResult.CreateSuccess(s, null, true)).ToList()); + } + + public Task InstallAsync(InstallRequest installRequest, IManagedTemplatePackageProvider provider, CancellationToken cancellationToken) + { + _ = installRequest ?? throw new ArgumentNullException(nameof(installRequest)); + _ = provider ?? throw new ArgumentNullException(nameof(provider)); + + if (_settings.Host.FileSystem.DirectoryExists(installRequest.PackageIdentifier)) + { + //on installation we update last modification date to trigger package rebuild. + //on folder package update the date may not change. + return Task.FromResult(InstallResult.CreateSuccess( + installRequest, + new FolderManagedTemplatePackage(_settings, this, provider, installRequest.PackageIdentifier, DateTime.UtcNow), + [])); + } + else + { + return Task.FromResult( + InstallResult.CreateFailure( + installRequest, + InstallerErrorCode.PackageNotFound, + string.Format(LocalizableStrings.FolderInstaller_InstallResult_Error_FolderDoesNotExist, installRequest.PackageIdentifier), + [])); + } + } + + public TemplatePackageData Serialize(IManagedTemplatePackage templatePackage) + { + _ = templatePackage ?? throw new ArgumentNullException(nameof(templatePackage)); + if (templatePackage is not FolderManagedTemplatePackage) + { + throw new ArgumentException($"{nameof(templatePackage)} should be of type {nameof(FolderManagedTemplatePackage)}", nameof(templatePackage)); + } + + FolderManagedTemplatePackage folderTemplatePackage = templatePackage as FolderManagedTemplatePackage + ?? throw new ArgumentException($"{nameof(templatePackage)} should be of type {nameof(FolderManagedTemplatePackage)}", nameof(templatePackage)); + + return new TemplatePackageData(Factory.Id, folderTemplatePackage.MountPointUri, folderTemplatePackage.LastChangeTime, null); + } + + public Task UninstallAsync(IManagedTemplatePackage templatePackage, IManagedTemplatePackageProvider provider, CancellationToken cancellationToken) + { + _ = templatePackage ?? throw new ArgumentNullException(nameof(templatePackage)); + + return Task.FromResult(UninstallResult.CreateSuccess(templatePackage)); + } + + public Task UpdateAsync(UpdateRequest updateRequest, IManagedTemplatePackageProvider provider, CancellationToken cancellationToken) + { + _ = updateRequest ?? throw new ArgumentNullException(nameof(updateRequest)); + + // update installation date + return Task.FromResult(UpdateResult.CreateSuccess( + updateRequest, + new FolderManagedTemplatePackage(_settings, this, provider, updateRequest.TemplatePackage.Identifier, DateTime.UtcNow), + [])); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/Folder/FolderInstallerFactory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/Folder/FolderInstallerFactory.cs new file mode 100644 index 000000000000..f9b312faaebc --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/Folder/FolderInstallerFactory.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; + +namespace Microsoft.TemplateEngine.Edge.Installers.Folder +{ + public sealed class FolderInstallerFactory : IInstallerFactory + { + internal static readonly Guid FactoryId = new("{F01DEA33-E89C-46D1-89C2-1CA1F394C5AA}"); + + Guid IIdentifiedComponent.Id => FactoryId; + + string IInstallerFactory.Name => "Folder"; + + IInstaller IInstallerFactory.CreateInstaller(IEngineEnvironmentSettings settings, string installPath) + { + return new FolderInstaller(settings, this); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/Folder/FolderManagedTemplatePackage.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/Folder/FolderManagedTemplatePackage.cs new file mode 100644 index 000000000000..57119414563d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/Folder/FolderManagedTemplatePackage.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; + +namespace Microsoft.TemplateEngine.Edge.Installers.Folder +{ + internal class FolderManagedTemplatePackage : IManagedTemplatePackage + { + private static readonly Dictionary EmptyDictionary = new Dictionary(); + private readonly IEngineEnvironmentSettings _settings; + private readonly ILogger _logger; + private readonly DateTime _currentLastChangedDateTime; + + public FolderManagedTemplatePackage(IEngineEnvironmentSettings settings, IInstaller installer, IManagedTemplatePackageProvider provider, string mountPointUri, DateTime lastChangeTime) + { + if (string.IsNullOrWhiteSpace(mountPointUri)) + { + throw new ArgumentException($"{nameof(mountPointUri)} cannot be null or empty", nameof(mountPointUri)); + } + MountPointUri = mountPointUri; + _currentLastChangedDateTime = lastChangeTime; + Installer = installer ?? throw new ArgumentNullException(nameof(installer)); + ManagedProvider = provider ?? throw new ArgumentNullException(nameof(provider)); + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _logger = settings.Host.LoggerFactory.CreateLogger(); + } + + public string DisplayName => Identifier; + + public string Identifier => MountPointUri; + + public IInstaller Installer { get; } + + public DateTime LastChangeTime + { + get + { + try + { + //installation or last modification date time: whatever is later. + DateTime physicalWriteTimeUtc = _settings.Host.FileSystem.GetLastWriteTimeUtc(MountPointUri); + return physicalWriteTimeUtc > _currentLastChangedDateTime ? physicalWriteTimeUtc : _currentLastChangedDateTime; + } + catch (Exception e) + { + _logger.LogDebug($"Failed to get last changed time for {MountPointUri}, details: {e}"); + return default; + } + } + } + + public string MountPointUri { get; } + + public ITemplatePackageProvider Provider => ManagedProvider; + + public IManagedTemplatePackageProvider ManagedProvider { get; } + + public string? Version => null; + + public bool IsLocalPackage => true; + + public IReadOnlyDictionary GetDetails() => EmptyDictionary; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/DownloadException.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/DownloadException.cs new file mode 100644 index 000000000000..f2375581de56 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/DownloadException.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + internal class DownloadException : Exception + { + public DownloadException(string packageIdentifier, string packageVersion, string filePath) : base($"Failed to download {packageIdentifier}@{packageVersion} from {filePath}") + { + PackageIdentifier = packageIdentifier; + PackageVersion = packageVersion; + PackageLocation = filePath; + } + + public DownloadException(string packageIdentifier, string packageVersion, IEnumerable attemptedSources) : base($"Failed to download {packageIdentifier}@{packageVersion} from NuGet feeds {string.Join(";", attemptedSources)}") + { + PackageIdentifier = packageIdentifier; + PackageVersion = packageVersion; + SourcesList = attemptedSources; + } + + public DownloadException(string packageIdentifier, string packageVersion, IEnumerable attemptedSources, Exception inner) : base($"Failed to download {packageIdentifier}@{packageVersion} from NuGet feeds {string.Join(";", attemptedSources)}", inner) + { + PackageIdentifier = packageIdentifier; + PackageVersion = packageVersion; + SourcesList = attemptedSources; + } + + public string PackageIdentifier { get; private set; } + + public string? PackageLocation { get; private set; } + + public string PackageVersion { get; private set; } + + public IEnumerable? SourcesList { get; private set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/InvalidNuGetPackageException.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/InvalidNuGetPackageException.cs new file mode 100644 index 000000000000..f1f13b5fffb4 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/InvalidNuGetPackageException.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + internal class InvalidNuGetPackageException : Exception + { + public InvalidNuGetPackageException(string packagePath) : base($"The NuGet package {packagePath} is invalid.") + { + PackageLocation = packagePath; + } + + public InvalidNuGetPackageException(string packagePath, Exception innerException) : base($"The NuGet package {packagePath} is invalid.", innerException) + { + PackageLocation = packagePath; + } + + public string PackageLocation { get; private set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/InvalidNuGetSourceException.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/InvalidNuGetSourceException.cs new file mode 100644 index 000000000000..5e53733c2c4f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/InvalidNuGetSourceException.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + internal class InvalidNuGetSourceException : Exception + { + public InvalidNuGetSourceException(string message) : base(message) { } + + public InvalidNuGetSourceException(string message, IEnumerable sources) + : base(message + ", attempted sources: " + string.Join(", ", sources) + ".") + { + SourcesList = sources; + } + + public InvalidNuGetSourceException(string message, Exception inner) : base(message, inner) { } + + public InvalidNuGetSourceException(string message, IEnumerable sources, Exception inner) + : base(message + ", attempted sources: " + string.Join(", ", sources) + ".", inner) + { + SourcesList = sources; + } + + public IEnumerable? SourcesList { get; private set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/PackageNotFoundException.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/PackageNotFoundException.cs new file mode 100644 index 000000000000..263469b891c0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/PackageNotFoundException.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using NuGet.Versioning; + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + internal class PackageNotFoundException : Exception + { + public PackageNotFoundException(string packageIdentifier, IEnumerable attemptedSources) : base($"{packageIdentifier} was not found in NuGet feeds {string.Join(";", attemptedSources)}") + { + PackageIdentifier = packageIdentifier; + SourcesList = attemptedSources; + } + + public PackageNotFoundException(string packageIdentifier, NuGetVersion packageVersion, IEnumerable attemptedSources) : base($"{packageIdentifier}@{packageVersion} was not found in NuGet feeds {string.Join(";", attemptedSources)}") + { + PackageIdentifier = packageIdentifier; + PackageVersion = packageVersion; + SourcesList = attemptedSources; + } + + public string PackageIdentifier { get; private set; } + + public NuGetVersion? PackageVersion { get; private set; } + + public IEnumerable SourcesList { get; private set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/VulnerablePackageException.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/VulnerablePackageException.cs new file mode 100644 index 000000000000..faccc7cef838 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/Exceptions/VulnerablePackageException.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Installer; + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + internal class VulnerablePackageException : Exception + { + public VulnerablePackageException(string message, string packageIdentifier, string packageVersion, IReadOnlyList vulnerabilities) + : base(message) + { + PackageIdentifier = packageIdentifier; + Vulnerabilities = vulnerabilities; + PackageVersion = packageVersion; + } + + public IReadOnlyList Vulnerabilities { get; internal set; } + + public string PackageIdentifier { get; internal set; } + + public string PackageVersion { get; private set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/IDownloader.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/IDownloader.cs new file mode 100644 index 000000000000..d2592914d443 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/IDownloader.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Installer; + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + internal interface IDownloader + { + Task DownloadPackageAsync(string downloadPath, string identifier, string? version = null, IEnumerable? additionalSources = null, bool force = false, CancellationToken cancellationToken = default); + } + + internal class NuGetPackageInfo + { + public NuGetPackageInfo(string author, string owners, bool reserved, string fullPath, string? nuGetSource, string packageIdentifier, string packageVersion, IReadOnlyList vulnerabilities) + { + Author = author; + Owners = owners; + Reserved = reserved; + FullPath = fullPath; + NuGetSource = nuGetSource; + PackageIdentifier = packageIdentifier; + PackageVersion = packageVersion; + PackageVulnerabilities = vulnerabilities; + } + + public string Author { get; } + + public string Owners { get; } + + public bool Reserved { get; } + + public string FullPath { get; } + + public string? NuGetSource { get; } + + public string PackageIdentifier { get; } + + public string PackageVersion { get; } + + public IReadOnlyList PackageVulnerabilities { get; } + + internal NuGetPackageInfo WithFullPath(string newFullPath) + { + return new NuGetPackageInfo( + Author, + Owners, + Reserved, + newFullPath, + NuGetSource, + PackageIdentifier, + PackageVersion, + PackageVulnerabilities); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/IUpdateChecker.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/IUpdateChecker.cs new file mode 100644 index 000000000000..acf90a25a28a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/IUpdateChecker.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using static Microsoft.TemplateEngine.Edge.Installers.NuGet.NuGetApiPackageManager; + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + internal interface IUpdateChecker + { + Task<(string LatestVersion, bool IsLatestVersion, NugetPackageMetadata PackageMetadata)> GetLatestVersionAsync(string identifier, string? version = null, string? additionalNuGetSource = null, CancellationToken cancellationToken = default); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetInstaller.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetInstaller.cs new file mode 100644 index 000000000000..485946c15fe2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetInstaller.cs @@ -0,0 +1,449 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; +using NuGet.Packaging; +using static Microsoft.TemplateEngine.Edge.Installers.NuGet.NuGetApiPackageManager; + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + internal class NuGetInstaller : IInstaller, ISerializableInstaller + { + private readonly IEngineEnvironmentSettings _environmentSettings; + private readonly ILogger _logger; + private readonly string _installPath; + private readonly IDownloader _packageDownloader; + private readonly IUpdateChecker _updateChecker; + + public NuGetInstaller(IInstallerFactory factory, IEngineEnvironmentSettings settings, string installPath) + { + Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + _environmentSettings = settings ?? throw new ArgumentNullException(nameof(settings)); + _logger = settings.Host.LoggerFactory.CreateLogger(); + + if (string.IsNullOrWhiteSpace(installPath)) + { + throw new ArgumentException($"{nameof(installPath)} should not be null or empty", nameof(installPath)); + } + if (!_environmentSettings.Host.FileSystem.DirectoryExists(installPath)) + { + _environmentSettings.Host.FileSystem.CreateDirectory(installPath); + } + _installPath = installPath; + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(settings); + _packageDownloader = packageManager; + _updateChecker = packageManager; + } + + public NuGetInstaller(IInstallerFactory factory, IEngineEnvironmentSettings settings, string installPath, IDownloader packageDownloader, IUpdateChecker updateChecker) + { + Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + _environmentSettings = settings ?? throw new ArgumentNullException(nameof(settings)); + _logger = settings.Host.LoggerFactory.CreateLogger(); + _packageDownloader = packageDownloader ?? throw new ArgumentNullException(nameof(packageDownloader)); + _updateChecker = updateChecker ?? throw new ArgumentNullException(nameof(updateChecker)); + + if (string.IsNullOrWhiteSpace(installPath)) + { + throw new ArgumentException($"{nameof(installPath)} should not be null or empty", nameof(installPath)); + } + if (!_environmentSettings.Host.FileSystem.DirectoryExists(installPath)) + { + _environmentSettings.Host.FileSystem.CreateDirectory(installPath); + } + _installPath = installPath; + } + + public IInstallerFactory Factory { get; } + + public Task CanInstallAsync(InstallRequest installationRequest, CancellationToken cancellationToken) + { + try + { + ReadPackageInformation(installationRequest.PackageIdentifier); + } + catch (Exception) + { + _logger.LogDebug($"{installationRequest.PackageIdentifier} is not a local NuGet package."); + + //check if identifier is a valid package ID + bool validPackageId = PackageIdValidator.IsValidPackageId(installationRequest.PackageIdentifier); + //check if version is specified it is correct version + bool hasValidVersion = NuGetVersionHelper.IsSupportedVersionString(installationRequest.Version); + if (!validPackageId) + { + _logger.LogDebug($"{installationRequest.PackageIdentifier} is not a valid NuGet package ID."); + } + if (!hasValidVersion) + { + _logger.LogDebug($"{installationRequest.Version} is not a valid NuGet package version."); + } + if (validPackageId && hasValidVersion) + { + _logger.LogDebug($"{installationRequest.DisplayName} is identified as the downloadable NuGet package."); + } + + //not a local package file + return Task.FromResult(validPackageId && hasValidVersion); + } + _logger.LogDebug($"{installationRequest.PackageIdentifier} is identified as the local NuGet package."); + return Task.FromResult(true); + } + + public IManagedTemplatePackage Deserialize(IManagedTemplatePackageProvider provider, TemplatePackageData data) + { + _ = provider ?? throw new ArgumentNullException(nameof(provider)); + if (data.InstallerId != Factory.Id) + { + throw new ArgumentException($"{nameof(NuGetInstaller)} can only deserialize packages with {nameof(data.InstallerId)} {Factory.Id}", nameof(data)); + } + _ = data.Details ?? throw new ArgumentException($"{nameof(data)} should contain {nameof(data.Details)} with package identifier.", nameof(data)); + return NuGetManagedTemplatePackage.Deserialize(_environmentSettings, this, provider, data.MountPointUri, data.Details); + } + + public async Task> GetLatestVersionAsync( + IEnumerable packages, + IManagedTemplatePackageProvider provider, + CancellationToken cancellationToken) + { + _ = packages ?? throw new ArgumentNullException(nameof(packages)); + return await Task.WhenAll(packages.Select(async package => + { + if (package is NuGetManagedTemplatePackage nugetPackage) + { + try + { + (string version, bool isLatestVersion, NugetPackageMetadata packageMetadata) = await _updateChecker.GetLatestVersionAsync( + nugetPackage.Identifier, + nugetPackage.Version, + nugetPackage.NuGetSource, + cancellationToken).ConfigureAwait(false); + + if (packageMetadata.Vulnerabilities != null && packageMetadata.Vulnerabilities.Any()) + { + throw new VulnerablePackageException( + string.Format(LocalizableStrings.NuGetApiPackageManager_UpdateCheckError_VulnerablePackage, nugetPackage.Identifier), + nugetPackage.Identifier, + nugetPackage?.Version ?? string.Empty, + packageMetadata.Vulnerabilities); + } + + nugetPackage.Owners = packageMetadata.Owners; + nugetPackage.Reserved = packageMetadata.PrefixReserved.ToString(); + + return CheckUpdateResult.CreateSuccess(nugetPackage, version, isLatestVersion); + } + catch (PackageNotFoundException e) + { + return CheckUpdateResult.CreateFailure( + package, + InstallerErrorCode.PackageNotFound, + string.Format(LocalizableStrings.NuGetInstaller_Error_FailedToReadPackage, e.PackageIdentifier, string.Join(", ", e.SourcesList)), + []); + } + catch (InvalidNuGetSourceException e) + { + string message = e.SourcesList == null || !e.SourcesList.Any() + ? LocalizableStrings.NuGetInstaller_InstallResult_Error_InvalidSources_None + : string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_InvalidSources, string.Join(", ", e.SourcesList)); + + return CheckUpdateResult.CreateFailure( + package, + InstallerErrorCode.InvalidSource, + message, + []); + } + catch (OperationCanceledException) + { + return CheckUpdateResult.CreateFailure( + package, + InstallerErrorCode.GenericError, + LocalizableStrings.NuGetInstaller_InstallResult_Error_OperationCancelled, + []); + } + catch (VulnerablePackageException e) + { + return CheckUpdateResult.CreateFailure( + package, + InstallerErrorCode.VulnerablePackage, + string.Format(LocalizableStrings.NuGetInstaller_UpdateCheck_Error_VulnerablePackage, nugetPackage.Identifier), + e.Vulnerabilities); + } + catch (Exception e) + { + _logger.LogDebug($"Retrieving latest version for package {package.DisplayName} failed. Details: {e}."); + return CheckUpdateResult.CreateFailure( + package, + InstallerErrorCode.GenericError, + string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_UpdateCheckGeneric, package.DisplayName, e.Message), + []); + } + } + else + { + return CheckUpdateResult.CreateFailure( + package, + InstallerErrorCode.UnsupportedRequest, + string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_PackageNotSupported, package.DisplayName, Factory.Name), + []); + } + })).ConfigureAwait(false); + } + + public async Task InstallAsync(InstallRequest installRequest, IManagedTemplatePackageProvider provider, CancellationToken cancellationToken) + { + _ = installRequest ?? throw new ArgumentNullException(nameof(installRequest)); + _ = provider ?? throw new ArgumentNullException(nameof(provider)); + + if (!await CanInstallAsync(installRequest, cancellationToken).ConfigureAwait(false)) + { + return InstallResult.CreateFailure( + installRequest, + InstallerErrorCode.UnsupportedRequest, + string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_PackageNotSupported, installRequest.DisplayName, Factory.Name), + []); + } + + try + { + bool isLocalPackage = IsLocalPackage(installRequest); + NuGetPackageInfo nuGetPackageInfo; + if (isLocalPackage) + { + nuGetPackageInfo = InstallLocalPackage(installRequest); + } + else + { + string[] additionalNuGetSources = []; + if (installRequest.Details != null && installRequest.Details.TryGetValue(InstallerConstants.NuGetSourcesKey, out string nugetSources)) + { + additionalNuGetSources = nugetSources.Split(InstallerConstants.NuGetSourcesSeparator); + } + + nuGetPackageInfo = await _packageDownloader.DownloadPackageAsync( + _installPath, + installRequest.PackageIdentifier, + installRequest.Version, + additionalNuGetSources, + force: installRequest.Force, + cancellationToken) + .ConfigureAwait(false); + } + + NuGetManagedTemplatePackage package = new NuGetManagedTemplatePackage( + _environmentSettings, + installer: this, + provider, + nuGetPackageInfo.FullPath, + nuGetPackageInfo.PackageIdentifier) + { + Author = nuGetPackageInfo.Author, + Owners = nuGetPackageInfo.Owners, + Reserved = nuGetPackageInfo.Reserved.ToString(), + NuGetSource = nuGetPackageInfo.NuGetSource, + Version = nuGetPackageInfo.PackageVersion.ToString(), + IsLocalPackage = isLocalPackage + }; + + return InstallResult.CreateSuccess(installRequest, package, nuGetPackageInfo.PackageVulnerabilities); + } + catch (DownloadException e) + { + string? packageLocation = e.SourcesList == null + ? e.PackageLocation + : string.Join(", ", e.SourcesList); + + return InstallResult.CreateFailure( + installRequest, + InstallerErrorCode.DownloadFailed, + string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_DownloadFailed, installRequest.DisplayName, packageLocation), + []); + } + catch (PackageNotFoundException e) + { + return InstallResult.CreateFailure( + installRequest, + InstallerErrorCode.PackageNotFound, + string.Format(LocalizableStrings.NuGetInstaller_Error_FailedToReadPackage, e.PackageIdentifier, string.Join(", ", e.SourcesList)), + []); + } + catch (InvalidNuGetSourceException e) + { + string message = e.SourcesList == null || !e.SourcesList.Any() + ? LocalizableStrings.NuGetInstaller_InstallResult_Error_InvalidSources_None + : string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_InvalidSources, string.Join(", ", e.SourcesList)); + + return InstallResult.CreateFailure( + installRequest, + InstallerErrorCode.InvalidSource, + message, + []); + } + catch (InvalidNuGetPackageException e) + { + return InstallResult.CreateFailure( + installRequest, + InstallerErrorCode.InvalidPackage, + string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_InvalidPackage, e.PackageLocation), + []); + } + catch (VulnerablePackageException e) + { + return InstallResult.CreateFailure( + installRequest, + InstallerErrorCode.VulnerablePackage, + string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_VulnerablePackage, e.PackageIdentifier), + e.Vulnerabilities); + } + catch (OperationCanceledException) + { + return InstallResult.CreateFailure( + installRequest, + InstallerErrorCode.GenericError, + LocalizableStrings.NuGetInstaller_InstallResult_Error_OperationCancelled, + []); + } + catch (Exception e) + { + _logger.LogDebug($"Installing {installRequest.DisplayName} failed. Details:{e}"); + return InstallResult.CreateFailure( + installRequest, + InstallerErrorCode.GenericError, + string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_InstallGeneric, installRequest.DisplayName, e.Message), + []); + } + } + + public TemplatePackageData Serialize(IManagedTemplatePackage templatePackage) + { + _ = templatePackage ?? throw new ArgumentNullException(nameof(templatePackage)); + NuGetManagedTemplatePackage nuGetTemplatePackage = templatePackage as NuGetManagedTemplatePackage + ?? throw new ArgumentException($"{nameof(templatePackage)} should be of type {nameof(NuGetManagedTemplatePackage)}", nameof(templatePackage)); + + return new TemplatePackageData( + Factory.Id, + nuGetTemplatePackage.MountPointUri, + nuGetTemplatePackage.LastChangeTime, + nuGetTemplatePackage.Details); + } + + public Task UninstallAsync(IManagedTemplatePackage templatePackage, IManagedTemplatePackageProvider provider, CancellationToken cancellationToken) + { + _ = templatePackage ?? throw new ArgumentNullException(nameof(templatePackage)); + if (templatePackage is not NuGetManagedTemplatePackage) + { + return Task.FromResult(UninstallResult.CreateFailure( + templatePackage, + InstallerErrorCode.UnsupportedRequest, + string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_PackageNotSupported, templatePackage.DisplayName, Factory.Name))); + } + try + { + _environmentSettings.Host.FileSystem.FileDelete(templatePackage.MountPointUri); + return Task.FromResult(UninstallResult.CreateSuccess(templatePackage)); + } + catch (Exception e) + { + _logger.LogDebug("Uninstalling {0} failed. Details:{1}", templatePackage.DisplayName, e); + return Task.FromResult(UninstallResult.CreateFailure( + templatePackage, + InstallerErrorCode.GenericError, + string.Format(LocalizableStrings.NuGetInstaller_InstallResult_Error_UninstallGeneric, templatePackage.DisplayName, e.Message))); + } + } + + public async Task UpdateAsync(UpdateRequest updateRequest, IManagedTemplatePackageProvider provider, CancellationToken cancellationToken) + { + _ = updateRequest ?? throw new ArgumentNullException(nameof(updateRequest)); + _ = provider ?? throw new ArgumentNullException(nameof(provider)); + + if (string.IsNullOrWhiteSpace(updateRequest.Version)) + { + throw new ArgumentException("Version cannot be null or empty", nameof(updateRequest.Version)); + } + + //ensure uninstall is performed + UninstallResult uninstallResult = await UninstallAsync(updateRequest.TemplatePackage, provider, cancellationToken).ConfigureAwait(false); + if (!uninstallResult.Success) + { + if (uninstallResult.ErrorMessage is null) + { + throw new InvalidOperationException($"{nameof(uninstallResult.ErrorMessage)} cannot be null when {nameof(uninstallResult.Success)} is 'true'"); + } + return UpdateResult.CreateFailure(updateRequest, uninstallResult.Error, uninstallResult.ErrorMessage, []); + } + + Dictionary installationDetails = new Dictionary(); + if (updateRequest.TemplatePackage is NuGetManagedTemplatePackage nuGetManagedSource && !string.IsNullOrWhiteSpace(nuGetManagedSource.NuGetSource)) + { + installationDetails.Add(InstallerConstants.NuGetSourcesKey, nuGetManagedSource.NuGetSource!); + } + InstallRequest installRequest = new InstallRequest(updateRequest.TemplatePackage.Identifier, updateRequest.Version, details: installationDetails); + return UpdateResult.FromInstallResult(updateRequest, await InstallAsync(installRequest, provider, cancellationToken).ConfigureAwait(false)); + } + + private bool IsLocalPackage(InstallRequest installRequest) + { + return _environmentSettings.Host.FileSystem.FileExists(installRequest.PackageIdentifier); + } + + private NuGetPackageInfo InstallLocalPackage(InstallRequest installRequest) + { + _ = installRequest ?? throw new ArgumentNullException(nameof(installRequest)); + + NuGetPackageInfo packageInfo; + try + { + packageInfo = ReadPackageInformation(installRequest.PackageIdentifier); + } + catch (Exception ex) + { + _logger.LogError(string.Format(LocalizableStrings.NuGetInstaller_Error_FailedToReadPackage, installRequest.PackageIdentifier)); + _logger.LogDebug($"Details: {ex}."); + throw new InvalidNuGetPackageException(installRequest.PackageIdentifier, ex); + } + string targetPackageLocation = Path.Combine(_installPath, packageInfo.PackageIdentifier + "." + packageInfo.PackageVersion + ".nupkg"); + if (!installRequest.Force && _environmentSettings.Host.FileSystem.FileExists(targetPackageLocation)) + { + _logger.LogError(string.Format(LocalizableStrings.NuGetInstaller_Error_CopyFailed, installRequest.PackageIdentifier, targetPackageLocation)); + _logger.LogError(string.Format(LocalizableStrings.NuGetInstaller_Error_FileAlreadyExists, targetPackageLocation)); + throw new DownloadException(packageInfo.PackageIdentifier, packageInfo.PackageVersion, installRequest.PackageIdentifier); + } + try + { + _environmentSettings.Host.FileSystem.FileCopy(installRequest.PackageIdentifier, targetPackageLocation, overwrite: installRequest.Force); + packageInfo = packageInfo.WithFullPath(targetPackageLocation); + } + catch (Exception ex) + { + _logger.LogError(string.Format(LocalizableStrings.NuGetInstaller_Error_CopyFailed, installRequest.PackageIdentifier, targetPackageLocation), null, 0); + _logger.LogDebug($"Details: {ex}."); + throw new DownloadException(packageInfo.PackageIdentifier, packageInfo.PackageVersion, installRequest.PackageIdentifier); + } + return packageInfo; + } + + private NuGetPackageInfo ReadPackageInformation(string packageLocation) + { + using Stream inputStream = _environmentSettings.Host.FileSystem.OpenRead(packageLocation); + using PackageArchiveReader reader = new PackageArchiveReader(inputStream); + + NuspecReader nuspec = reader.NuspecReader; + + return new NuGetPackageInfo( + nuspec.GetAuthors(), + nuspec.GetOwners(), + // The prefix reservation is not applicable to local packages. + reserved: false, + packageLocation, + null, + nuspec.GetId(), + nuspec.GetVersion().ToNormalizedString(), + []); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetInstallerFactory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetInstallerFactory.cs new file mode 100644 index 000000000000..41fa55fc8b17 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetInstallerFactory.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + public sealed class NuGetInstallerFactory : IInstallerFactory + { + internal static readonly Guid FactoryId = new("{015DCBAC-B4A5-49EA-94A6-061616EB60E2}"); + + Guid IIdentifiedComponent.Id => FactoryId; + + string IInstallerFactory.Name => "NuGet"; + + IInstaller IInstallerFactory.CreateInstaller(IEngineEnvironmentSettings settings, string installPath) + { + return new NuGetInstaller(this, settings, installPath); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetLogger.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetLogger.cs new file mode 100644 index 000000000000..e94062cf1ddd --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetLogger.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using NuGet.Common; +using INuGetLogger = global::NuGet.Common.ILogger; +using NuGetLogLevel = global::NuGet.Common.LogLevel; + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + /// + /// Default logger to be used with NuGet API. It forwards all the messages to different methods of ITemplateEngineHost depending on the log level. + /// + internal class NuGetLogger : INuGetLogger + { + private readonly Microsoft.Extensions.Logging.ILogger _baseLogger; + + internal NuGetLogger(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) + { + _baseLogger = loggerFactory.CreateLogger("NuGetLogger") ?? throw new System.ArgumentNullException(nameof(loggerFactory)); + } + + public void Log(NuGetLogLevel level, string data) + { + switch (level) + { + case NuGetLogLevel.Debug: LogDebug(data); break; + case NuGetLogLevel.Error: LogError(data); break; + case NuGetLogLevel.Information: LogInformation(data); break; + case NuGetLogLevel.Minimal: LogMinimal(data); break; + case NuGetLogLevel.Verbose: LogVerbose(data); break; + case NuGetLogLevel.Warning: LogWarning(data); break; + } + } + + public void Log(ILogMessage message) + { + Log(message.Level, message.Message); + } + + public Task LogAsync(NuGetLogLevel level, string data) + { + Log(level, data); + return Task.FromResult(0); + } + + public Task LogAsync(ILogMessage message) + { + Log(message); + return Task.FromResult(0); + } + + public void LogDebug(string data) + { + _baseLogger.LogDebug(data); + } + + public void LogError(string data) + { + _baseLogger.LogError(data); + } + + public void LogInformation(string data) + { + //TODO: NuGet is putting too much logs to info level, check if we want this data + _baseLogger.LogDebug(data); + } + + public void LogInformationSummary(string data) + { + _baseLogger.LogInformation(data); + } + + public void LogMinimal(string data) + { + _baseLogger.LogInformation(data); + } + + public void LogVerbose(string data) + { + _baseLogger.LogDebug(data); + } + + public void LogWarning(string data) + { + _baseLogger.LogWarning(data); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetManagedTemplatePackage.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetManagedTemplatePackage.cs new file mode 100644 index 000000000000..9256e9e53e43 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetManagedTemplatePackage.cs @@ -0,0 +1,188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + internal class NuGetManagedTemplatePackage : IManagedTemplatePackage + { + private const string AuthorKey = "Author"; + private const string LocalPackageKey = "LocalPackage"; + private const string OwnersKey = "Owners"; + private const string ReservedKey = "Reserved"; + private const string NuGetSourceKey = "NuGetSource"; + private const string PackageIdKey = "PackageId"; + private const string PackageVersionKey = "Version"; + + private readonly IEngineEnvironmentSettings _settings; + private readonly ILogger _logger; + + public NuGetManagedTemplatePackage( + IEngineEnvironmentSettings settings, + IInstaller installer, + IManagedTemplatePackageProvider provider, + string mountPointUri, + string packageIdentifier) + { + if (string.IsNullOrWhiteSpace(mountPointUri)) + { + throw new ArgumentException($"{nameof(mountPointUri)} cannot be null or empty", nameof(mountPointUri)); + } + if (string.IsNullOrWhiteSpace(packageIdentifier)) + { + throw new ArgumentException($"{nameof(packageIdentifier)} cannot be null or empty", nameof(packageIdentifier)); + } + MountPointUri = mountPointUri; + Installer = installer ?? throw new ArgumentNullException(nameof(installer)); + ManagedProvider = provider ?? throw new ArgumentNullException(nameof(provider)); + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _logger = settings.Host.LoggerFactory.CreateLogger(); + + Details = new Dictionary + { + [PackageIdKey] = packageIdentifier + }; + } + + /// + /// Private constructor used for de-serialization only. + /// + private NuGetManagedTemplatePackage( + IEngineEnvironmentSettings settings, + IInstaller installer, + IManagedTemplatePackageProvider provider, + string mountPointUri, + IReadOnlyDictionary details) + { + if (string.IsNullOrWhiteSpace(mountPointUri)) + { + throw new ArgumentException($"{nameof(mountPointUri)} cannot be null or empty", nameof(mountPointUri)); + } + MountPointUri = mountPointUri; + Installer = installer ?? throw new ArgumentNullException(nameof(installer)); + ManagedProvider = provider ?? throw new ArgumentNullException(nameof(provider)); + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + Details = details?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? throw new ArgumentNullException(nameof(details)); + if (Details.TryGetValue(PackageIdKey, out string packageId)) + { + if (string.IsNullOrWhiteSpace(packageId)) + { + throw new ArgumentException($"{nameof(details)} should contain key {PackageIdKey} with non-empty value", nameof(details)); + } + } + else + { + throw new ArgumentException($"{nameof(details)} should contain key {PackageIdKey}", nameof(details)); + } + _logger = settings.Host.LoggerFactory.CreateLogger(); + } + + public ITemplatePackageProvider Provider => ManagedProvider; + + public IManagedTemplatePackageProvider ManagedProvider { get; } + + public string DisplayName => string.IsNullOrWhiteSpace(Version) ? Identifier : $"{Identifier}@{Version}"; + + public string Identifier => Details[PackageIdKey]; + + public IInstaller Installer { get; } + + public string MountPointUri { get; } + + public DateTime LastChangeTime + { + get + { + try + { + return _settings.Host.FileSystem.GetLastWriteTimeUtc(MountPointUri); + } + catch (Exception e) + { + _logger.LogDebug($"Failed to get last changed time for {MountPointUri}, details: {e}"); + return default; + } + } + } + + public string? Reserved + { + get => Details.TryGetValue(ReservedKey, out string reserved) ? reserved : false.ToString(); + set => UpdateOrRemoveValue(Details, ReservedKey, value, (entry) => !string.IsNullOrEmpty(entry)); + } + + public string? Author + { + get => Details.TryGetValue(AuthorKey, out string author) ? author : null; + set => UpdateOrRemoveValue(Details, AuthorKey, value, (entry) => !string.IsNullOrEmpty(entry)); + } + + public string? Owners + { + get => Details.TryGetValue(OwnersKey, out string owners) ? owners : null; + set => UpdateOrRemoveValue(Details, OwnersKey, value, (entry) => !string.IsNullOrEmpty(entry)); + } + + public bool IsLocalPackage + { + get + { + if (Details.TryGetValue(LocalPackageKey, out string val) && bool.TryParse(val, out bool isLocalPackage)) + { + return isLocalPackage; + } + return false; + } + set => UpdateOrRemoveValue(Details, LocalPackageKey, value.ToString(), (value) => value == true.ToString()); + } + + public string? NuGetSource + { + get => Details.TryGetValue(NuGetSourceKey, out string nugetSource) ? nugetSource : null; + set => UpdateOrRemoveValue(Details, NuGetSourceKey, value, (entry) => !string.IsNullOrEmpty(entry)); + } + + public string? Version + { + get => Details.TryGetValue(PackageVersionKey, out string version) ? version : null; + set => UpdateOrRemoveValue(Details, PackageVersionKey, value, (entry) => !string.IsNullOrEmpty(entry)); + } + + internal Dictionary Details { get; } + + public static NuGetManagedTemplatePackage Deserialize( + IEngineEnvironmentSettings settings, + IInstaller installer, + IManagedTemplatePackageProvider provider, + string mountPointUri, + IReadOnlyDictionary details) + { + return new NuGetManagedTemplatePackage(settings, installer, provider, mountPointUri, details); + } + + public IReadOnlyDictionary GetDetails() + { + var details = new Dictionary(); + + details.TryAdd(AuthorKey, Author ?? string.Empty, (entry) => !string.IsNullOrEmpty(entry)); + details.TryAdd(OwnersKey, Owners ?? string.Empty, (entry) => !string.IsNullOrEmpty(entry)); + details.TryAdd(ReservedKey, Reserved ?? string.Empty, (entry) => !string.IsNullOrEmpty(entry)); + details.TryAdd(NuGetSourceKey, NuGetSource ?? string.Empty, (entry) => !string.IsNullOrEmpty(entry)); + + return details; + } + + private void UpdateOrRemoveValue(IDictionary dict, TKey key, TValue? value, Predicate condition) + { + if (value is null || !dict.TryAdd(key, value, condition)) + { + dict.Remove(key); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetVersionHelper.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetVersionHelper.cs new file mode 100644 index 000000000000..f6627490c187 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NuGetVersionHelper.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using NuGet.Versioning; + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + internal static class NuGetVersionHelper + { + // '*' + private static readonly FloatRange UnspecifiedVersion = new FloatRange(NuGetVersionFloatBehavior.Major); + + public static bool IsSupportedVersionString(string? versionString) + { + return + string.IsNullOrEmpty(versionString) + || + NuGetVersion.TryParse(versionString, out _) + || + FloatRange.TryParse(versionString!, out _); + } + + public static bool IsUnrestricted(this FloatRange floatRange) + { + return floatRange.Equals(UnspecifiedVersion); + } + + /// + /// Tries to parse given string and return provided there + /// is an existing floating range behavior in the provided string. + /// null or empty string are regarded to be requests to any release version (behavior identical to '*'). + /// + /// Input string to be parsed. + /// Output parameter, populated in case function returned true. + /// + public static bool TryParseFloatRangeEx(string? versionString, out FloatRange floatRange) + { + floatRange = + string.IsNullOrEmpty(versionString) ? + UnspecifiedVersion : FloatRange.Parse(versionString!); + + return floatRange.FloatBehavior != NuGetVersionFloatBehavior.None; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NugetApiPackageManager.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NugetApiPackageManager.cs new file mode 100644 index 000000000000..3accc85a2f63 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Installers/NuGet/NugetApiPackageManager.cs @@ -0,0 +1,532 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using NuGet.Configuration; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; +using ILogger = NuGet.Common.ILogger; + +namespace Microsoft.TemplateEngine.Edge.Installers.NuGet +{ + internal class NuGetApiPackageManager : IDownloader, IUpdateChecker + { + private static readonly ConcurrentDictionary SourcesCache = new(); + private readonly IEngineEnvironmentSettings _environmentSettings; + private readonly ILogger _nugetLogger; + + private readonly SourceCacheContext _cacheSettings = new SourceCacheContext() + { + NoCache = true, + DirectDownload = true + }; + + internal NuGetApiPackageManager(IEngineEnvironmentSettings settings) + { + _environmentSettings = settings ?? throw new ArgumentNullException(nameof(settings)); + _nugetLogger = new NuGetLogger(_environmentSettings.Host.LoggerFactory); + } + + /// + /// Downloads the package from configured NuGet package feeds. NuGet feeds to use are read for current directory, if additional feeds are specified in installation request, they are checked as well. + /// + /// path to download to. + /// NuGet package identifier. + /// The version to download. If empty, the latest stable version will be downloaded. If stable version is not available, the latest preview will be downloaded. + /// Additional NuGet feeds to use (in addition to default feeds configured for current directory). + /// If true, overwriting existing package is allowed. + /// + /// containing full path to downloaded package and package details. + /// when sources passed to install request are not valid NuGet sources or failed to read default NuGet configuration. + /// when the download of the package failed. + /// when the package cannot be find in default or passed to install request NuGet feeds. + /// when the package has any vulnerabilities. + public async Task DownloadPackageAsync(string downloadPath, string identifier, string? version = null, IEnumerable? additionalSources = null, bool force = false, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(identifier)) + { + throw new ArgumentException($"{nameof(identifier)} cannot be null or empty", nameof(identifier)); + } + if (string.IsNullOrWhiteSpace(downloadPath)) + { + throw new ArgumentException($"{nameof(downloadPath)} cannot be null or empty", nameof(downloadPath)); + } + + IEnumerable packagesSources = LoadNuGetSources(additionalSources?.ToArray() ?? []); + + if (!force) + { + packagesSources = RemoveInsecurePackages(packagesSources); + } + + PackageSource source; + NugetPackageMetadata packageMetadata; + + if (NuGetVersionHelper.TryParseFloatRangeEx(version, out FloatRange floatRange)) + { + (source, packageMetadata) = + await GetLatestVersionInternalAsync( + identifier, + packagesSources, + floatRange, + cancellationToken) + .ConfigureAwait(false); + } + else + { + NuGetVersion packageVersion = new NuGetVersion(version!); + (source, packageMetadata) = await GetPackageMetadataAsync(identifier, packageVersion, packagesSources, cancellationToken).ConfigureAwait(false); + } + + if (packageMetadata.Vulnerabilities.Any() && !force) + { + var foundPackageVersion = packageMetadata.Identity.Version.OriginalVersion; + throw new VulnerablePackageException( + string.Format(LocalizableStrings.NuGetApiPackageManager_DownloadError_VulnerablePackage, source), + packageMetadata.Identity.Id, + foundPackageVersion!, + packageMetadata.Vulnerabilities); + } + + FindPackageByIdResource resource; + SourceRepository repository = SourcesCache.GetOrAdd(source, Repository.Factory.GetCoreV3(source)); + try + { + resource = await repository.GetResourceAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + _nugetLogger.LogError(string.Format(LocalizableStrings.NuGetApiPackageManager_Error_FailedToLoadSource, source.Source)); + _nugetLogger.LogDebug($"Details: {e}."); + throw new InvalidNuGetSourceException("Failed to load NuGet source", new[] { source.Source }, e); + } + + string filePath = Path.Combine(downloadPath, packageMetadata.Identity.Id + "." + packageMetadata.Identity.Version + ".nupkg"); + if (!force && _environmentSettings.Host.FileSystem.FileExists(filePath)) + { + _nugetLogger.LogError(string.Format(LocalizableStrings.NuGetApiPackageManager_Error_FileAlreadyExists, filePath)); + throw new DownloadException(packageMetadata.Identity.Id, packageMetadata.Identity.Version.ToNormalizedString(), new[] { source.Source }); + } + try + { + using Stream packageStream = _environmentSettings.Host.FileSystem.CreateFile(filePath); + if (await resource.CopyNupkgToStreamAsync( + packageMetadata.Identity.Id, + packageMetadata.Identity.Version, + packageStream, + _cacheSettings, + _nugetLogger, + cancellationToken).ConfigureAwait(false)) + { + return new NuGetPackageInfo( + packageMetadata.Authors, + packageMetadata.Owners, + reserved: packageMetadata.PrefixReserved, + filePath, + source.Source, + packageMetadata.Identity.Id, + packageMetadata.Identity.Version.ToNormalizedString(), + packageMetadata.Vulnerabilities); + } + else + { + _nugetLogger.LogWarning( + string.Format( + LocalizableStrings.NuGetApiPackageManager_Warning_FailedToDownload, + $"{packageMetadata.Identity.Id}@{packageMetadata.Identity.Version}", + source.Source)); + try + { + _environmentSettings.Host.FileSystem.FileDelete(filePath); + } + catch (Exception ex) + { + _nugetLogger.LogWarning( + string.Format( + LocalizableStrings.NuGetApiPackageManager_Warning_FailedToDelete, + filePath)); + _nugetLogger.LogDebug($"Details: {ex}."); + } + throw new DownloadException(packageMetadata.Identity.Id, packageMetadata.Identity.Version.ToNormalizedString(), new[] { source.Source }); + } + } + catch (Exception e) + { + _nugetLogger.LogWarning( + string.Format( + LocalizableStrings.NuGetApiPackageManager_Warning_FailedToDownload, + $"{packageMetadata.Identity.Id}@{packageMetadata.Identity.Version}", + source.Source)); + _nugetLogger.LogDebug($"Details: {e}."); + try + { + _environmentSettings.Host.FileSystem.FileDelete(filePath); + } + catch (Exception ex) + { + _nugetLogger.LogWarning( + string.Format( + LocalizableStrings.NuGetApiPackageManager_Warning_FailedToDelete, + filePath)); + _nugetLogger.LogDebug($"Details: {ex}."); + } + throw new DownloadException(packageMetadata.Identity.Id, packageMetadata.Identity.Version.ToNormalizedString(), new[] { source.Source }, e.InnerException); + } + } + + /// + /// Gets the latest stable version for the package. If the package has preview version installed, returns the latest preview. + /// Uses NuGet feeds configured for current directory and the source if specified from . + /// + /// NuGet package identifier. + /// current version of NuGet package. + /// additional NuGet feeds to check from. + /// + /// the latest version for the and indication if installed version is latest. + /// when sources passed to install request are not valid NuGet feeds or failed to read default NuGet configuration. + /// when the package cannot be find in default or source NuGet feeds. + public async Task<(string LatestVersion, bool IsLatestVersion, NugetPackageMetadata PackageMetadata)> GetLatestVersionAsync(string identifier, string? version = null, string? additionalSource = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(identifier)) + { + throw new ArgumentException($"{nameof(identifier)} cannot be null or empty", nameof(identifier)); + } + + //if preview version is installed, check for the latest preview version, otherwise for latest stable + bool previewVersionInstalled = false; + if (NuGetVersion.TryParse(version, out NuGetVersion? currentVersion)) + { + previewVersionInstalled = currentVersion!.IsPrerelease; + } + + FloatRange floatRange = new FloatRange(previewVersionInstalled ? NuGetVersionFloatBehavior.AbsoluteLatest : NuGetVersionFloatBehavior.Major); + + string[] additionalSources = string.IsNullOrWhiteSpace(additionalSource) ? [] : new[] { additionalSource! }; + IEnumerable packageSources = LoadNuGetSources(additionalSources); + var (_, package) = await GetLatestVersionInternalAsync(identifier, packageSources, floatRange, cancellationToken).ConfigureAwait(false); + bool isLatestVersion = currentVersion != null && currentVersion >= package.Identity.Version; + + return (package.Identity.Version.ToNormalizedString(), isLatestVersion, package); + } + + internal IEnumerable RemoveInsecurePackages(IEnumerable packagesSources) + { + var insecurePackages = new List(); + var securePackages = new List(); + foreach (var packageSource in packagesSources) + { + // NuGet IsHttp property can be both http and https sources + if (packageSource.IsHttp && !packageSource.IsHttps) + { + insecurePackages.Add(packageSource); + } + else + { + securePackages.Add(packageSource); + } + } + + if (insecurePackages.Any()) + { + var packagesString = string.Join(", ", insecurePackages.Select(package => package.Source)); + _nugetLogger.LogWarning(string.Format(LocalizableStrings.NuGetApiPackageManager_Warning_InsecureFeed, packagesString)); + } + + return securePackages; + } + + private async Task<(PackageSource, NugetPackageMetadata)> GetLatestVersionInternalAsync( + string packageIdentifier, + IEnumerable packageSources, + FloatRange floatRange, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(packageIdentifier)) + { + throw new ArgumentException($"{nameof(packageIdentifier)} cannot be null or empty", nameof(packageIdentifier)); + } + _ = packageSources ?? throw new ArgumentNullException(nameof(packageSources)); + + (PackageSource Source, IEnumerable? FoundPackages)[] foundPackagesBySource = + await Task.WhenAll( + packageSources.Select(source => GetPackageMetadataAsync(source, packageIdentifier, includePrerelease: true, cancellationToken))) + .ConfigureAwait(false); + + if (!foundPackagesBySource.Any(result => result.FoundPackages != null)) + { + throw new InvalidNuGetSourceException("Failed to load NuGet sources", packageSources.Select(source => source.Source)); + } + + var accumulativeSearchResults = foundPackagesBySource + .Where(result => result.FoundPackages != null) + .SelectMany(result => result.FoundPackages.Select(package => (result.Source, package))); + + (PackageSource, NugetPackageMetadata)? latestVersion = accumulativeSearchResults.Aggregate( + ((PackageSource, NugetPackageMetadata)?)null, + (max, current) => + { + return + (max == null || current.package.Identity.Version > max.Value.Item2.Identity.Version) + && + floatRange.Satisfies(current.package.Identity.Version) ? + current : max; + }); + + // In case no package was found and we haven't been restricting versions - try prerelease as well (so behave like '*-*') + if (latestVersion == null && floatRange.IsUnrestricted()) + { + latestVersion = accumulativeSearchResults.Aggregate( + ((PackageSource, NugetPackageMetadata)?)null, + (max, current) => + { + return + (max == null || current.package.Identity.Version > max.Value.Item2.Identity.Version) + ? current + : max; + }); + } + + if (latestVersion == null) + { + _nugetLogger.LogDebug( + string.Format( + LocalizableStrings.NuGetApiPackageManager_Warning_PackageNotFound, + packageIdentifier, + string.Join(", ", packageSources.Select(source => source.Source)))); + throw new PackageNotFoundException(packageIdentifier, packageSources.Select(source => source.Source)); + } + + return latestVersion.Value; + } + + private async Task<(PackageSource, NugetPackageMetadata)> GetPackageMetadataAsync( + string packageIdentifier, + NuGetVersion packageVersion, + IEnumerable sources, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(packageIdentifier)) + { + throw new ArgumentException($"{nameof(packageIdentifier)} cannot be null or empty", nameof(packageIdentifier)); + } + _ = packageVersion ?? throw new ArgumentNullException(nameof(packageVersion)); + _ = sources ?? throw new ArgumentNullException(nameof(sources)); + + bool atLeastOneSourceValid = false; + using CancellationTokenSource linkedCts = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + List? FoundPackages)>> tasks = + sources.Select(source => GetPackageMetadataAsync(source, packageIdentifier, includePrerelease: true, linkedCts.Token)).ToList(); + while (tasks.Any()) + { + Task<(PackageSource Source, IEnumerable? FoundPackages)> finishedTask = + await Task.WhenAny(tasks).ConfigureAwait(false); + _ = tasks.Remove(finishedTask); + (PackageSource foundSource, IEnumerable? foundPackages) = await finishedTask.ConfigureAwait(false); + if (foundPackages == null) + { + continue; + } + atLeastOneSourceValid = true; + NugetPackageMetadata? matchedVersion = foundPackages.FirstOrDefault(package => package.Identity.Version == packageVersion); + if (matchedVersion != null) + { + _nugetLogger.LogDebug($"{packageIdentifier}@{packageVersion} was found in {foundSource.Source}."); + linkedCts.Cancel(); + return (foundSource, matchedVersion); + } + else + { + _nugetLogger.LogDebug($"{packageIdentifier}@{packageVersion} is not found in NuGet feed {foundSource.Source}."); + } + } + if (!atLeastOneSourceValid) + { + throw new InvalidNuGetSourceException("Failed to load NuGet sources", sources.Select(s => s.Source)); + } + _nugetLogger.LogWarning( + string.Format( + LocalizableStrings.NuGetApiPackageManager_Warning_PackageNotFound, + $"{packageIdentifier}@{packageVersion}", + string.Join(", ", sources.Select(source => source.Source)))); + throw new PackageNotFoundException(packageIdentifier, packageVersion, sources.Select(source => source.Source)); + } + + private async Task<(PackageSource Source, IEnumerable? FoundPackages)> GetPackageMetadataAsync( + PackageSource source, + string packageIdentifier, + bool includePrerelease = false, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(packageIdentifier)) + { + throw new ArgumentException($"{nameof(packageIdentifier)} cannot be null or empty", nameof(packageIdentifier)); + } + _ = source ?? throw new ArgumentNullException(nameof(source)); + + _nugetLogger.LogDebug($"Searching for {packageIdentifier} in {source.Source}."); + try + { + SourceRepository repository = SourcesCache.GetOrAdd(source, Repository.Factory.GetCoreV3(source)); + PackageMetadataResource resource = await repository.GetResourceAsync(cancellationToken).ConfigureAwait(false); + IEnumerable packageMetadata = await resource.GetMetadataAsync( + packageIdentifier, + includePrerelease: includePrerelease, + includeUnlisted: false, + _cacheSettings, + _nugetLogger, + cancellationToken).ConfigureAwait(false); + + if (packageMetadata.Any()) + { + _nugetLogger.LogDebug($"Found {packageMetadata.Count()} versions for {packageIdentifier} in NuGet feed {source.Source}."); + + // extra call is needed because GetMetadataAsync call doesn't include owners and prefixVerified info + // https://github.com/NuGet/NuGetGallery/issues/5647 + var (owners, verified) = await GetPackageAdditionalMetadata( + repository, + packageIdentifier, + includePrerelease, + cancellationToken).ConfigureAwait(false); + + return (source, packageMetadata.Select(pm => new NugetPackageMetadata(pm, owners, verified))); + } + else + { + _nugetLogger.LogDebug($"{packageIdentifier} is not found in NuGet feed {source.Source}."); + } + + return (source, Enumerable.Empty()); + } + catch (TaskCanceledException) + { + //do nothing + //GetMetadataAsync may cancel the task in case package is found in another feed. + } + catch (Exception ex) + { + _nugetLogger.LogDebug(string.Format(LocalizableStrings.NuGetApiPackageManager_Error_FailedToReadPackage, source.Source)); + _nugetLogger.LogDebug($"Details: {ex}."); + } + return (source, FoundPackages: null); + } + + private async Task<(string Owners, bool Verified)> GetPackageAdditionalMetadata( + SourceRepository repository, + string packageIdentifier, + bool includePrerelease, + CancellationToken cancellationToken) + { + var nugetSearchClient = await repository.GetResourceAsync(cancellationToken).ConfigureAwait(false); + + var searchResult = (await nugetSearchClient.SearchAsync( + packageIdentifier, + new SearchFilter(includePrerelease), + skip: 0, + take: 1, + _nugetLogger, + cancellationToken).ConfigureAwait(false)).FirstOrDefault(); + + return (searchResult.Owners ?? string.Empty, searchResult.PrefixReserved); + } + + private IEnumerable LoadNuGetSources(IEnumerable additionalSources) + { + IEnumerable defaultSources; + string currentDirectory = string.Empty; + try + { + currentDirectory = Directory.GetCurrentDirectory(); + ISettings settings = global::NuGet.Configuration.Settings.LoadDefaultSettings(currentDirectory); + PackageSourceProvider packageSourceProvider = new PackageSourceProvider(settings); + defaultSources = packageSourceProvider.LoadPackageSources().Where(source => source.IsEnabled); + } + catch (Exception ex) + { + _nugetLogger.LogError(string.Format(LocalizableStrings.NuGetApiPackageManager_Error_FailedToLoadSources, currentDirectory)); + _nugetLogger.LogDebug($"Details: {ex}."); + throw new InvalidNuGetSourceException($"Failed to load NuGet sources configured for the folder {currentDirectory}", ex); + } + + if (!additionalSources.Any()) + { + if (!defaultSources.Any()) + { + _nugetLogger.LogError(LocalizableStrings.NuGetApiPackageManager_Error_NoSources); + throw new InvalidNuGetSourceException("No NuGet sources are defined or enabled"); + } + return defaultSources; + } + + List customSources = new List(); + foreach (string source in additionalSources) + { + if (string.IsNullOrWhiteSpace(source)) + { + continue; + } + if (defaultSources.Any(s => s.Source.Equals(source, StringComparison.OrdinalIgnoreCase))) + { + _nugetLogger.LogDebug($"Custom source {source} is already loaded from default configuration."); + continue; + } + PackageSource packageSource = new PackageSource(source); + if (packageSource.TrySourceAsUri == null) + { + _nugetLogger.LogWarning(string.Format(LocalizableStrings.NuGetApiPackageManager_Warning_FailedToLoadSource, source)); + continue; + } + customSources.Add(packageSource); + } + + IEnumerable retrievedSources = customSources.Concat(defaultSources); + if (!retrievedSources.Any()) + { + _nugetLogger.LogError(LocalizableStrings.NuGetApiPackageManager_Error_NoSources); + throw new InvalidNuGetSourceException("No NuGet sources are defined or enabled"); + } + return retrievedSources; + } + + internal class NugetPackageMetadata + { + public NugetPackageMetadata(IPackageSearchMetadata metadata, string owners, bool reserved) + { + Authors = metadata.Authors; + Identity = metadata.Identity; + PrefixReserved = reserved; + Owners = owners; + Vulnerabilities = ConvertVulnerabilityMetadata(metadata.Vulnerabilities); + } + + public string Authors { get; } + + public PackageIdentity Identity { get; } + + public string Owners { get; } + + public bool PrefixReserved { get; } + + public IReadOnlyList Vulnerabilities { get; } + + private IReadOnlyList ConvertVulnerabilityMetadata(IEnumerable? vulnerabilities) + { + if (vulnerabilities is null) + { + return []; + } + + return vulnerabilities.GroupBy(x => x.Severity) + .Select(g => new VulnerabilityInfo( + g.Key, + g.Select(x => x.AdvisoryUrl.AbsoluteUri).ToArray())) + .OrderBy(x => x.Severity) + .ToList(); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/LocalizableStrings.resx b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/LocalizableStrings.resx new file mode 100644 index 000000000000..3474d168dfab --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/LocalizableStrings.resx @@ -0,0 +1,469 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + {0} is the dependency chain + + + '{0}' should not contain empty items + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + + + '{0}' does not contain valid items. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + + + Environment variables + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + + + The folder {0} doesn't exist. + + + Check the constraint configuration in template.json. + + + latest version + small letters, string is used in the sentence + + + version {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + + + {0} is already installed. + + + {0} cannot be installed. + + + {0} is already installed, it will be replaced with {1}. + + + {0} was successfully uninstalled. + + + '{0}' does not have mandatory property '{1}'. + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + + + Host defined parameters + + + The checked package {0} is vulnerable. + + + Found package is vulnerable source: {0} + + + Failed to load the NuGet source {0}. + + + Failed to load NuGet sources configured for the folder {0}. + + + Failed to read package information from NuGet source {0}. + + + File {0} already exists. + + + No NuGet sources are defined or enabled. + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + + + Failed to download {0} from NuGet feed {1}. + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + + + {0} is not found in NuGet feeds {1}. + + + Failed to copy package {0} to {1}. + + + Failed to read content of package {0}. + + + File {0} already exists. + + + Failed to download {0} from {1}. + + + Failed to install the package {0}. +Details: {1}. + + + The install request {0} cannot be processed by installer {1}. + + + The NuGet package {0} is invalid. + + + The requested package {0} has vulnerabilities. + + + The configured NuGet sources are invalid: {0}. + + + No NuGet sources are configured. + + + The operation was cancelled. + + + {0} was not found in NuGet feeds {1}. + + + The package {0} is not supported by installer {1}. + + + Failed to uninstall the package {0}. +Details: {1}. + + + The checked package {0} has vulnerabilities. + + + Failed to check the update for the package {0}. +Details: {1}. + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + + + The template {0} has the following validation errors: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + '{0}' is not a valid semver version. + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + {0} is constraint type + + + Could not load template. + + + Failed to create template. +Details: {0} + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + + + Destructive changes detected. + + + Failed to load host data in {0} at {1}. + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to retrieve package with identifier '{0}'. + + + Failed to scan {0}. +Details: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + {0} should not end with period - period is a part of the format entry. + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + {0} is the list of duplicates + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + * 'TEMPLATE' from 'PACKAGE_ID' + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + + + The template is invalid and cannot be instantiated. + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Microsoft.TemplateEngine.Edge.csproj b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Microsoft.TemplateEngine.Edge.csproj new file mode 100644 index 000000000000..b6bd9226e0d2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Microsoft.TemplateEngine.Edge.csproj @@ -0,0 +1,53 @@ + + + + $(NetMinimum);$(NetCurrent);netstandard2.0;$(NetFrameworkMinimum) + Helper package for adding Template Engine to consuming applications + true + true + true + + true + + + + + + annotations + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileDirectory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileDirectory.cs new file mode 100644 index 000000000000..df39da79788a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileDirectory.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Edge.Mount.Archive +{ + internal class ZipFileDirectory : DirectoryBase + { + private readonly ZipFileMountPoint _mountPoint; + + internal ZipFileDirectory(IMountPoint mountPoint, string fullPath, string name) + : base(mountPoint, fullPath, name) + { + _mountPoint = (ZipFileMountPoint)mountPoint; + } + + public override bool Exists => _mountPoint.Universe.ContainsKey(FullPath); + + public override IEnumerable EnumerateFileSystemInfos(string pattern, SearchOption searchOption) + { + string rx = Regex.Escape(pattern); + rx = rx.Replace("\\*", ".*").Replace("\\?", ".?"); + Regex r = new Regex($"^{rx}$"); + return _mountPoint.Universe.Values.Where(x => x.FullPath.StartsWith(FullPath, StringComparison.Ordinal) && x.FullPath.Length != FullPath.Length && r.IsMatch(x.Name)).Where(x => searchOption == SearchOption.AllDirectories || x.FullPath.TrimEnd('/').Count(y => y == '/') == FullPath.Count(y => y == '/')); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileFile.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileFile.cs new file mode 100644 index 000000000000..556ef80b9160 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileFile.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. + +using System.IO.Compression; +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Edge.Mount.Archive +{ + internal class ZipFileFile : FileBase + { + private readonly ZipFileMountPoint _mountPoint; + private ZipArchiveEntry? _entry; + + internal ZipFileFile(IMountPoint mountPoint, string fullPath, string name, ZipArchiveEntry? entry) + : base(mountPoint, fullPath, name) + { + _entry = entry; + _mountPoint = (ZipFileMountPoint)mountPoint; + } + + public override bool Exists => _entry != null || (_mountPoint.Universe.TryGetValue(FullPath, out var info) && info.Kind == FileSystemInfoKind.File); + + public override Stream OpenRead() + { + if (_entry == null) + { + if (!_mountPoint.Universe.TryGetValue(FullPath, out var info) || info.Kind != FileSystemInfoKind.File || !info.Exists) + { + throw new FileNotFoundException("File not found", FullPath); + } + if (info is not ZipFileFile self) + { + throw new FileNotFoundException("File not found", FullPath); + } + + if (self._entry == null) + { + throw new FileNotFoundException("File not found", FullPath); + } + + _entry = self._entry; + } + + return _entry.Open(); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileMountPoint.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileMountPoint.cs new file mode 100644 index 000000000000..6b0207d47692 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileMountPoint.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Compression; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Edge.Mount.Archive +{ + /// + /// Mount point implementation for zip file. + /// NuGet packages are zip files, so they are handled by this mount point. + /// + internal class ZipFileMountPoint : IMountPoint + { + private IReadOnlyDictionary? _universe; + + internal ZipFileMountPoint(IEngineEnvironmentSettings environmentSettings, IMountPoint? parent, string mountPointUri, ZipArchive archive) + { + MountPointUri = mountPointUri; + Parent = parent; + EnvironmentSettings = environmentSettings; + Archive = archive; + Root = new ZipFileDirectory(this, "/", string.Empty); + } + + public IDirectory Root { get; } + + public IMountPoint? Parent { get; } + + public string MountPointUri { get; } + + public IEngineEnvironmentSettings EnvironmentSettings { get; } + + internal ZipArchive Archive { get; } + + internal IReadOnlyDictionary Universe + { + get + { + if (_universe == null) + { + Dictionary universe = new Dictionary + { + ["/"] = Root + }; + + foreach (ZipArchiveEntry entry in Archive.Entries) + { + string[] parts = entry.FullName.Split('/', '\\'); + string path = "/"; + IDirectory? parentDir = (IDirectory)universe["/"]; + + for (int i = 0; parentDir != null && i < parts.Length - 1; ++i) + { + parts[i] = Uri.UnescapeDataString(parts[i]); + path += parts[i] + "/"; + + if (!universe.TryGetValue(path, out IFileSystemInfo? parentDirEntry)) + { + universe[path] = parentDirEntry = new ZipFileDirectory(this, path, parts[i]); + } + + //If we mistakenly classified something with children as a file before, reclassify it as a directory + if (parentDirEntry is IFile file) + { + universe[path] = parentDirEntry = new ZipFileDirectory(this, file.FullPath, file.Name); + } + + parentDir = parentDirEntry as IDirectory; + } + + if (parentDir != null && !string.IsNullOrEmpty(entry.Name)) + { + string unescaped = Uri.UnescapeDataString(entry.Name); + path += unescaped; + universe[path] = new ZipFileFile(this, path, unescaped, entry); + } + } + + _universe = universe; + } + + return _universe; + } + } + + internal Guid MountPointFactoryId => ZipFileMountPointFactory.FactoryId; + + public IFile FileInfo(string path) + { + return new ZipFileFile(this, path, path.Substring(path.LastIndexOf('/') + 1), null); + } + + public IDirectory? DirectoryInfo(string path) + { + if (Universe.TryGetValue(path, out IFileSystemInfo info)) + { + return info as IDirectory; + } + else if (Universe.TryGetValue(path + "/", out info)) + { + return info as IDirectory; + } + + return new ZipFileDirectory(this, path, path.Substring(path.LastIndexOf('/') + 1)); + } + + public IFileSystemInfo? FileSystemInfo(string path) + { + IFile? file = FileInfo(path); + + if (file != null && file.Exists) + { + return file; + } + + return DirectoryInfo(path); + } + + public void Dispose() + { + Archive.Dispose(); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileMountPointFactory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileMountPointFactory.cs new file mode 100644 index 000000000000..3b26df760712 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/Archive/ZipFileMountPointFactory.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Compression; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Mount.Archive +{ + public sealed class ZipFileMountPointFactory : IMountPointFactory + { + internal static readonly Guid FactoryId = new("94E92610-CF4C-4F6D-AEB6-9E42DDE1899D"); + + Guid IIdentifiedComponent.Id => FactoryId; + + bool IMountPointFactory.TryMount(IEngineEnvironmentSettings environmentSettings, IMountPoint? parent, string mountPointUri, out IMountPoint? mountPoint) + { + if (!Uri.TryCreate(mountPointUri, UriKind.Absolute, out var uri)) + { + mountPoint = null; + return false; + } + + if (!uri.IsFile) + { + mountPoint = null; + return false; + } + + ZipArchive archive; + + if (parent == null) + { + if (!environmentSettings.Host.FileSystem.FileExists(uri.LocalPath)) + { + mountPoint = null; + return false; + } + + try + { + archive = new ZipArchive(environmentSettings.Host.FileSystem.OpenRead(uri.LocalPath), ZipArchiveMode.Read, false); + } + catch + { + mountPoint = null; + return false; + } + } + else + { + IFile? file = parent.Root.FileInfo(uri.LocalPath); + + if (file == null || !file.Exists) + { + mountPoint = null; + return false; + } + + try + { + archive = new ZipArchive(file.OpenRead(), ZipArchiveMode.Read, false); + } + catch + { + mountPoint = null; + return false; + } + } + + mountPoint = new ZipFileMountPoint(environmentSettings, parent, mountPointUri, archive); + return true; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/DirectoryBase.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/DirectoryBase.cs new file mode 100644 index 000000000000..e9c7b014009f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/DirectoryBase.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Edge.Mount +{ + internal abstract class DirectoryBase : FileSystemInfoBase, IDirectory + { + protected DirectoryBase(IMountPoint mountPoint, string fullPath, string name) + : base(mountPoint, fullPath, name, FileSystemInfoKind.Directory) + { + } + + public virtual IEnumerable EnumerateDirectories(string pattern, SearchOption searchOption) + { + return EnumerateFileSystemInfos(pattern, searchOption).OfType(); + } + + public virtual IEnumerable EnumerateFiles(string pattern, SearchOption searchOption) + { + return EnumerateFileSystemInfos(pattern, searchOption).OfType(); + } + + public abstract IEnumerable EnumerateFileSystemInfos(string pattern, SearchOption searchOption); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileBase.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileBase.cs new file mode 100644 index 000000000000..32600522ebb8 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileBase.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Edge.Mount +{ + internal abstract class FileBase : FileSystemInfoBase, IFile + { + protected FileBase(IMountPoint mountPoint, string fullPath, string name) + : base(mountPoint, fullPath, name, FileSystemInfoKind.File) + { + } + + public abstract Stream OpenRead(); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemDirectory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemDirectory.cs new file mode 100644 index 000000000000..75fbb6edddac --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemDirectory.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; +using Microsoft.TemplateEngine.Edge.Settings; + +namespace Microsoft.TemplateEngine.Edge.Mount.FileSystem +{ + internal class FileSystemDirectory : DirectoryBase + { + private readonly string _physicalPath; + private readonly SettingsFilePaths _paths; + private readonly IPhysicalFileSystem _fileSystem; + + internal FileSystemDirectory(IMountPoint mountPoint, string fullPath, string name, string physicalPath) + : base(mountPoint, EnsureTrailingSlash(fullPath), name) + { + _physicalPath = physicalPath; + _paths = new SettingsFilePaths(mountPoint.EnvironmentSettings); + _fileSystem = mountPoint.EnvironmentSettings.Host.FileSystem; + } + + public override bool Exists => _fileSystem.DirectoryExists(_physicalPath); + + public override IEnumerable EnumerateFileSystemInfos(string pattern, SearchOption searchOption) + { + return _paths.EnumerateFileSystemEntries(_physicalPath, pattern, searchOption).Select(x => + { + if (MountPoint is not FileSystemMountPoint fileSystemMountPoint) + { + throw new NotSupportedException($"{nameof(FileSystemDirectory)} may only exist in {nameof(FileSystemMountPoint)} mount point."); + } + + string baseName = x.Substring(fileSystemMountPoint.MountPointRootPath.Length).Replace(Path.DirectorySeparatorChar, '/'); + + if (baseName.Length == 0) + { + baseName = "/"; + } + + if (baseName[0] != '/') + { + baseName = "/" + baseName; + } + + if (_fileSystem.DirectoryExists(x) && baseName[baseName.Length - 1] != '/') + { + baseName += "/"; + } + + return fileSystemMountPoint.FileSystemInfo(baseName); + }); + } + + public override IEnumerable EnumerateDirectories(string pattern, SearchOption searchOption) + { + return _paths.EnumerateDirectories(_physicalPath, pattern, searchOption).Select(x => + { + string baseName = x.Substring(((FileSystemMountPoint)MountPoint).MountPointRootPath.Length).Replace(Path.DirectorySeparatorChar, '/'); + + if (baseName.Length == 0) + { + baseName = "/"; + } + + if (baseName[0] != '/') + { + baseName = "/" + baseName; + } + + if (baseName[baseName.Length - 1] != '/') + { + baseName += "/"; + } + + return new FileSystemDirectory(MountPoint, baseName, _paths.Name(x), x); + }); + } + + public override IEnumerable EnumerateFiles(string pattern, SearchOption searchOption) + { + return _paths.EnumerateFiles(_physicalPath, pattern, searchOption).Select(x => + { + string baseName = x.Substring(((FileSystemMountPoint)MountPoint).MountPointRootPath.Length).Replace(Path.DirectorySeparatorChar, '/'); + + if (baseName.Length == 0) + { + baseName = "/"; + } + + if (baseName[0] != '/') + { + baseName = "/" + baseName; + } + + return new FileSystemFile(MountPoint, baseName, _paths.Name(x), x); + }); + } + + private static string EnsureTrailingSlash(string path) + { + if (path.Last() == '/') + { + return path; + } + else + { + return path + "/"; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemFile.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemFile.cs new file mode 100644 index 000000000000..d89bf84c4d40 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemFile.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Edge.Mount.FileSystem +{ + internal class FileSystemFile : FileBase + { + private readonly string _physicalPath; + + internal FileSystemFile(IMountPoint mountPoint, string fullPath, string name, string physicalPath) + : base(mountPoint, fullPath, name) + { + _physicalPath = physicalPath; + } + + public override bool Exists => MountPoint.EnvironmentSettings.Host.FileSystem.FileExists(_physicalPath); + + public override Stream OpenRead() + { + return MountPoint.EnvironmentSettings.Host.FileSystem.OpenRead(_physicalPath); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemMountPoint.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemMountPoint.cs new file mode 100644 index 000000000000..65074964d094 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemMountPoint.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Edge.Settings; + +namespace Microsoft.TemplateEngine.Edge.Mount.FileSystem +{ + /// + /// Mount point implementation for file system directory. + /// + internal class FileSystemMountPoint : IMountPoint + { + private readonly SettingsFilePaths _paths; + + internal FileSystemMountPoint(IEngineEnvironmentSettings environmentSettings, string mountPointUri, string mountPointRootPath) + { + MountPointUri = mountPointUri; + MountPointRootPath = mountPointRootPath; + EnvironmentSettings = environmentSettings; + _paths = new SettingsFilePaths(environmentSettings); + Root = new FileSystemDirectory(this, "/", string.Empty, MountPointRootPath); + } + + public IDirectory Root { get; } + + public IEngineEnvironmentSettings EnvironmentSettings { get; } + + public IMountPoint? Parent { get; } + + public Guid MountPointFactoryId => FileSystemMountPointFactory.FactoryId; + + public string MountPointUri { get; } + + /// + /// Returns full path of the mounted directory. + /// + internal string MountPointRootPath { get; } + + public IFile FileInfo(string path) + { + string fullPath = Path.Combine(MountPointRootPath, path.TrimStart('/')); + + if (!path.StartsWith("/")) + { + path = "/" + path; + } + + return new FileSystemFile(this, path, _paths.Name(fullPath), fullPath); + } + + public IDirectory DirectoryInfo(string path) + { + string fullPath = Path.Combine(MountPointRootPath, path.TrimStart('/')); + return new FileSystemDirectory(this, path, _paths.Name(fullPath), fullPath); + } + + public IFileSystemInfo FileSystemInfo(string path) + { + string fullPath = Path.Combine(MountPointRootPath, path.TrimStart('/')); + + if (EnvironmentSettings.Host.FileSystem.DirectoryExists(fullPath)) + { + return new FileSystemDirectory(this, path, _paths.Name(fullPath), fullPath); + } + + return new FileSystemFile(this, path, _paths.Name(fullPath), fullPath); + } + + public void Dispose() + { + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemMountPointFactory.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemMountPointFactory.cs new file mode 100644 index 000000000000..2ecf2acca575 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystem/FileSystemMountPointFactory.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Edge.Mount.FileSystem +{ + public sealed class FileSystemMountPointFactory : IMountPointFactory + { + internal static readonly Guid FactoryId = new Guid("8C19221B-DEA3-4250-86FE-2D4E189A11D2"); + + Guid IIdentifiedComponent.Id => FactoryId; + + bool IMountPointFactory.TryMount(IEngineEnvironmentSettings environmentSettings, IMountPoint? parent, string mountPointUri, out IMountPoint? mountPoint) + { + if (!Uri.TryCreate(mountPointUri, UriKind.Absolute, out var uri)) + { + mountPoint = null; + return false; + } + + if (!uri.IsFile) + { + mountPoint = null; + return false; + } + + if (parent != null || !environmentSettings.Host.FileSystem.DirectoryExists(uri.LocalPath)) + { + mountPoint = null; + return false; + } + + mountPoint = new FileSystemMountPoint(environmentSettings, mountPointUri, uri.LocalPath); + return true; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystemInfoBase.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystemInfoBase.cs new file mode 100644 index 000000000000..b7132a16a94d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Mount/FileSystemInfoBase.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Edge.Mount +{ + internal abstract class FileSystemInfoBase : IFileSystemInfo + { + private IDirectory? _parent; + + protected FileSystemInfoBase(IMountPoint mountPoint, string fullPath, string name, FileSystemInfoKind kind) + { + FullPath = fullPath.Replace('\\', '/'); + Name = name; + Kind = kind; + MountPoint = mountPoint; + } + + public abstract bool Exists { get; } + + public string FullPath { get; } + + public FileSystemInfoKind Kind { get; } + + public virtual IDirectory? Parent + { + get + { + if (string.Equals(FullPath, "/", StringComparison.Ordinal)) + { + return null; + } + + if (string.Equals(FullPath, $"/{Name}", StringComparison.Ordinal)) + { + return MountPoint.Root; + } + + if (_parent == null) + { + if (FullPath == "/" || FullPath.Length < 2) + { + return null; + } + + int lastSlash = FullPath.LastIndexOf('/', FullPath.Length - 2); + + if (lastSlash < 0) + { + return null; + } + + string parentPath = FullPath.Substring(0, lastSlash + 1); + _parent = MountPoint.DirectoryInfo(parentPath); + } + + return (_parent?.Exists ?? false) ? _parent : null; + } + } + + public string Name { get; } + + public IMountPoint MountPoint { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/PublicAPI.Shipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..276f69f40bac --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/PublicAPI.Shipped.txt @@ -0,0 +1,270 @@ +#nullable enable +Microsoft.TemplateEngine.Edge.Components +Microsoft.TemplateEngine.Edge.DefaultEnvironment +Microsoft.TemplateEngine.Edge.DefaultEnvironment.ConsoleBufferWidth.get -> int +Microsoft.TemplateEngine.Edge.DefaultEnvironment.DefaultEnvironment() -> void +Microsoft.TemplateEngine.Edge.DefaultEnvironment.ExpandEnvironmentVariables(string! name) -> string! +Microsoft.TemplateEngine.Edge.DefaultEnvironment.GetEnvironmentVariable(string! name) -> string? +Microsoft.TemplateEngine.Edge.DefaultEnvironment.GetEnvironmentVariables() -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Edge.DefaultEnvironment.NewLine.get -> string! +Microsoft.TemplateEngine.Edge.DefaultPathInfo +Microsoft.TemplateEngine.Edge.DefaultPathInfo.DefaultPathInfo(Microsoft.TemplateEngine.Abstractions.IEnvironment! environment, Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost! host, string? globalSettingsDir = null, string? hostSettingsDir = null, string? hostVersionSettingsDir = null) -> void +Microsoft.TemplateEngine.Edge.DefaultPathInfo.GlobalSettingsDir.get -> string! +Microsoft.TemplateEngine.Edge.DefaultPathInfo.HostSettingsDir.get -> string! +Microsoft.TemplateEngine.Edge.DefaultPathInfo.HostVersionSettingsDir.get -> string! +Microsoft.TemplateEngine.Edge.DefaultPathInfo.UserProfileDir.get -> string! +Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost +Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.DefaultTemplateEngineHost(string! hostIdentifier, string! version, System.Collections.Generic.Dictionary? defaults = null, System.Collections.Generic.IReadOnlyList<(System.Type! InterfaceType, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! Instance)>? builtIns = null, System.Collections.Generic.IReadOnlyList? fallbackHostTemplateConfigNames = null, Microsoft.Extensions.Logging.ILoggerFactory? loggerFactory = null) -> void +Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.FallbackHostTemplateConfigNames.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.FileSystem.get -> Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem! +Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.HostIdentifier.get -> string! +Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.Logger.get -> Microsoft.Extensions.Logging.ILogger! +Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.LoggerFactory.get -> Microsoft.Extensions.Logging.ILoggerFactory! +Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.Version.get -> string! +Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.VirtualizeDirectory(string! path) -> void +Microsoft.TemplateEngine.Edge.EngineEnvironmentSettings +Microsoft.TemplateEngine.Edge.EngineEnvironmentSettings.Components.get -> Microsoft.TemplateEngine.Abstractions.IComponentManager! +Microsoft.TemplateEngine.Edge.EngineEnvironmentSettings.EngineEnvironmentSettings(Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost! host, bool virtualizeSettings = false, string? settingsLocation = null, Microsoft.TemplateEngine.Abstractions.IEnvironment? environment = null, Microsoft.TemplateEngine.Abstractions.IComponentManager? componentManager = null, Microsoft.TemplateEngine.Abstractions.IPathInfo? pathInfo = null) -> void +Microsoft.TemplateEngine.Edge.EngineEnvironmentSettings.Environment.get -> Microsoft.TemplateEngine.Abstractions.IEnvironment! +Microsoft.TemplateEngine.Edge.EngineEnvironmentSettings.Host.get -> Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost! +Microsoft.TemplateEngine.Edge.EngineEnvironmentSettings.Paths.get -> Microsoft.TemplateEngine.Abstractions.IPathInfo! +Microsoft.TemplateEngine.Edge.EngineEnvironmentSettings.SettingsLoader.get -> Microsoft.TemplateEngine.Abstractions.ISettingsLoader! +Microsoft.TemplateEngine.Edge.FilterableTemplateInfo +Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.ConfigTimestampUtc.get -> System.DateTime? +Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.ConfigTimestampUtc.set -> void +Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.FilterableTemplateInfo() -> void +Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.GeneratorId.get -> System.Guid +Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.HasScriptRunningPostActions.get -> bool +Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.HasScriptRunningPostActions.set -> void +Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.Precedence.get -> int +Microsoft.TemplateEngine.Edge.Settings.FilteredTemplateInfo +Microsoft.TemplateEngine.Edge.Settings.FilteredTemplateInfo.HasAmbiguousParameterMatch.get -> bool +Microsoft.TemplateEngine.Edge.Settings.FilteredTemplateInfo.HasInvalidParameterValue.get -> bool +Microsoft.TemplateEngine.Edge.Settings.FilteredTemplateInfo.HasParameterMismatch.get -> bool +Microsoft.TemplateEngine.Edge.Settings.FilteredTemplateInfo.IsMatch.get -> bool +Microsoft.TemplateEngine.Edge.Settings.FilteredTemplateInfo.IsParameterMatch.get -> bool +Microsoft.TemplateEngine.Edge.Settings.FilteredTemplateInfo.IsPartialMatch.get -> bool +Microsoft.TemplateEngine.Edge.Settings.InstallationScope +Microsoft.TemplateEngine.Edge.Settings.InstallationScope.Global = 0 -> Microsoft.TemplateEngine.Edge.Settings.InstallationScope +Microsoft.TemplateEngine.Edge.Settings.ITemplateInfoHostJsonCache +Microsoft.TemplateEngine.Edge.Settings.Scanner +Microsoft.TemplateEngine.Edge.Settings.Scanner.Scan(string! mountPointUri) -> Microsoft.TemplateEngine.Edge.Settings.ScanResult! +Microsoft.TemplateEngine.Edge.Settings.Scanner.Scan(string! mountPointUri, bool scanForComponents) -> Microsoft.TemplateEngine.Edge.Settings.ScanResult! +Microsoft.TemplateEngine.Edge.Settings.Scanner.Scanner(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings) -> void +Microsoft.TemplateEngine.Edge.Settings.ScanResult +Microsoft.TemplateEngine.Edge.Settings.ScanResult.Components.get -> System.Collections.Generic.IReadOnlyList<(string! AssemblyPath, System.Type! InterfaceType, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! Instance)>! +Microsoft.TemplateEngine.Edge.Settings.ScanResult.Dispose() -> void +Microsoft.TemplateEngine.Edge.Settings.ScanResult.Localizations.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Edge.Settings.ScanResult.MountPoint.get -> Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint! +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.Dispose() -> void +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.GetBuiltInManagedProvider(Microsoft.TemplateEngine.Edge.Settings.InstallationScope scope = Microsoft.TemplateEngine.Edge.Settings.InstallationScope.Global) -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider! +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.GetManagedProvider(string! name) -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider! +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.GetManagedProvider(System.Guid id) -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackageProvider! +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.GetManagedTemplatePackagesAsync(bool force, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.GetTemplatePackageAsync(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! template, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.GetTemplatePackagesAsync(bool force, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.GetTemplatesAsync(Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackage! templatePackage, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.GetTemplatesAsync(System.Func! matchFilter, System.Collections.Generic.IEnumerable!>! filters, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.GetTemplatesAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.RebuildTemplateCacheAsync(System.Threading.CancellationToken token) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.TemplatePackageManager(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings) -> void +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.TemplatePackagesChanged -> System.Action? +Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.CreationResultStatus.Cancelled = -2147467260 -> Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.CreationResultStatus.CreateFailed = -2147352567 -> Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.CreationResultStatus.DestructiveChangesDetected = -2147352563 -> Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.CreationResultStatus.InvalidParamValues = -2147352571 -> Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.CreationResultStatus.MissingMandatoryParam = -2147352561 -> Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.CreationResultStatus.NotFound = -2147352570 -> Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.CreationResultStatus.OperationNotSpecified = -2147352562 -> Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.CreationResultStatus.Success = 0 -> Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.FilteredTemplateEqualityComparer +Microsoft.TemplateEngine.Edge.Template.FilteredTemplateEqualityComparer.FilteredTemplateEqualityComparer() -> void +Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo +Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo.HasAmbiguousParameterMatch.get -> bool +Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo.HasInvalidParameterValue.get -> bool +Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo.HasParameterMismatch.get -> bool +Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo.IsMatch.get -> bool +Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo.IsParameterMatch.get -> bool +Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo.IsPartialMatch.get -> bool +Microsoft.TemplateEngine.Edge.Template.ITemplateCreationResult +Microsoft.TemplateEngine.Edge.Template.ITemplateCreationResult.CreationEffects.get -> Microsoft.TemplateEngine.Abstractions.ICreationEffects? +Microsoft.TemplateEngine.Edge.Template.ITemplateCreationResult.CreationResult.get -> Microsoft.TemplateEngine.Abstractions.ICreationResult? +Microsoft.TemplateEngine.Edge.Template.ITemplateCreationResult.ErrorMessage.get -> string? +Microsoft.TemplateEngine.Edge.Template.ITemplateCreationResult.OutputBaseDirectory.get -> string? +Microsoft.TemplateEngine.Edge.Template.ITemplateCreationResult.Status.get -> Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.ITemplateCreationResult.TemplateFullName.get -> string! +Microsoft.TemplateEngine.Edge.Template.ITemplateMatchInfo +Microsoft.TemplateEngine.Edge.Template.ITemplateMatchInfo.AddDisposition(Microsoft.TemplateEngine.Edge.Template.MatchInfo newDisposition) -> void +Microsoft.TemplateEngine.Edge.Template.ITemplateMatchInfo.IsMatch.get -> bool +Microsoft.TemplateEngine.Edge.Template.ITemplateMatchInfo.IsPartialMatch.get -> bool +Microsoft.TemplateEngine.Edge.Template.MatchInfo +Microsoft.TemplateEngine.Edge.Template.MatchInfo.Kind -> Microsoft.TemplateEngine.Edge.Template.MatchKind +Microsoft.TemplateEngine.Edge.Template.MatchInfo.Location -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchInfo.MatchInfo() -> void +Microsoft.TemplateEngine.Edge.Template.MatchKind +Microsoft.TemplateEngine.Edge.Template.MatchKind.AmbiguousParameterValue = 3 -> Microsoft.TemplateEngine.Edge.Template.MatchKind +Microsoft.TemplateEngine.Edge.Template.MatchKind.Exact = 1 -> Microsoft.TemplateEngine.Edge.Template.MatchKind +Microsoft.TemplateEngine.Edge.Template.MatchKind.InvalidParameterName = 4 -> Microsoft.TemplateEngine.Edge.Template.MatchKind +Microsoft.TemplateEngine.Edge.Template.MatchKind.InvalidParameterValue = 5 -> Microsoft.TemplateEngine.Edge.Template.MatchKind +Microsoft.TemplateEngine.Edge.Template.MatchKind.Mismatch = 6 -> Microsoft.TemplateEngine.Edge.Template.MatchKind +Microsoft.TemplateEngine.Edge.Template.MatchKind.Partial = 2 -> Microsoft.TemplateEngine.Edge.Template.MatchKind +Microsoft.TemplateEngine.Edge.Template.MatchKind.SingleStartsWith = 7 -> Microsoft.TemplateEngine.Edge.Template.MatchKind +Microsoft.TemplateEngine.Edge.Template.MatchKind.Unspecified = 0 -> Microsoft.TemplateEngine.Edge.Template.MatchKind +Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchLocation.Alias = 3 -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchLocation.Author = 10 -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchLocation.Baseline = 8 -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchLocation.Classification = 4 -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchLocation.Context = 6 -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchLocation.DefaultLanguage = 9 -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchLocation.Language = 5 -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchLocation.Name = 1 -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchLocation.OtherParameter = 7 -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchLocation.ShortName = 2 -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.MatchLocation.Unspecified = 0 -> Microsoft.TemplateEngine.Edge.Template.MatchLocation +Microsoft.TemplateEngine.Edge.Template.OrdinalIgnoreCaseMatchInfoComparer +Microsoft.TemplateEngine.Edge.Template.OrdinalIgnoreCaseMatchInfoComparer.Equals(Microsoft.TemplateEngine.Edge.Template.MatchInfo x, Microsoft.TemplateEngine.Edge.Template.MatchInfo y) -> bool +Microsoft.TemplateEngine.Edge.Template.OrdinalIgnoreCaseMatchInfoComparer.GetHashCode(Microsoft.TemplateEngine.Edge.Template.MatchInfo obj) -> int +Microsoft.TemplateEngine.Edge.Template.OrdinalIgnoreCaseMatchInfoComparer.OrdinalIgnoreCaseMatchInfoComparer() -> void +Microsoft.TemplateEngine.Edge.Template.TemplateCreator +Microsoft.TemplateEngine.Edge.Template.TemplateCreator.InstantiateAsync(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! templateInfo, string? name, string? fallbackName, string? outputPath, System.Collections.Generic.IReadOnlyDictionary! inputParameters, bool forceCreation = false, string? baselineName = null, bool dryRun = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Edge.Template.TemplateCreator.LoadTemplate(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! info, string? baselineName) -> Microsoft.TemplateEngine.Abstractions.ITemplate? +Microsoft.TemplateEngine.Edge.Template.TemplateCreator.ReleaseMountPoints(Microsoft.TemplateEngine.Abstractions.ITemplate! template) -> void +Microsoft.TemplateEngine.Edge.Template.TemplateCreator.ResolveUserParameters(Microsoft.TemplateEngine.Abstractions.ITemplate! template, Microsoft.TemplateEngine.Abstractions.IParameterSet! templateParams, System.Collections.Generic.IReadOnlyDictionary! inputParameters, out System.Collections.Generic.IReadOnlyList! paramsWithInvalidValues) -> void +Microsoft.TemplateEngine.Edge.Template.TemplateCreator.SetupDefaultParamValuesFromTemplateAndHost(Microsoft.TemplateEngine.Abstractions.ITemplate! templateInfo, string! realName, out System.Collections.Generic.IReadOnlyList! paramsWithInvalidValues) -> Microsoft.TemplateEngine.Abstractions.IParameterSet! +Microsoft.TemplateEngine.Edge.Template.TemplateCreator.TemplateCreator(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings) -> void +Microsoft.TemplateEngine.Edge.Template.TemplateEqualityComparer +Microsoft.TemplateEngine.Edge.Template.TemplateEqualityComparer.TemplateEqualityComparer() -> void +Microsoft.TemplateEngine.Edge.Template.TemplateMatchInfoEqualityComparer +Microsoft.TemplateEngine.Edge.Template.TemplateMatchInfoEqualityComparer.TemplateMatchInfoEqualityComparer() -> void +Microsoft.TemplateEngine.Edge.Template.WellKnownSearchFilters +Microsoft.TemplateEngine.Edge.TemplateListFilter +static Microsoft.TemplateEngine.Edge.Components.AllComponents.get -> System.Collections.Generic.IReadOnlyList<(System.Type! Type, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! Instance)>! +static Microsoft.TemplateEngine.Edge.Template.WellKnownSearchFilters.AuthorFilter(string! author) -> System.Func! +static Microsoft.TemplateEngine.Edge.Template.WellKnownSearchFilters.BaselineFilter(string! baselineName) -> System.Func! +static Microsoft.TemplateEngine.Edge.Template.WellKnownSearchFilters.ClassificationsFilter(string! name) -> System.Func! +static Microsoft.TemplateEngine.Edge.Template.WellKnownSearchFilters.ContextFilter(string! inputContext) -> System.Func! +static Microsoft.TemplateEngine.Edge.Template.WellKnownSearchFilters.LanguageFilter(string! language) -> System.Func! +static Microsoft.TemplateEngine.Edge.Template.WellKnownSearchFilters.NameFilter(string! name) -> System.Func! +static Microsoft.TemplateEngine.Edge.Template.WellKnownSearchFilters.TagFilter(string! tagFilter) -> System.Func! +static Microsoft.TemplateEngine.Edge.TemplateListFilter.ExactMatchFilter.get -> System.Func! +static Microsoft.TemplateEngine.Edge.TemplateListFilter.FilterTemplates(System.Collections.Generic.IReadOnlyList! templateList, bool exactMatchesOnly, params System.Func![]! filters) -> System.Collections.Generic.IReadOnlyCollection! +static Microsoft.TemplateEngine.Edge.TemplateListFilter.GetTemplateMatchInfo(System.Collections.Generic.IReadOnlyList! templateList, System.Func! matchFilter, params System.Func![]! filters) -> System.Collections.Generic.IReadOnlyCollection! +static Microsoft.TemplateEngine.Edge.TemplateListFilter.GetTemplateMatchInfo(System.Collections.Generic.IReadOnlyList! templateList, System.Func! matchFilter, params System.Func![]! filters) -> System.Collections.Generic.IReadOnlyCollection! +static Microsoft.TemplateEngine.Edge.TemplateListFilter.PartialMatchFilter.get -> System.Func! +virtual Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.BuiltInComponents.get -> System.Collections.Generic.IReadOnlyList<(System.Type! InterfaceType, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! Instance)>! +virtual Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.TryGetHostParamDefault(string! paramName, out string? value) -> bool +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.Author.get -> string +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.BaselineInfo.get -> System.Collections.Generic.IReadOnlyDictionary +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.CacheParameters.get -> System.Collections.Generic.IReadOnlyDictionary +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.Classifications.get -> System.Collections.Generic.IReadOnlyList +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.ConfigPlace.get -> string +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.DefaultName.get -> string +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.Description.get -> string +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.GroupIdentity.get -> string +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.GroupShortNameList.get -> System.Collections.Generic.IReadOnlyList +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.GroupShortNameList.set -> void +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.HostConfigPlace.get -> string +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.Identity.get -> string +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.LocaleConfigPlace.get -> string +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.MountPointUri.get -> string +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.Name.get -> string +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.Parameters.get -> System.Collections.Generic.IReadOnlyList +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.ShortName.get -> string +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.ShortNameList.get -> System.Collections.Generic.IReadOnlyList +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.ShortNameList.set -> void +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.Tags.get -> System.Collections.Generic.IReadOnlyDictionary +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.TagsCollection.get -> System.Collections.Generic.IReadOnlyDictionary +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.ThirdPartyNotices.get -> string +~static Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.FromITemplateInfo(Microsoft.TemplateEngine.Abstractions.ITemplateInfo source) -> Microsoft.TemplateEngine.Edge.FilterableTemplateInfo +Microsoft.TemplateEngine.Edge.BuiltInManagedProvider.GlobalSettingsTemplatePackageProviderFactory +Microsoft.TemplateEngine.Edge.BuiltInManagedProvider.GlobalSettingsTemplatePackageProviderFactory.GlobalSettingsTemplatePackageProviderFactory() -> void +Microsoft.TemplateEngine.Edge.Constraints.HostConstraintFactory +Microsoft.TemplateEngine.Edge.Constraints.HostConstraintFactory.HostConstraintFactory() -> void +Microsoft.TemplateEngine.Edge.Constraints.OSConstraintFactory +Microsoft.TemplateEngine.Edge.Constraints.OSConstraintFactory.OSConstraintFactory() -> void +Microsoft.TemplateEngine.Edge.Constraints.SdkVersionConstraintFactory +Microsoft.TemplateEngine.Edge.Constraints.SdkVersionConstraintFactory.SdkVersionConstraintFactory() -> void +Microsoft.TemplateEngine.Edge.Constraints.WorkloadConstraintFactory +Microsoft.TemplateEngine.Edge.Constraints.WorkloadConstraintFactory.WorkloadConstraintFactory() -> void +Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.Dispose() -> void +Microsoft.TemplateEngine.Edge.EnvironmentVariablesBindSource +Microsoft.TemplateEngine.Edge.EnvironmentVariablesBindSource.EnvironmentVariablesBindSource() -> void +Microsoft.TemplateEngine.Edge.HostParametersBindSource +Microsoft.TemplateEngine.Edge.HostParametersBindSource.HostParametersBindSource() -> void +Microsoft.TemplateEngine.Edge.Installers.Folder.FolderInstallerFactory +Microsoft.TemplateEngine.Edge.Installers.Folder.FolderInstallerFactory.FolderInstallerFactory() -> void +Microsoft.TemplateEngine.Edge.Installers.NuGet.NuGetInstallerFactory +Microsoft.TemplateEngine.Edge.Installers.NuGet.NuGetInstallerFactory.NuGetInstallerFactory() -> void +Microsoft.TemplateEngine.Edge.Mount.Archive.ZipFileMountPointFactory +Microsoft.TemplateEngine.Edge.Mount.Archive.ZipFileMountPointFactory.ZipFileMountPointFactory() -> void +Microsoft.TemplateEngine.Edge.Mount.FileSystem.FileSystemMountPointFactory +Microsoft.TemplateEngine.Edge.Mount.FileSystem.FileSystemMountPointFactory.FileSystemMountPointFactory() -> void +Microsoft.TemplateEngine.Edge.EngineEnvironmentSettings.Dispose() -> void +Microsoft.TemplateEngine.Edge.Settings.FilteredTemplateInfo.FilteredTemplateInfo(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! info, System.Collections.Generic.IReadOnlyList! matchDisposition) -> void +Microsoft.TemplateEngine.Edge.Settings.FilteredTemplateInfo.Info.get -> Microsoft.TemplateEngine.Abstractions.ITemplateInfo! +Microsoft.TemplateEngine.Edge.Settings.FilteredTemplateInfo.MatchDisposition.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Edge.Settings.FilteredTemplateInfo.MatchDisposition.set -> void +Microsoft.TemplateEngine.Edge.Template.CreationResultStatus.CondtionsEvaluationMismatch = -2147312509 -> Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.CreationResultStatus.TemplateIssueDetected = -2147352566 -> Microsoft.TemplateEngine.Edge.Template.CreationResultStatus +Microsoft.TemplateEngine.Edge.Template.EvaluatedInputParameterData +Microsoft.TemplateEngine.Edge.Template.EvaluatedInputParameterData.EvaluatedInputParameterData(Microsoft.TemplateEngine.Abstractions.ITemplateParameter! parameterDefinition, object? value, Microsoft.TemplateEngine.Abstractions.Parameters.DataSource dataSource, bool? isEnabledConditionResult, bool? isRequiredConditionResult, Microsoft.TemplateEngine.Edge.Template.InputDataState inputDataState = Microsoft.TemplateEngine.Edge.Template.InputDataState.Set) -> void +Microsoft.TemplateEngine.Edge.Template.EvaluatedInputParameterData.IsEnabledConditionResult.get -> bool? +Microsoft.TemplateEngine.Edge.Template.EvaluatedInputParameterData.IsRequiredConditionResult.get -> bool? +Microsoft.TemplateEngine.Edge.Template.FilteredTemplateEqualityComparer.Equals(Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo! x, Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo! y) -> bool +Microsoft.TemplateEngine.Edge.Template.FilteredTemplateEqualityComparer.GetHashCode(Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo! obj) -> int +Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo.Info.get -> Microsoft.TemplateEngine.Abstractions.ITemplateInfo! +Microsoft.TemplateEngine.Edge.Template.IFilteredTemplateInfo.MatchDisposition.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Edge.Template.InputDataSet +Microsoft.TemplateEngine.Edge.Template.InputDataSet.ContainsKey(Microsoft.TemplateEngine.Abstractions.ITemplateParameter! key) -> bool +Microsoft.TemplateEngine.Edge.Template.InputDataSet.ContinueOnMismatchedConditionsEvaluation.get -> bool +Microsoft.TemplateEngine.Edge.Template.InputDataSet.ContinueOnMismatchedConditionsEvaluation.init -> void +Microsoft.TemplateEngine.Edge.Template.InputDataSet.Count.get -> int +Microsoft.TemplateEngine.Edge.Template.InputDataSet.GetEnumerator() -> System.Collections.Generic.IEnumerator>! +Microsoft.TemplateEngine.Edge.Template.InputDataSet.InputDataSet(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! templateInfo) -> void +Microsoft.TemplateEngine.Edge.Template.InputDataSet.InputDataSet(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! templateInfo, System.Collections.Generic.IReadOnlyDictionary! inputParameters) -> void +Microsoft.TemplateEngine.Edge.Template.InputDataSet.InputDataSet(Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet! parameters, System.Collections.Generic.IReadOnlyList! parameterData) -> void +Microsoft.TemplateEngine.Edge.Template.InputDataSet.Keys.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Edge.Template.InputDataSet.ParameterDefinitionSet.get -> Microsoft.TemplateEngine.Abstractions.Parameters.ParameterDefinitionSet! +Microsoft.TemplateEngine.Edge.Template.InputDataSet.this[Microsoft.TemplateEngine.Abstractions.ITemplateParameter! key].get -> Microsoft.TemplateEngine.Edge.Template.InputParameterData! +Microsoft.TemplateEngine.Edge.Template.InputDataSet.TryGetValue(Microsoft.TemplateEngine.Abstractions.ITemplateParameter! key, out Microsoft.TemplateEngine.Edge.Template.InputParameterData! value) -> bool +Microsoft.TemplateEngine.Edge.Template.InputDataSet.Values.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Edge.Template.InputDataState +Microsoft.TemplateEngine.Edge.Template.InputDataState.ExplicitEmpty = 2 -> Microsoft.TemplateEngine.Edge.Template.InputDataState +Microsoft.TemplateEngine.Edge.Template.InputDataState.Set = 0 -> Microsoft.TemplateEngine.Edge.Template.InputDataState +Microsoft.TemplateEngine.Edge.Template.InputDataState.Unset = 1 -> Microsoft.TemplateEngine.Edge.Template.InputDataState +Microsoft.TemplateEngine.Edge.Template.InputDataStateUtil +Microsoft.TemplateEngine.Edge.Template.InputParameterData +Microsoft.TemplateEngine.Edge.Template.InputParameterData.DataSource.get -> Microsoft.TemplateEngine.Abstractions.Parameters.DataSource +Microsoft.TemplateEngine.Edge.Template.InputParameterData.InputDataState.get -> Microsoft.TemplateEngine.Edge.Template.InputDataState +Microsoft.TemplateEngine.Edge.Template.InputParameterData.InputParameterData(Microsoft.TemplateEngine.Abstractions.ITemplateParameter! parameterDefinition, object? value, Microsoft.TemplateEngine.Abstractions.Parameters.DataSource dataSource = Microsoft.TemplateEngine.Abstractions.Parameters.DataSource.User, Microsoft.TemplateEngine.Edge.Template.InputDataState inputDataState = Microsoft.TemplateEngine.Edge.Template.InputDataState.Set) -> void +Microsoft.TemplateEngine.Edge.Template.InputParameterData.ParameterDefinition.get -> Microsoft.TemplateEngine.Abstractions.ITemplateParameter! +Microsoft.TemplateEngine.Edge.Template.InputParameterData.Value.get -> object? +Microsoft.TemplateEngine.Edge.Template.ITemplateMatchInfo.DispositionOfDefaults.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Edge.Template.ITemplateMatchInfo.Info.get -> Microsoft.TemplateEngine.Abstractions.ITemplateInfo! +Microsoft.TemplateEngine.Edge.Template.ITemplateMatchInfo.MatchDisposition.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Edge.Template.MatchInfo.AdditionalInformation -> string! +Microsoft.TemplateEngine.Edge.Template.MatchInfo.InputParameterFormat -> string! +Microsoft.TemplateEngine.Edge.Template.MatchInfo.InputParameterName -> string! +Microsoft.TemplateEngine.Edge.Template.MatchInfo.ParameterValue -> string! +Microsoft.TemplateEngine.Edge.Template.TemplateCreator.InstantiateAsync(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! templateInfo, string? name, string? fallbackName, string? outputPath, Microsoft.TemplateEngine.Edge.Template.InputDataSet? inputParameters, bool forceCreation = false, string? baselineName = null, bool dryRun = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Edge.Template.TemplateEqualityComparer.Equals(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! x, Microsoft.TemplateEngine.Abstractions.ITemplateInfo! y) -> bool +Microsoft.TemplateEngine.Edge.Template.TemplateEqualityComparer.GetHashCode(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! obj) -> int +Microsoft.TemplateEngine.Edge.Template.TemplateMatchInfoEqualityComparer.Equals(Microsoft.TemplateEngine.Edge.Template.ITemplateMatchInfo! x, Microsoft.TemplateEngine.Edge.Template.ITemplateMatchInfo! y) -> bool +Microsoft.TemplateEngine.Edge.Template.TemplateMatchInfoEqualityComparer.GetHashCode(Microsoft.TemplateEngine.Edge.Template.ITemplateMatchInfo! obj) -> int +Microsoft.TemplateEngine.Edge.TemplateConstraintManager +Microsoft.TemplateEngine.Edge.TemplateConstraintManager.Dispose() -> void +Microsoft.TemplateEngine.Edge.TemplateConstraintManager.EvaluateConstraintAsync(string! type, string? args, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Edge.TemplateConstraintManager.EvaluateConstraintsAsync(System.Collections.Generic.IEnumerable! templates, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Result)>!>! +Microsoft.TemplateEngine.Edge.TemplateConstraintManager.GetConstraintsAsync(System.Collections.Generic.IEnumerable? templates = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Edge.TemplateConstraintManager.TemplateConstraintManager(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! engineEnvironmentSettings) -> void +override Microsoft.TemplateEngine.Edge.Template.InputParameterData.ToString() -> string! +static Microsoft.TemplateEngine.Edge.Components.MandatoryComponents.get -> System.Collections.Generic.IReadOnlyList<(System.Type! Type, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! Instance)>! +static Microsoft.TemplateEngine.Edge.Template.FilteredTemplateEqualityComparer.Default.get -> System.Collections.Generic.IEqualityComparer! +static Microsoft.TemplateEngine.Edge.Template.InputDataStateUtil.GetInputDataState(object? value) -> Microsoft.TemplateEngine.Edge.Template.InputDataState +static Microsoft.TemplateEngine.Edge.Template.TemplateEqualityComparer.Default.get -> System.Collections.Generic.IEqualityComparer! +static Microsoft.TemplateEngine.Edge.Template.TemplateMatchInfoEqualityComparer.Default.get -> System.Collections.Generic.IEqualityComparer! +~Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.ParameterDefinitions.get -> Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet +Microsoft.TemplateEngine.Edge.DefaultTemplateEngineHost.WorkingDirectory.get -> string! \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/PublicAPI.Unshipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..6d200233a555 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/PublicAPI.Unshipped.txt @@ -0,0 +1,10 @@ +Microsoft.TemplateEngine.Edge.DefaultEnvironment.DefaultEnvironment(System.Collections.Generic.IReadOnlyDictionary! environmentVariables) -> void +Microsoft.TemplateEngine.Edge.Settings.Scanner.ScanAsync(string! mountPointUri, bool logValidationResults = true, bool returnInvalidTemplates = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Edge.Settings.Scanner.ScanAsync(string! mountPointUri, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Edge.Settings.ScanResult.Templates.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Edge.Settings.ITemplateInfoHostJsonCache.HostData.get -> string? +Microsoft.TemplateEngine.Edge.Settings.TemplatePackageManager.GetManagedTemplatePackageAsync(string! packageIdentifier, string? packageVersion = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<(Microsoft.TemplateEngine.Abstractions.TemplatePackage.IManagedTemplatePackage? Package, System.Collections.Generic.IEnumerable? Templates)>! +Microsoft.TemplateEngine.Edge.VirtualEnvironment +Microsoft.TemplateEngine.Edge.VirtualEnvironment.VirtualEnvironment(System.Collections.Generic.IReadOnlyDictionary? virtualEnvironment, bool includeRealEnvironment) -> void +static Microsoft.TemplateEngine.Edge.DefaultEnvironment.FetchEnvironmentVariables() -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Edge.FilterableTemplateInfo.PreferDefaultName.get -> bool \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/ReflectionLoadProbingPath.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/ReflectionLoadProbingPath.cs new file mode 100644 index 000000000000..82c7a71da38c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/ReflectionLoadProbingPath.cs @@ -0,0 +1,261 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Reflection; +#if NET +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Loader; +#endif + +namespace Microsoft.TemplateEngine.Edge +{ + internal class ReflectionLoadProbingPath + { + private static readonly ConcurrentDictionary LoadedAssemblies = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private static readonly List Instance = new List(); + + private readonly string _path; + + private ReflectionLoadProbingPath(string path) + { + _path = path; + } + + internal static void Add(string basePath) + { + Instance.Add(new ReflectionLoadProbingPath(basePath)); +#if NET + AssemblyLoadContext.Default.Resolving += Resolving; +#else + AppDomain.CurrentDomain.AssemblyResolve += Resolving; +#endif + } + + internal static bool HasLoaded(string assemblyName) + { + return LoadedAssemblies.ContainsKey(assemblyName); + } + + internal static void Reset() + { + Instance.Clear(); + } + +#if NET + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "Assembly probing loads assemblies by path at runtime.")] + private static Assembly? SelectBestMatch(AssemblyLoadContext loadContext, AssemblyName match, IEnumerable candidates) +#else + private static Assembly? SelectBestMatch(object sender, AssemblyName match, IEnumerable candidates) +#endif + { + return LoadedAssemblies.GetOrAdd(match.ToString(), n => + { + Stack bestMatch = new Stack(); + byte[] pk = match.GetPublicKey(); + bool cultureMatch = false; + bool majorVersionMatch = false; + bool minorVersionMatch = false; + bool buildMatch = false; + bool revisionMatch = false; + + foreach (FileInfo file in candidates) + { + if (!file.Exists) + { + continue; + } + +#if NET + AssemblyName candidateName = AssemblyLoadContext.GetAssemblyName(file.FullName); +#else + AssemblyName candidateName = AssemblyName.GetAssemblyName(file.FullName); +#endif + + //Only pursue things that may have the same identity + if (!string.Equals(candidateName.Name, match.Name, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + //If the required match has a strong name, the public key token must match + if (pk != null && !pk.SequenceEqual(candidateName.GetPublicKey() ?? Enumerable.Empty())) + { + continue; + } + + if (match.Version != null) + { + //Don't go backwards + if (candidateName.Version.Major < match.Version.Major) + { + continue; + } + + if (candidateName.Version.Major == match.Version.Major) + { + //Don't go backwards + if (candidateName.Version.Minor < match.Version.Minor) + { + continue; + } + + if (candidateName.Version.Minor == match.Version.Minor) + { + //Don't go backwards + if (candidateName.Version.Build < match.Version.Build) + { + continue; + } + + if (candidateName.Version.Build == match.Version.Build) + { + //Don't go backwards + if (candidateName.Version.Revision < match.Version.Revision) + { + continue; + } + + if (candidateName.Version.Revision != match.Version.Revision) + { + if (revisionMatch) + { + continue; + } + } + else + { + revisionMatch = true; + } + + majorVersionMatch = true; + minorVersionMatch = true; + buildMatch = true; + } + else + { + if (buildMatch) + { + continue; + } + + majorVersionMatch = true; + minorVersionMatch = true; + } + } + else + { + if (minorVersionMatch) + { + continue; + } + + majorVersionMatch = true; + } + } + else + { + if (majorVersionMatch) + { + continue; + } + } + } + + if (string.Equals(candidateName.CultureName, match.CultureName, StringComparison.OrdinalIgnoreCase)) + { + cultureMatch = true; + } + else if (cultureMatch) + { + continue; + } + + bestMatch.Push(file.FullName); + } + + while (bestMatch.Count > 0) + { + try + { + string attempt = bestMatch.Pop(); +#if NET + Assembly result = loadContext.LoadFromAssemblyPath(attempt); +#else + Assembly result = Assembly.LoadFile(attempt); +#endif + return result; + } + catch + { + } + } + + return null; + }); + } + +#if NET + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "Assembly resolution probes and loads assemblies at runtime.")] + private static Assembly? Resolving(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName) +#else + private static Assembly? Resolving(object sender, ResolveEventArgs resolveEventArgs) +#endif + { +#if NET + string stringName = assemblyName.Name; +#else + string stringName = resolveEventArgs.Name; + AssemblyName assemblyName = new AssemblyName(stringName); +#endif + + foreach (ReflectionLoadProbingPath selector in Instance) + { + DirectoryInfo info = new DirectoryInfo(Path.Combine(selector._path, stringName)); + Assembly? found = null; + + if (info.Exists) + { + IEnumerable files = info.EnumerateFiles($"{stringName}.dll", SearchOption.AllDirectories) + .Where(x => x.FullName.IndexOf($"{Path.DirectorySeparatorChar}lib{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase) > -1 + && (x.FullName.IndexOf($"{Path.DirectorySeparatorChar}netstandard", StringComparison.OrdinalIgnoreCase) > -1 + || x.FullName.IndexOf($"{Path.DirectorySeparatorChar}netcoreapp", StringComparison.OrdinalIgnoreCase) > -1)) + .OrderByDescending(x => x.FullName); +#if NET + found = SelectBestMatch(assemblyLoadContext, assemblyName, files); +#else + found = SelectBestMatch(sender, assemblyName, files); +#endif + } + else if (File.Exists(Path.Combine(selector._path, stringName + ".dll"))) + { + FileInfo f = new FileInfo(Path.Combine(selector._path, stringName + ".dll")); + FileInfo[] files = { f }; +#if NET + found = SelectBestMatch(assemblyLoadContext, assemblyName, files); +#else + found = SelectBestMatch(sender, assemblyName, files); +#endif + } + + if (found != null) + { + foreach (AssemblyName reference in found.GetReferencedAssemblies()) + { +#if NET + Resolving(assemblyLoadContext, reference); +#else + ResolveEventArgs referenceArgs = new ResolveEventArgs(reference.FullName, found); + Resolving(sender, referenceArgs); +#endif + } + + return found; + } + } + + return null; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/AsyncMutex.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/AsyncMutex.cs new file mode 100644 index 000000000000..8ddd7946d61a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/AsyncMutex.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + /// + /// Helper class to work with in async method, since await + /// can switch to different thread and must be called from same thread. + /// Hence this helper class. + /// + internal sealed class AsyncMutex : IDisposable + { + private readonly TaskCompletionSource _taskCompletionSource; + private readonly ManualResetEvent _blockReleasingMutex = new ManualResetEvent(false); + private readonly string _mutexName; + private readonly CancellationToken _token; + private volatile bool _disposed; + private volatile bool _isLocked = true; + + private AsyncMutex(string mutexName, CancellationToken token) + { + _mutexName = mutexName; + _token = token; + _taskCompletionSource = new TaskCompletionSource(); + new Thread(WaitLoop).Start(); + } + + /// + /// Returns true if the mutex is acquired. + /// + public bool IsLocked => _isLocked; + + /// + /// Creates the and task for waiting until underlying is acquired. + /// + /// The mutex name. The name is case-sensitive. + /// The to observe. + /// created . + public static Task WaitAsync(string mutexName, CancellationToken token) + { + var mutex = new AsyncMutex(mutexName, token); + return mutex._taskCompletionSource.Task; + } + + /// + /// Disposes the . If disposed, the underlying is released. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + _disposed = true; + _isLocked = false; + + _blockReleasingMutex.Set(); + } + + private void WaitLoop(object state) + { + var mutex = new Mutex(false, _mutexName); + var mutexAcquired = false; + try + { + while (true) + { + if (_token.IsCancellationRequested) + { + _taskCompletionSource.SetCanceled(); + return; + } + if (mutex.WaitOne(100)) + { + mutexAcquired = true; + break; + } + } + _taskCompletionSource.SetResult(this); + _blockReleasingMutex.WaitOne(); + } + finally + { + if (mutexAcquired) + { + mutex.ReleaseMutex(); + } + mutex.Dispose(); + _blockReleasingMutex.Dispose(); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/ComponentManager.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/ComponentManager.cs new file mode 100644 index 000000000000..42d3712b0507 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/ComponentManager.cs @@ -0,0 +1,322 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET7_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Reflection; +using Microsoft.TemplateEngine.Abstractions; +#if NET +using System.Runtime.Loader; +#endif + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + internal class ComponentManager : IComponentManager + { + private readonly List _loadLocations = new List(); + private readonly Dictionary _componentIdToAssemblyQualifiedTypeName = new Dictionary(); + private readonly Dictionary> _componentIdsByType; + private readonly SettingsStore _settings; + private readonly SettingsFilePaths _paths; + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + +#if NET7_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL2057", Justification = "Component types are resolved by assembly-qualified name from the settings store.")] +#endif + public ComponentManager(IEngineEnvironmentSettings engineEnvironmentSettings) + { + _engineEnvironmentSettings = engineEnvironmentSettings; + _paths = new SettingsFilePaths(engineEnvironmentSettings); + _settings = SettingsStore.Load(engineEnvironmentSettings, _paths); + _loadLocations.AddRange(_settings.ProbingPaths); + + ReflectionLoadProbingPath.Reset(); + foreach (string loadLocation in _loadLocations) + { + ReflectionLoadProbingPath.Add(loadLocation); + } + + _componentIdsByType = new Dictionary>(); + + foreach (KeyValuePair> bucket in _settings.ComponentTypeToGuidList) + { + Type interfaceType = Type.GetType(bucket.Key); + if (interfaceType != null) + { + _componentIdsByType[interfaceType] = bucket.Value; + } + } + + foreach (KeyValuePair entry in _settings.ComponentGuidToAssemblyQualifiedName) + { + if (Guid.TryParse(entry.Key, out Guid componentId)) + { + _componentIdToAssemblyQualifiedTypeName[componentId] = entry.Value; + } + } + + foreach (var (interfaceType, instance) in engineEnvironmentSettings.Host.BuiltInComponents) + { + AddComponent(interfaceType, instance); + } + } + + internal Dictionary> ComponentCache { get; } = new Dictionary>(); + + public IEnumerable OfType() + where T : class, IIdentifiedComponent + { + lock (_componentIdToAssemblyQualifiedTypeName) + { + if (!_componentIdsByType.TryGetValue(typeof(T), out HashSet ids)) + { + if (_settings.ComponentTypeToGuidList.TryGetValue(typeof(T).AssemblyQualifiedName, out ids)) + { + _componentIdsByType[typeof(T)] = ids; + } + else + { + return []; + } + } + + var components = new List(); + foreach (Guid id in ids) + { + if (TryGetComponent(id, out T? component)) + { + if (component is null) + { + throw new InvalidOperationException($"{nameof(component)} cannot be null when {nameof(TryGetComponent)} is 'true'"); + } + + components.Add(component); + } + } + + return components; + } + } + + // Attempt to register the type, and then save the settings. + public void Register(Type type) + { + if (RegisterType(type)) + { + Save(); + } + } + + // Attempt to register every type in the typeList + // Save once at the end if anything was registered. + public void RegisterMany(IEnumerable typeList) + { + bool anyRegistered = false; + + foreach (Type type in typeList) + { + anyRegistered |= RegisterType(type); + } + + if (anyRegistered) + { + Save(); + } + } + +#if NET7_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "Components are resolved by name and instantiated via reflection.")] + [UnconditionalSuppressMessage("AOT", "IL2067", Justification = "Component type is resolved at runtime from stored assembly-qualified name.")] + [UnconditionalSuppressMessage("AOT", "IL2072", Justification = "Component type is resolved at runtime from stored assembly-qualified name.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "Component instantiation uses Activator.CreateInstance with runtime-resolved types.")] +#endif + public bool TryGetComponent(Guid id, out T? component) + where T : class, IIdentifiedComponent + { + lock (_componentIdToAssemblyQualifiedTypeName) + { + component = default; + if (ComponentCache.TryGetValue(typeof(T), out Dictionary typeCache) && typeCache != null + && typeCache.TryGetValue(id, out object resolvedComponent) && resolvedComponent != null && resolvedComponent is T t) + { + component = t; + return true; + } + + if (_componentIdToAssemblyQualifiedTypeName.TryGetValue(id, out string assemblyQualifiedName)) + { + Type type = GetType(assemblyQualifiedName); + component = Activator.CreateInstance(type) as T; + + if (component != null) + { + AddComponent(typeof(T), component); + return true; + } + } + return false; + } + } + + internal void AddProbingPath(string probeIn) + { + const int maxAttempts = 10; + int attemptCount = 0; + bool successfulWrite = false; + + while (!successfulWrite && attemptCount++ < maxAttempts) + { + if (!_settings.ProbingPaths.Add(probeIn)) + { + return; + } + + try + { + Save(); + successfulWrite = true; + } + catch + { + Task.Delay(10).Wait(); + } + } + } + + // This method does not save the settings, it just registers into the memory cache. +#if NET7_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "Component registration inspects and instantiates types via reflection.")] + [UnconditionalSuppressMessage("AOT", "IL2067", Justification = "Component type metadata is inspected at runtime.")] + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "Component type interfaces are enumerated at runtime.")] + [UnconditionalSuppressMessage("AOT", "IL3000", Justification = "Assembly.Location is used to register probing paths for component resolution.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "Component instantiation uses Activator.CreateInstance with runtime-resolved types.")] +#endif + private bool RegisterType(Type type) + { + if (!typeof(IIdentifiedComponent).IsAssignableFrom(type) || type.GetConstructor(Type.EmptyTypes) == null || !type.IsClass) + { + return false; + } + + IReadOnlyList interfaceTypesToRegisterFor = type.GetInterfaces().Where(x => x != typeof(IIdentifiedComponent) && typeof(IIdentifiedComponent).IsAssignableFrom(x)).ToList(); + if (interfaceTypesToRegisterFor.Count == 0) + { + return false; + } + + lock (_componentIdToAssemblyQualifiedTypeName) + { + IIdentifiedComponent instance = (IIdentifiedComponent)Activator.CreateInstance(type); + + foreach (Type interfaceType in interfaceTypesToRegisterFor) + { + AddComponent(interfaceType, instance); + AddProbingPath(Path.GetDirectoryName(type.Assembly.Location)); + + _componentIdToAssemblyQualifiedTypeName[instance.Id] = type.AssemblyQualifiedName; + _settings.ComponentGuidToAssemblyQualifiedName[instance.Id.ToString()] = type.AssemblyQualifiedName; + + if (!_componentIdsByType.TryGetValue(interfaceType, out HashSet idsForInterfaceType)) + { + _componentIdsByType[interfaceType] = idsForInterfaceType = new HashSet(); + } + idsForInterfaceType.Add(instance.Id); + + // for backwards compat & cleanup from when the keys were interfaceType.FullName + if (_settings.ComponentTypeToGuidList.TryGetValue(interfaceType.FullName, out HashSet idsFromOldStyleKey)) + { + _settings.ComponentTypeToGuidList.Remove(interfaceType.FullName); + } + + if (!_settings.ComponentTypeToGuidList.TryGetValue(interfaceType.AssemblyQualifiedName, out HashSet idsForInterfaceTypeForSettings)) + { + _settings.ComponentTypeToGuidList[interfaceType.AssemblyQualifiedName] = idsForInterfaceTypeForSettings = new HashSet(); + } + idsForInterfaceTypeForSettings.Add(instance.Id); + + // for backwards compat & cleanup from when the keys were interfaceType.FullName + if (idsFromOldStyleKey != null) + { + idsForInterfaceTypeForSettings.UnionWith(idsFromOldStyleKey); + } + } + + return true; + } + } + +#pragma warning disable SA1202 // Elements should be ordered by access + internal void Save() +#pragma warning restore SA1202 // Elements should be ordered by access + { + bool successfulWrite = false; + const int maxAttempts = 10; + int attemptCount = 0; + + while (!successfulWrite && attemptCount++ < maxAttempts) + { + try + { + _engineEnvironmentSettings.Host.FileSystem.WriteObject(_paths.SettingsFile, _settings, SettingsStoreJsonSerializerContext.Default.SettingsStore); + successfulWrite = true; + } + catch (IOException) + { + Task.Delay(10).Wait(); + } + } + } + +#pragma warning disable SA1202 // Elements should be ordered by access + public void AddComponent(Type type, IIdentifiedComponent component) +#pragma warning restore SA1202 // Elements should be ordered by access + { + if (!type.IsInstanceOfType(component)) + { + throw new ArgumentException($"{component.GetType().Name} should be assignable from {type.Name} type", nameof(type)); + } + + if (!ComponentCache.TryGetValue(type, out Dictionary typeCache)) + { + typeCache = new Dictionary(); + ComponentCache[type] = typeCache; + } + typeCache[component.Id] = component; + + if (!_componentIdsByType.TryGetValue(type, out HashSet ids)) + { + ids = new HashSet(); + _componentIdsByType[type] = ids; + } + ids.Add(component.Id); + } + +#if NET7_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "Assembly loading resolves assemblies by name at runtime.")] + [UnconditionalSuppressMessage("AOT", "IL2057", Justification = "Component type is resolved by stored assembly-qualified name.")] +#endif + private Type GetType(string typeName) + { + int commaIndex = typeName.IndexOf(','); + if (commaIndex < 0) + { + return Type.GetType(typeName); + } + + string asmName = typeName.Substring(commaIndex + 1).Trim(); + + if (!ReflectionLoadProbingPath.HasLoaded(asmName)) + { + AssemblyName name = new AssemblyName(asmName); +#if NET + AssemblyLoadContext.Default.LoadFromAssemblyName(name); +#else + AppDomain.CurrentDomain.Load(name); +#endif + } + + return Type.GetType(typeName); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/FilteredTemplateInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/FilteredTemplateInfo.cs new file mode 100644 index 000000000000..b5195b2d6e40 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/FilteredTemplateInfo.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Edge.Template; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + [Obsolete("Use ITemplateMatchInfo instead")] + public class FilteredTemplateInfo : IFilteredTemplateInfo + { + public FilteredTemplateInfo(ITemplateInfo info, IReadOnlyList matchDisposition) + { + Info = info; + MatchDisposition = matchDisposition; + } + + public ITemplateInfo Info { get; } + + public IReadOnlyList MatchDisposition { get; set; } + + public bool IsMatch => MatchDisposition.Count > 0 && !MatchDisposition.Any(x => x.Kind == MatchKind.Mismatch); + + // There is any criteria that is not Mismatch + // allowing context misses again + public bool IsPartialMatch => MatchDisposition.Any(x => x.Kind != MatchKind.Mismatch) + && MatchDisposition.All(x => x.Location != MatchLocation.Context + || (x.Location == MatchLocation.Context && x.Kind == MatchKind.Exact)); + + // All parameter matches are exact (or there are no parameter matches) + public bool HasParameterMismatch => MatchDisposition.Any(x => x.Location == MatchLocation.OtherParameter && x.Kind != MatchKind.Exact); + + public bool IsParameterMatch => !HasParameterMismatch && MatchDisposition.Any(x => x.Location == MatchLocation.OtherParameter); + + public bool HasInvalidParameterValue => MatchDisposition.Any(x => x.Location == MatchLocation.OtherParameter && x.Kind == MatchKind.InvalidParameterValue); + + public bool HasAmbiguousParameterMatch => !HasInvalidParameterValue && MatchDisposition.Any(x => x.Location == MatchLocation.OtherParameter && x.Kind == MatchKind.AmbiguousParameterValue); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/ITemplateInfoHostJsonCache.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/ITemplateInfoHostJsonCache.cs new file mode 100644 index 000000000000..caaf57a0d983 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/ITemplateInfoHostJsonCache.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + /// + /// Extension interface of which stores .host.json data in cache so host doesn't need to re-load it from .nupkg every time. + /// + public interface ITemplateInfoHostJsonCache + { + /// + /// Full content of .host.json file in JSON format. + /// + string? HostData { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/InstallationScope.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/InstallationScope.cs new file mode 100644 index 000000000000..db6cddf6b80e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/InstallationScope.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + /// + /// Defines the scope that managed by built-in providers. + /// + public enum InstallationScope + { + /// + /// Template packages are visible to all template hosts. + /// + Global = 0, + +#pragma warning disable CS1587 // XML comment is not placed on a valid language element + /// + /// Template packages are visible to all versions of certain template host. + /// + // Host = 1, //not supported at the moment + + /// + /// Template packages are visible to only to specific version of the host. + /// + // Version = 2 //not supported at the moment + } +#pragma warning restore CS1587 // XML comment is not placed on a valid language element +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/LocalizationCacheKeyComparer.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/LocalizationCacheKeyComparer.cs new file mode 100644 index 000000000000..680033a2763b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/LocalizationCacheKeyComparer.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + /// + /// Makes an ordinal, case insensitive comparison with the two strings of the given tuples. + /// + internal class LocalizationCacheKeyComparer : IEqualityComparer<(string? Key, string? Value)> + { + public bool Equals((string? Key, string? Value) x, (string? Key, string? Value) y) + { + return string.Equals(x.Key, y.Key, StringComparison.OrdinalIgnoreCase) && + string.Equals(x.Value, y.Value, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode((string? Key, string? Value) obj) + { + return (StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Key) * 17) + + StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Value); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/ScanResult.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/ScanResult.cs new file mode 100644 index 000000000000..d0267cb02d09 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/ScanResult.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + /// + /// Returned by . + /// + public class ScanResult : IDisposable + { + internal ScanResult( + IMountPoint mountPoint, + IReadOnlyList templates, + IReadOnlyList localizations, + IReadOnlyList<(string AssemblyPath, Type InterfaceType, IIdentifiedComponent Instance)> components) + { + MountPoint = mountPoint; + Templates = templates; +#pragma warning disable CS0618 // Type or member is obsolete + Localizations = localizations; +#pragma warning restore CS0618 // Type or member is obsolete + Components = components; + } + + /// + /// Gets the mount point that was scanned. + /// + public IMountPoint MountPoint { get; } + + /// + /// All components found inside mountpoint. + /// AssemblyPath is full path inside . + /// InterfaceType is type of interface that is implemented by component. + /// Instance is object that implements InterfaceType. + /// + public IReadOnlyList<(string AssemblyPath, Type InterfaceType, IIdentifiedComponent Instance)> Components { get; } + + /// + /// All template localizations found inside mountpoint. + /// + [Obsolete("Use IScanTemplateInfo.Localizations instead.")] + public IReadOnlyList Localizations { get; } + + /// + /// All templates found inside mountpoint. + /// + public IReadOnlyList Templates { get; } + + /// + /// Disposes that was scanned. + /// + public void Dispose() + { + MountPoint.Dispose(); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/Scanner.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/Scanner.cs new file mode 100644 index 000000000000..58d2c20ba2d8 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/Scanner.cs @@ -0,0 +1,334 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +#if NET +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Loader; +#endif +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Edge.Mount.FileSystem; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + /// + /// Utility for scanning for templates, localizations and components. + /// + public class Scanner + { + private readonly IEngineEnvironmentSettings _environmentSettings; + private readonly SettingsFilePaths _paths; + private readonly ILogger _logger; + + public Scanner(IEngineEnvironmentSettings environmentSettings) + { + _environmentSettings = environmentSettings; + _paths = new SettingsFilePaths(environmentSettings); + _logger = environmentSettings.Host.LoggerFactory.CreateLogger(); + } + + /// + /// Scans mount point for templates, localizations and components. + /// + /// + /// The mount point will not be disposed by the . Use to dispose mount point. + /// + [Obsolete("Use ScanAsync instead.")] + public ScanResult Scan(string mountPointUri) + { + return Scan(mountPointUri, scanForComponents: true); + } + + /// + /// Same as , however allows to enable or disable components scanning via . + /// + /// + /// The mount point will not be disposed by the . Use to dispose mount point. + /// + /// + [Obsolete("Use ScanAsync instead.")] + public ScanResult Scan(string mountPointUri, bool scanForComponents) + { + if (string.IsNullOrWhiteSpace(mountPointUri)) + { + throw new ArgumentException($"{nameof(mountPointUri)} should not be null or empty"); + } + MountPointScanSource source = GetOrCreateMountPointScanInfoForInstallSource(mountPointUri); + + if (scanForComponents) + { + ScanForComponents(source); + } + return Task.Run(async () => await ScanMountPointForTemplatesAsync(source, default).ConfigureAwait(false)).GetAwaiter().GetResult(); + } + + /// + /// Scans mount point for templates. + /// + /// + /// The mount point will not be disposed by the . Use to dispose mount point. + /// + public Task ScanAsync(string mountPointUri, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(mountPointUri)) + { + throw new ArgumentException($"{nameof(mountPointUri)} should not be null or empty"); + } + MountPointScanSource source = GetOrCreateMountPointScanInfoForInstallSource(mountPointUri); + cancellationToken.ThrowIfCancellationRequested(); + return ScanMountPointForTemplatesAsync(source, cancellationToken: cancellationToken); + } + + /// + /// Scans mount point for templates. + /// + /// + /// The mount point will not be disposed by the . Use to dispose mount point. + /// + public Task ScanAsync( + string mountPointUri, + bool logValidationResults = true, + bool returnInvalidTemplates = false, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(mountPointUri)) + { + throw new ArgumentException($"{nameof(mountPointUri)} should not be null or empty"); + } + MountPointScanSource source = GetOrCreateMountPointScanInfoForInstallSource(mountPointUri); + cancellationToken.ThrowIfCancellationRequested(); + return ScanMountPointForTemplatesAsync(source, logValidationResults, returnInvalidTemplates, cancellationToken); + } + + private MountPointScanSource GetOrCreateMountPointScanInfoForInstallSource(string sourceLocation) + { + foreach (IMountPointFactory factory in _environmentSettings.Components.OfType()) + { + if (factory.TryMount(_environmentSettings, null, sourceLocation, out IMountPoint? mountPoint)) + { + if (mountPoint is null) + { + throw new InvalidOperationException($"{nameof(mountPoint)} cannot be null when {nameof(factory.TryMount)} is 'true'"); + } + + // file-based and not originating in the scratch dir. + bool isLocalFlatFileSource = mountPoint is FileSystemMountPoint + && !sourceLocation.StartsWith(_paths.ScratchDir); + + return new MountPointScanSource( + location: sourceLocation, + mountPoint: mountPoint, + shouldStayInOriginalLocation: isLocalFlatFileSource, + foundComponents: false, + foundTemplates: false); + } + } + throw new Exception(string.Format(LocalizableStrings.Scanner_Error_TemplatePackageLocationIsNotSupported, sourceLocation)); + } + +#if NET + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "Component scanning uses Assembly.GetTypes() on dynamically loaded assemblies.")] +#endif + private void ScanForComponents(MountPointScanSource source) + { + _ = source ?? throw new ArgumentNullException(nameof(source)); + + bool isCopiedIntoContentDirectory; + + if (!source.MountPoint.Root.EnumerateFiles("*.dll", SearchOption.AllDirectories).Any()) + { + return; + } + + string? actualScanPath; + if (!source.ShouldStayInOriginalLocation) + { + if (!TryCopyForNonFileSystemBasedMountPoints(source.MountPoint, source.Location, _paths.Content, true, out actualScanPath) || actualScanPath == null) + { + return; + } + + isCopiedIntoContentDirectory = true; + } + else + { + actualScanPath = source.Location; + isCopiedIntoContentDirectory = false; + } + + foreach (KeyValuePair asm in LoadAllFromPath(out _, actualScanPath)) + { + try + { + IReadOnlyList typeList = asm.Value.GetTypes(); + + if (typeList.Count > 0) + { + // TODO: figure out what to do with probing path registration when components are not found. + // They need to be registered for dependent assemblies, not just when an assembly can be loaded. + // We'll need to figure out how to know when that is. +#pragma warning disable CS0618 // Type or member is obsolete + _environmentSettings.Components.RegisterMany(typeList); +#pragma warning restore CS0618 // Type or member is obsolete + source.FoundComponents = true; + } + } + catch + { + // exceptions here are ok, due to dependency errors, etc. + } + } + + if (!source.FoundComponents && isCopiedIntoContentDirectory) + { + try + { + // The source was copied to content and then scanned for components. + // Nothing was found, and this is a copy that now has no use, so delete it. + // Note: no mount point was created for this copy, so no need to release it. + _environmentSettings.Host.FileSystem.DirectoryDelete(actualScanPath, true); + } + catch (Exception ex) + { + _logger.LogDebug($"During ScanForComponents() cleanup, couldn't delete source copied into the content dir: {actualScanPath}. Details: {ex}."); + } + } + } + + private bool TryCopyForNonFileSystemBasedMountPoints(IMountPoint mountPoint, string sourceLocation, string targetBasePath, bool expandIfArchive, out string? diskPath) + { + string targetPath = Path.Combine(targetBasePath, Path.GetFileName(sourceLocation)); + + try + { + if (expandIfArchive) + { + mountPoint.Root.CopyTo(targetPath); + } + else + { + _environmentSettings.Host.FileSystem.CreateDirectory(targetBasePath); // creates Packages/ or Content/ if needed + _paths.Copy(sourceLocation, targetPath); + } + } + catch (IOException) + { + _logger.LogDebug($"Error copying scanLocation: {sourceLocation} into the target dir: {targetPath}"); + diskPath = null; + return false; + } + + diskPath = targetPath; + return true; + } + + private async Task ScanMountPointForTemplatesAsync( + MountPointScanSource source, + bool logValidationResults = true, + bool returnInvalidTemplates = false, + CancellationToken cancellationToken = default) + { + _ = source ?? throw new ArgumentNullException(nameof(source)); + + var templates = new List(); + foreach (IGenerator generator in _environmentSettings.Components.OfType()) + { + IReadOnlyList templateList = await generator.GetTemplatesFromMountPointAsync(source.MountPoint, cancellationToken).ConfigureAwait(false); + + if (logValidationResults) + { + _logger.LogDebug("Scanning mount point '{0}' by generator '{1}': found {2} templates", source.MountPoint.MountPointUri, generator.Id, templateList.Count); + ValidationUtils.LogValidationResults(_logger, templateList); + } + + IEnumerable validTemplates = templateList.Where(t => t.IsValid || returnInvalidTemplates); + templates.AddRange(validTemplates); + source.FoundTemplates |= validTemplates.Any(); + } + + //backward compatibility + var localizationLocators = templates.SelectMany(t => t.Localizations.Values.Where(li => li.IsValid || returnInvalidTemplates)).ToList(); + return new ScanResult(source.MountPoint, templates, localizationLocators, []); + } + + /// + /// Loads assemblies for components from the given . + /// + /// Errors happened when loading assemblies. + /// The path to load assemblies from. + /// Filename pattern to use when searching for files. + /// to use when searching for files. + /// The list of loaded assemblies in format (filename, loaded assembly). +#if NET + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "Assembly loading via AssemblyLoadContext.LoadFromStream() is inherently reflection-based.")] +#endif + private IEnumerable> LoadAllFromPath( + out IEnumerable loadFailures, + string path, + string pattern = "*.dll", + SearchOption searchOption = SearchOption.AllDirectories) + { + List> loaded = new List>(); + List failures = new List(); + + foreach (string file in _paths.EnumerateFiles(path, pattern, searchOption)) + { + try + { + Assembly? assembly = null; + +#if NET + if (file.IndexOf("netcoreapp", StringComparison.OrdinalIgnoreCase) > -1) + { + using Stream fileStream = _environmentSettings.Host.FileSystem.OpenRead(file); + assembly = AssemblyLoadContext.Default.LoadFromStream(fileStream); + } +#else + if (file.IndexOf("netstandard", StringComparison.OrdinalIgnoreCase) > -1 || file.IndexOf("net4", StringComparison.OrdinalIgnoreCase) > -1) + { + byte[] fileBytes = _environmentSettings.Host.FileSystem.ReadAllBytes(file); + assembly = Assembly.Load(fileBytes); + } +#endif + + if (assembly != null) + { + loaded.Add(new KeyValuePair(file, assembly)); + } + } + catch + { + failures.Add(file); + } + } + + loadFailures = failures; + return loaded; + } + + private class MountPointScanSource + { + public MountPointScanSource(string location, IMountPoint mountPoint, bool shouldStayInOriginalLocation, bool foundComponents, bool foundTemplates) + { + Location = location; + MountPoint = mountPoint; + ShouldStayInOriginalLocation = shouldStayInOriginalLocation; + FoundComponents = foundComponents; + FoundTemplates = foundTemplates; + } + + public string Location { get; } + + public IMountPoint MountPoint { get; } + + public bool ShouldStayInOriginalLocation { get; } + + public bool FoundComponents { get; set; } + + public bool FoundTemplates { get; set; } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SettingsFilePaths.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SettingsFilePaths.cs new file mode 100644 index 000000000000..3b8658b33584 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SettingsFilePaths.cs @@ -0,0 +1,164 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + internal class SettingsFilePaths + { + private readonly IEngineEnvironmentSettings _environmentSettings; + private string? _aliasesFile; + private string? _firstRunCookie; + private string? _scratchDir; + private string? _settingsFile; + private string? _contentDir; + private string? _templatesCacheFile; + + internal SettingsFilePaths(IEngineEnvironmentSettings environmentSettings) + { + _environmentSettings = environmentSettings; + } + + internal string AliasesFile => GetOrComputePath(ref _aliasesFile, BaseDir, "aliases.json"); + + internal string BaseDir => _environmentSettings.Paths.HostVersionSettingsDir; + + internal string Content => GetOrComputePath(ref _contentDir, BaseDir, "content"); + + internal string FirstRunCookie => GetOrComputePath(ref _firstRunCookie, BaseDir, ".firstrun"); + + internal string ScratchDir => GetOrComputePath(ref _scratchDir, BaseDir, "scratch"); + + internal string SettingsFile => GetOrComputePath(ref _settingsFile, BaseDir, "settings.json"); + + internal string TemplateCacheFile => GetOrComputePath(ref _templatesCacheFile, BaseDir, "templatecache.json"); + + internal void Copy(string path, string targetPath) + { + if (_environmentSettings.Host.FileSystem.FileExists(path)) + { + _environmentSettings.Host.FileSystem.FileCopy(path, targetPath, true); + return; + } + + foreach (string p in EnumerateFiles(path, "*", SearchOption.AllDirectories).OrderBy(x => x.Length)) + { + string localPath = p.Substring(path.Length).TrimStart('\\', '/'); + + if (_environmentSettings.Host.FileSystem.DirectoryExists(p)) + { + CreateDirectory(localPath, targetPath); + } + else + { + int parentDirEndIndex = localPath.LastIndexOfAny(new[] { '/', '\\' }); + string wholeTargetPath = Path.Combine(targetPath, localPath); + + if (parentDirEndIndex > -1) + { + string parentDir = localPath.Substring(0, parentDirEndIndex); + CreateDirectory(parentDir, targetPath); + } + + _environmentSettings.Host.FileSystem.FileCopy(p, wholeTargetPath, true); + } + } + } + + internal void Delete(string path) + { + if (_environmentSettings.Host.FileSystem.DirectoryExists(path)) + { + _environmentSettings.Host.FileSystem.DirectoryDelete(path, true); + } + if (_environmentSettings.Host.FileSystem.FileExists(path)) + { + _environmentSettings.Host.FileSystem.FileDelete(path); + } + } + + internal IEnumerable EnumerateDirectories(string path, string pattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + if (_environmentSettings.Host.FileSystem.DirectoryExists(path)) + { + return _environmentSettings.Host.FileSystem.EnumerateDirectories(path, pattern, searchOption); + } + + return Enumerable.Empty(); + } + + internal IEnumerable EnumerateFiles(string path, string pattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + if (_environmentSettings.Host.FileSystem.FileExists(path)) + { + return new[] { path }; + } + + if (_environmentSettings.Host.FileSystem.DirectoryExists(path)) + { + return _environmentSettings.Host.FileSystem.EnumerateFiles(ProcessPath(path), pattern, searchOption); + } + + return Enumerable.Empty(); + } + + internal IEnumerable EnumerateFileSystemEntries(string path, string pattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + if (_environmentSettings.Host.FileSystem.FileExists(path)) + { + return new[] { path }; + } + + if (_environmentSettings.Host.FileSystem.DirectoryExists(path)) + { + return _environmentSettings.Host.FileSystem.EnumerateFileSystemEntries(ProcessPath(path), pattern, searchOption); + } + + return Enumerable.Empty(); + } + + internal bool Exists(string path) + { + return _environmentSettings.Host.FileSystem.FileExists(path) || _environmentSettings.Host.FileSystem.DirectoryExists(path); + } + + internal string Name(string path) + { + path = path.TrimEnd('/', '\\'); + return Path.GetFileName(path); + } + + private void CreateDirectory(string path, string parent) + { + string[] parts = path.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + string current = parent; + + for (int i = 0; i < parts.Length; ++i) + { + current = Path.Combine(current, parts[i]); + _environmentSettings.Host.FileSystem.CreateDirectory(current); + } + } + + private string ProcessPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return path; + } + + if (path[0] != '~') + { + return path; + } + + return Path.Combine(_environmentSettings.Paths.UserProfileDir, path.Substring(1)); + } + + private string GetOrComputePath(ref string? cache, params string[] paths) + { + return cache ??= Path.Combine(paths); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SettingsStore.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SettingsStore.cs new file mode 100644 index 000000000000..688f8d3de9fa --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SettingsStore.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + internal class SettingsStore + { + internal SettingsStore(JsonObject? obj) + { + if (obj == null) + { + return; + } + + if (obj.TryGetValueCaseInsensitive(nameof(ComponentGuidToAssemblyQualifiedName), out JsonNode? componentGuidToAssemblyQualifiedNameToken)) + { + if (componentGuidToAssemblyQualifiedNameToken is JsonObject componentGuidToAssemblyQualifiedNameObject) + { + foreach (var entry in componentGuidToAssemblyQualifiedNameObject) + { + if (entry.Value?.GetValueKind() == JsonValueKind.String) + { + ComponentGuidToAssemblyQualifiedName[entry.Key] = entry.Value.GetValue(); + } + } + } + } + + if (obj.TryGetValueCaseInsensitive(nameof(ProbingPaths), out JsonNode? probingPathsToken)) + { + if (probingPathsToken is JsonArray probingPathsArray) + { + foreach (JsonNode? path in probingPathsArray) + { + if (path?.GetValueKind() == JsonValueKind.String) + { + ProbingPaths.Add(path.GetValue()); + } + } + } + } + + if (obj.TryGetValueCaseInsensitive(nameof(ComponentTypeToGuidList), out JsonNode? componentTypeToGuidListToken)) + { + if (componentTypeToGuidListToken is JsonObject componentTypeToGuidListObject) + { + foreach (var entry in componentTypeToGuidListObject) + { + if (entry.Value is JsonArray values) + { + HashSet set = new HashSet(); + ComponentTypeToGuidList[entry.Key] = set; + + foreach (JsonNode? value in values) + { + if (value?.GetValueKind() == JsonValueKind.String) + { + if (Guid.TryParse(value.GetValue(), out Guid id)) + { + set.Add(id); + } + } + } + } + } + } + } + } + + [JsonInclude] + internal Dictionary ComponentGuidToAssemblyQualifiedName { get; } = new(); + + [JsonInclude] + internal HashSet ProbingPaths { get; } = new(); + + [JsonInclude] + internal Dictionary> ComponentTypeToGuidList { get; } = new(); + + internal static SettingsStore Load(IEngineEnvironmentSettings engineEnvironmentSettings, SettingsFilePaths paths) + { + if (!paths.Exists(paths.SettingsFile)) + { + return new SettingsStore(null); + } + + JsonObject parsed; + using (Timing.Over(engineEnvironmentSettings.Host.Logger, "Parse settings")) + { + try + { + parsed = engineEnvironmentSettings.Host.FileSystem.ReadObject(paths.SettingsFile); + } + catch (Exception ex) + { + throw new EngineInitializationException("Error parsing the user settings file", "Settings File", ex); + } + } + SettingsStore settingsStore; + using (Timing.Over(engineEnvironmentSettings.Host.Logger, "Deserialize user settings")) + { + settingsStore = new SettingsStore(parsed); + } + + using (Timing.Over(engineEnvironmentSettings.Host.Logger, "Init probing paths")) + { + if (settingsStore.ProbingPaths.Count == 0) + { + settingsStore.ProbingPaths.Add(paths.Content); + } + } + return settingsStore; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SettingsStoreJsonSerializerContext.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SettingsStoreJsonSerializerContext.cs new file mode 100644 index 000000000000..b3d7753e4ee3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SettingsStoreJsonSerializerContext.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + [JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + [JsonSerializable(typeof(SettingsStore))] + internal partial class SettingsStoreJsonSerializerContext : JsonSerializerContext; +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SourceRepositoryDependencyProvider.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SourceRepositoryDependencyProvider.cs new file mode 100644 index 000000000000..e28748eb11f2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/SourceRepositoryDependencyProvider.cs @@ -0,0 +1,208 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//namespace Microsoft.TemplateEngine.Edge.Settings +//{ +// public class SourceRepositoryDependencyProvider : IRemoteDependencyProvider +// { +// private readonly object _lock = new object(); +// private readonly SourceRepository _sourceRepository; +// private readonly ILogger _logger; +// private readonly SourceCacheContext _cacheContext; +// private FindPackageByIdResource _findPackagesByIdResource; +// private readonly bool _ignoreFailedSources; +// private readonly bool _ignoreWarning; + +// // Limiting concurrent requests to limit the amount of files open at a time on Mac OSX +// // the default is 256 which is easy to hit if we don't limit concurrency +// private static readonly SemaphoreSlim Throttle = +// RuntimeEnvironmentHelper.IsMacOSX +// ? new SemaphoreSlim(ConcurrencyLimit, ConcurrencyLimit) +// : null; + +// // In order to avoid too many open files error, set concurrent requests number to 16 on Mac +// private const int ConcurrencyLimit = 16; + +// public SourceRepositoryDependencyProvider( +// SourceRepository sourceRepository, +// ILogger logger, +// SourceCacheContext cacheContext) +// : this(sourceRepository, logger, cacheContext, cacheContext.IgnoreFailedSources) +// { +// } + +// public SourceRepositoryDependencyProvider( +// SourceRepository sourceRepository, +// ILogger logger, +// SourceCacheContext cacheContext, +// bool ignoreFailedSources) +// : this(sourceRepository, logger, cacheContext, ignoreFailedSources, false) +// { +// } + +// public SourceRepositoryDependencyProvider( +// SourceRepository sourceRepository, +// ILogger logger, +// SourceCacheContext cacheContext, +// bool ignoreFailedSources, +// bool ignoreWarning) +// { +// _sourceRepository = sourceRepository; +// _logger = logger; +// _cacheContext = cacheContext; +// _ignoreFailedSources = ignoreFailedSources; +// _ignoreWarning = ignoreWarning; +// } + +// public bool IsHttp => _sourceRepository.PackageSource.IsHttp; + +// public async Task FindLibraryAsync(LibraryRange libraryRange, NuGetFramework targetFramework, CancellationToken cancellationToken) +// { +// await EnsureResource(); + +// IEnumerable packageVersions; + +// try +// { +// if (Throttle != null) +// { +// await Throttle.WaitAsync(); +// } +// packageVersions = await _findPackagesByIdResource.GetAllVersionsAsync(libraryRange.Name, cancellationToken); +// } +// catch (FatalProtocolException e) when (_ignoreFailedSources) +// { +// if (!_ignoreWarning) +// { +// _logger.LogWarning(e.Message); +// } +// return null; +// } +// finally +// { +// Throttle?.Release(); +// } + +// NuGetVersion packageVersion = packageVersions?.FindBestMatch(libraryRange.VersionRange, version => version); + +// if (packageVersion != null) +// { +// return new LibraryIdentity +// { +// Name = libraryRange.Name, +// Version = packageVersion, +// Type = LibraryType.Package +// }; +// } + +// return null; +// } + +// public async Task> GetDependenciesAsync(LibraryIdentity match, NuGetFramework targetFramework, CancellationToken cancellationToken) +// { +// await EnsureResource(); + +// FindPackageByIdDependencyInfo packageInfo; +// try +// { +// if (Throttle != null) +// { +// await Throttle.WaitAsync(); +// } +// packageInfo = await _findPackagesByIdResource.GetDependencyInfoAsync(match.Name, match.Version, cancellationToken); +// } +// catch (FatalProtocolException e) when (_ignoreFailedSources) +// { +// if (!_ignoreWarning) +// { +// _logger.LogWarning(e.Message); +// } +// return new List(); +// } +// finally +// { +// Throttle?.Release(); +// } + +// return GetDependencies(packageInfo, targetFramework); +// } + +// public async Task CopyToAsync(LibraryIdentity identity, Stream stream, CancellationToken cancellationToken) +// { +// await EnsureResource(); + +// try +// { +// if (Throttle != null) +// { +// await Throttle.WaitAsync(); +// } + +// using (Stream nupkgStream = await _findPackagesByIdResource.GetNupkgStreamAsync(identity.Name, identity.Version, cancellationToken)) +// { +// cancellationToken.ThrowIfCancellationRequested(); + +// // If the stream is already available, do not stop in the middle of copying the stream +// // Pass in CancellationToken.None +// await nupkgStream.CopyToAsync(stream, bufferSize: 8192, cancellationToken: CancellationToken.None); +// } +// } +// catch (FatalProtocolException e) when (_ignoreFailedSources) +// { +// if (!_ignoreWarning) +// { +// _logger.LogWarning(e.Message); +// } +// } +// finally +// { +// Throttle?.Release(); +// } +// } + +// private IEnumerable GetDependencies(FindPackageByIdDependencyInfo packageInfo, NuGetFramework targetFramework) +// { +// if (packageInfo == null) +// { +// return new List(); +// } +// PackageDependencyGroup dependencies = NuGetFrameworkUtility.GetNearest(packageInfo.DependencyGroups, +// targetFramework, +// item => item.TargetFramework); + +// return GetDependencies(targetFramework, dependencies); +// } + +// private static IList GetDependencies(NuGetFramework targetFramework, +// PackageDependencyGroup dependencies) +// { +// List libraryDependencies = new List(); + +// if (dependencies != null) +// { +// libraryDependencies.AddRange( +// dependencies.Packages.Select(PackagingUtility.GetLibraryDependencyFromNuspec)); +// } + +// return libraryDependencies; +// } + +// private async Task EnsureResource() +// { +// if (_findPackagesByIdResource == null) +// { +// FindPackageByIdResource resource = await _sourceRepository.GetResourceAsync(); +// resource.Logger = _logger; +// resource.CacheContext = _cacheContext; + +// lock (_lock) +// { +// if (_findPackagesByIdResource == null) +// { +// _findPackagesByIdResource = resource; +// } +// } +// } +// } +// } +//} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateCache.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateCache.cs new file mode 100644 index 000000000000..39cde883ea57 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateCache.cs @@ -0,0 +1,250 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + internal class TemplateCache + { + private const string BulletSymbol = "\u2022"; + private static readonly Guid RunnableProjectGeneratorId = new("0C434DF7-E2CB-4DEE-B216-D7C58C8EB4B3"); + + public TemplateCache(IReadOnlyList allTemplatePackages, ScanResult?[] scanResults, Dictionary mountPoints, IEngineEnvironmentSettings environmentSettings) + { + ILogger logger = environmentSettings.Host.Logger; + + // We need this dictionary to de-duplicate templates that have same identity + // notice that IEnumerable that we get in is order by priority which means + // last template with same Identity will win, others will be ignored... + var templateDeduplicationDictionary = new Dictionary>(); + foreach (var scanResult in scanResults) + { + if (scanResult == null) + { + continue; + } + + foreach (IScanTemplateInfo template in scanResult.Templates) + { + var templatePackage = allTemplatePackages.FirstOrDefault(tp => tp.MountPointUri == template.MountPointUri); + + if (templateDeduplicationDictionary.ContainsKey(template.Identity)) + { + templateDeduplicationDictionary[template.Identity].Add((template, templatePackage, GetBestLocalizationLocatorMatch(template), scanResult.MountPoint)); + } + else + { + templateDeduplicationDictionary[template.Identity] = new List<(IScanTemplateInfo Template, ITemplatePackage TemplatePackage, ILocalizationLocator? Localization, IMountPoint)> + { + (template, templatePackage, GetBestLocalizationLocatorMatch(template), scanResult.MountPoint) + }; + } + } + } + + var templates = new List(); + foreach (var duplicatedIdentities in templateDeduplicationDictionary) + { + // last template with same Identity wins, others will be ignored due to applied deduplication logic + (IScanTemplateInfo Template, ITemplatePackage TemplatePackage, ILocalizationLocator? Localization, IMountPoint MountPoint) chosenTemplate = duplicatedIdentities.Value.Last(); + + ILocalizationLocator? loc = GetBestLocalizationLocatorMatch(chosenTemplate.Template); + (string, JsonObject?)? hostFile = GetBestHostConfigMatch(chosenTemplate.Template, environmentSettings, chosenTemplate.MountPoint); + + templates.Add(new TemplateInfo(chosenTemplate.Template, loc, hostFile)); + } + + Version = Settings.TemplateInfo.CurrentVersion; + Locale = CultureInfo.CurrentUICulture.Name; + TemplateInfo = templates; + MountPointsInfo = mountPoints; + + PrintOverlappingIdentityWarning(logger, templateDeduplicationDictionary); + } + + public TemplateCache(JsonObject? contentJObject) + { + if (contentJObject != null && contentJObject.TryGetValueCaseInsensitive(nameof(Version), out JsonNode? versionToken)) + { + Version = versionToken!.ToJsonString().Trim('"'); + } + else + { + Version = null; + TemplateInfo = []; + MountPointsInfo = new Dictionary(); + Locale = string.Empty; + return; + } + + Locale = contentJObject.TryGetValueCaseInsensitive(nameof(Locale), out JsonNode? localeToken) + ? localeToken!.GetValue() + : string.Empty; + + var mountPointInfo = new Dictionary(); + + if (contentJObject.TryGetValueCaseInsensitive(nameof(MountPointsInfo), out JsonNode? mountPointInfoToken) && mountPointInfoToken is JsonObject mountPointInfoObj) + { + foreach (var entry in mountPointInfoObj) + { + if (entry.Value != null) + { + mountPointInfo.Add(entry.Key, entry.Value.GetValue()); + } + } + } + + MountPointsInfo = mountPointInfo; + + List templateList = new List(); + + if (contentJObject.TryGetValueCaseInsensitive(nameof(TemplateInfo), out JsonNode? templateInfoToken) && templateInfoToken is JsonArray arr) + { + foreach (JsonNode? entry in arr) + { + if (entry is JsonObject entryObj) + { + templateList.Add(Settings.TemplateInfo.FromJObject(entryObj)); + } + } + } + + TemplateInfo = templateList; + } + + [JsonPropertyName("Version")] + public string? Version { get; } + + [JsonPropertyName("Locale")] + public string Locale { get; } + + [JsonPropertyName("TemplateInfo")] + public IReadOnlyList TemplateInfo { get; } + + [JsonPropertyName("MountPointsInfo")] + public Dictionary MountPointsInfo { get; } + + private ILocalizationLocator? GetBestLocalizationLocatorMatch(IScanTemplateInfo template) + { + if (template.Localizations is null) + { + return null; + } + + if (!template.Localizations.Any()) + { + return null; + } + + string? bestMatch = GetBestLocaleMatch(template.Localizations.Keys); + if (string.IsNullOrWhiteSpace(bestMatch)) + { + return null; + } + return template.Localizations[bestMatch!]; + } + + /// see https://source.dot.net/#System.Private.CoreLib/ResourceFallbackManager.cs. + private string? GetBestLocaleMatch(IEnumerable availableLocalizations) + { + CultureInfo currentCulture = CultureInfo.CurrentUICulture; + do + { + if (availableLocalizations.Contains(currentCulture.Name, StringComparer.OrdinalIgnoreCase)) + { + return currentCulture.Name; + } + currentCulture = currentCulture.Parent; + } + while (currentCulture.Name != CultureInfo.InvariantCulture.Name); + return null; + } + + // add warning for the case when there is an attempt to overwrite existing managed by new managed template + private void PrintOverlappingIdentityWarning(ILogger logger, IDictionary> templateDeduplicationDictionary) + { + foreach (var identityToTemplates in templateDeduplicationDictionary) + { + // we print the message only if managed template wins and we have > 1 managed templates with overlapping identities + var lastTemplate = identityToTemplates.Value.Last(); + var managedTemplates = identityToTemplates.Value.Where(templateInto => templateInto.TemplatePackage is IManagedTemplatePackage).ToArray(); + if (lastTemplate.TemplatePackage is IManagedTemplatePackage && managedTemplates.Length > 1) + { + var templatesList = new StringBuilder(); + foreach (var (templateName, packageId, _, _) in managedTemplates) + { + templatesList.AppendLine(string.Format( + LocalizableStrings.TemplatePackageManager_Warning_DetectedTemplatesIdentityConflict_Subentry, + BulletSymbol, + templateName.Name, + (packageId as IManagedTemplatePackage)?.DisplayName)); + } + + logger.LogWarning(string.Format( + LocalizableStrings.TemplatePackageManager_Warning_DetectedTemplatesIdentityConflict, + identityToTemplates.Key, + templatesList.ToString().TrimEnd(Environment.NewLine.ToCharArray()), + lastTemplate.Template.Name)); + } + } + } + + private (string, JsonObject?)? GetBestHostConfigMatch(IScanTemplateInfo newTemplate, IEngineEnvironmentSettings settings, IMountPoint mountPoint) + { + if (newTemplate.HostConfigFiles.TryGetValue(settings.Host.HostIdentifier, out string? preferredHostFilePath)) + { + return (preferredHostFilePath, ReadHostFile(newTemplate, preferredHostFilePath, settings, mountPoint)); + } + + foreach (string fallbackHostName in settings.Host.FallbackHostTemplateConfigNames) + { + if (newTemplate.HostConfigFiles.TryGetValue(fallbackHostName, out string? fallbackHostFilePath)) + { + return (fallbackHostFilePath, ReadHostFile(newTemplate, fallbackHostFilePath, settings, mountPoint)); + } + } + return null; + } + + private JsonObject? ReadHostFile(IScanTemplateInfo template, string path, IEngineEnvironmentSettings settings, IMountPoint mountPoint) + { + if (template.GeneratorId != RunnableProjectGeneratorId) + { + return null; + } + settings.Host.Logger.LogDebug($"Start loading host config {template.MountPointUri}{path}"); + try + { + IFile? hostFile = mountPoint.FileInfo(path); + if (hostFile == null || !hostFile.Exists) + { + throw new FileNotFoundException($"Host file '{hostFile?.GetDisplayPath()}' does not exist."); + } + return hostFile.ReadJObjectFromIFile(); + } + catch (Exception e) + { + settings.Host.Logger.LogWarning( + e, + LocalizableStrings.TemplateInfo_Warning_FailedToReadHostData, + template.MountPointUri, + path); + } + finally + { + settings.Host.Logger.LogDebug($"End loading host config {template.MountPointUri}{path}"); + } + return null; + } + + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateCacheJsonSerializerContext.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateCacheJsonSerializerContext.cs new file mode 100644 index 000000000000..c429de2e4f0c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateCacheJsonSerializerContext.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + [JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + [JsonSerializable(typeof(TemplateCache))] + internal partial class TemplateCacheJsonSerializerContext : JsonSerializerContext; +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateInfo.cs new file mode 100644 index 000000000000..c762c3be7475 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateInfo.cs @@ -0,0 +1,299 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + internal partial class TemplateInfo : ITemplateInfo, ITemplateInfoHostJsonCache + { + internal const string CurrentVersion = "1.0.0.7"; + +#pragma warning disable CS0618 // Type or member is obsolete + private IReadOnlyDictionary? _tags; + private IReadOnlyDictionary? _cacheParameters; +#pragma warning restore CS0618 // Type or member is obsolete + + internal TemplateInfo(string identity, string name, IEnumerable shortNames, string mountPointUri, string configPlace) + { + if (string.IsNullOrWhiteSpace(identity)) + { + throw new ArgumentException($"'{nameof(identity)}' cannot be null or whitespace.", nameof(identity)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or whitespace.", nameof(name)); + } + + if (shortNames is null) + { + throw new ArgumentNullException(nameof(shortNames)); + } + if (!shortNames.Any()) + { + throw new ArgumentException($"'{nameof(shortNames)}' should contain at least one entry.", nameof(shortNames)); + } + if (shortNames.Any(string.IsNullOrWhiteSpace)) + { + throw new ArgumentException($"'{nameof(shortNames)}' should not contain empty values.", nameof(shortNames)); + } + + if (string.IsNullOrWhiteSpace(mountPointUri)) + { + throw new ArgumentException($"'{nameof(mountPointUri)}' cannot be null or whitespace.", nameof(mountPointUri)); + } + + if (string.IsNullOrWhiteSpace(configPlace)) + { + throw new ArgumentException($"'{nameof(configPlace)}' cannot be null or whitespace.", nameof(configPlace)); + } + + Identity = identity; + Name = name; + MountPointUri = mountPointUri; + ConfigPlace = configPlace; + ShortNameList = shortNames.ToList(); + } + + /// + /// Localization copy-constructor. + /// + /// unlocalized template. + /// localization information. + /// host config information. + internal TemplateInfo(IScanTemplateInfo template, ILocalizationLocator? localizationInfo, (string Path, JsonObject? Content)? hostConfig) + { + if (template is null) + { + throw new ArgumentNullException(nameof(template)); + } + GeneratorId = template.GeneratorId; + ConfigPlace = template.ConfigPlace; + MountPointUri = template.MountPointUri; + TagsCollection = template.TagsCollection; + Classifications = template.Classifications; + GroupIdentity = template.GroupIdentity; + Precedence = template.Precedence; + Identity = template.Identity; + DefaultName = template.DefaultName; + PreferDefaultName = template.PreferDefaultName; + HostConfigPlace = hostConfig?.Path; + ThirdPartyNotices = template.ThirdPartyNotices; + BaselineInfo = template.BaselineInfo; + ShortNameList = template.ShortNameList; + PostActions = template.PostActions; + Constraints = template.Constraints; + + LocaleConfigPlace = localizationInfo?.ConfigPlace; + + Author = localizationInfo?.Author ?? template.Author; + Description = localizationInfo?.Description ?? template.Description; + + Name = localizationInfo?.Name ?? template.Name; + ParameterDefinitions = LocalizeParameters(template, localizationInfo); + HostData = hostConfig?.Content?.ToJsonString(); + } + +#pragma warning disable CS0618 // Type or member is obsolete + [JsonPropertyName("Parameters")] +#pragma warning restore CS0618 // Type or member is obsolete + public IParameterDefinitionSet ParameterDefinitions { get; private set; } = ParameterDefinitionSet.Empty; + + [JsonIgnore] + [Obsolete("Use ParameterDefinitionSet instead.")] + public IReadOnlyList Parameters => ParameterDefinitions; + + [JsonPropertyName("MountPointUri")] + public string MountPointUri { get; } + + [JsonPropertyName("Author")] + public string? Author { get; private set; } + + [JsonPropertyName("Classifications")] + public IReadOnlyList Classifications { get; private set; } = new List(); + + [JsonPropertyName("DefaultName")] + public string? DefaultName { get; private set; } + + [JsonPropertyName("Description")] + public string? Description { get; private set; } + + [JsonPropertyName("Identity")] + public string Identity { get; } + + [JsonPropertyName("GeneratorId")] + public Guid GeneratorId { get; private set; } + + [JsonPropertyName("GroupIdentity")] + public string? GroupIdentity { get; private set; } + + [JsonPropertyName("Precedence")] + public int Precedence { get; private set; } + + [JsonPropertyName("Name")] + public string Name { get; } + + [JsonIgnore] + [Obsolete] + string ITemplateInfo.ShortName + { + get + { + if (ShortNameList.Count > 0) + { + return ShortNameList[0]; + } + + return string.Empty; + } + } + + public IReadOnlyList ShortNameList { get; } = new List(); + + [JsonPropertyName("PreferDefaultName")] + public bool PreferDefaultName { get; private set; } + + [JsonIgnore] + [Obsolete] + IReadOnlyDictionary ITemplateInfo.Tags + { + get + { + if (_tags == null) + { + Dictionary tags = new Dictionary(); + foreach (KeyValuePair tag in TagsCollection) + { + tags[tag.Key] = new CacheTag(null, null, new Dictionary { { tag.Value, new ParameterChoice(null, null) } }, tag.Value); + } + foreach (ITemplateParameter parameter in ParameterDefinitions.Where(TemplateParameterExtensions.IsChoice)) + { + IReadOnlyDictionary choices = parameter.Choices ?? new Dictionary(); + tags[parameter.Name] = new CacheTag(parameter.DisplayName, parameter.Documentation, choices, parameter.DefaultValue); + } + return _tags = tags; + } + return _tags; + } + } + + [JsonIgnore] + [Obsolete] + IReadOnlyDictionary ITemplateInfo.CacheParameters + { + get + { + if (_cacheParameters == null) + { + Dictionary cacheParameters = new Dictionary(); + foreach (ITemplateParameter parameter in ParameterDefinitions.Where(p => !p.IsChoice())) + { + cacheParameters[parameter.Name] = new CacheParameter() + { + DataType = parameter.DataType, + DefaultValue = parameter.DefaultValue, + Description = parameter.Documentation, + DefaultIfOptionWithoutValue = parameter.DefaultIfOptionWithoutValue, + DisplayName = parameter.DisplayName + + }; + } + return _cacheParameters = cacheParameters; + } + return _cacheParameters; + } + } + + [JsonPropertyName("ConfigPlace")] + public string ConfigPlace { get; } + + [JsonPropertyName("LocaleConfigPlace")] + public string? LocaleConfigPlace { get; private set; } + + [JsonPropertyName("HostConfigPlace")] + public string? HostConfigPlace { get; private set; } + + [JsonPropertyName("ThirdPartyNotices")] + public string? ThirdPartyNotices { get; private set; } + + [JsonPropertyName("BaselineInfo")] + public IReadOnlyDictionary BaselineInfo { get; private set; } = new Dictionary(); + + [JsonPropertyName("TagsCollection")] + public IReadOnlyDictionary TagsCollection { get; private set; } = new Dictionary(); + + [JsonIgnore] + bool ITemplateInfo.HasScriptRunningPostActions { get; set; } + + public string? HostData { get; private set; } + + [JsonPropertyName("PostActions")] + public IReadOnlyList PostActions { get; private set; } = []; + + [JsonPropertyName("Constraints")] + public IReadOnlyList Constraints { get; private set; } = []; + + public static TemplateInfo FromJObject(JsonObject entry) + { + return TemplateInfoReader.FromJObject(entry); + } + + private static IParameterDefinitionSet LocalizeParameters(IScanTemplateInfo template, ILocalizationLocator? localizationInfo) + { + //we would like to copy the parameters to format supported for serialization as we cannot be sure that ITemplateInfo supports serialization in needed format. + List localizedParameters = new List(); + + foreach (ITemplateParameter parameter in template.ParameterDefinitions) + { + IParameterSymbolLocalizationModel? localization = null; + Dictionary? localizedChoices = null; + if (localizationInfo != null) + { + if (!localizationInfo.ParameterSymbols.TryGetValue(parameter.Name, out localization)) + { + // There is no localization for this symbol. Use the symbol as is. + localizedParameters.Add(parameter); + continue; + } + if (parameter.IsChoice() && parameter.Choices != null) + { + localizedChoices = new Dictionary(); + foreach (KeyValuePair templateChoice in parameter.Choices) + { + ParameterChoice localizedChoice = new ParameterChoice( + templateChoice.Value.DisplayName, + templateChoice.Value.Description); + + if (localization.Choices.TryGetValue(templateChoice.Key, out ParameterChoiceLocalizationModel locModel)) + { + localizedChoice.Localize(locModel); + } + localizedChoices.Add(templateChoice.Key, localizedChoice); + } + } + } + + TemplateParameter localizedParameter = new TemplateParameter( + name: parameter.Name, + displayName: localization?.DisplayName ?? parameter.DisplayName, + description: localization?.Description ?? parameter.Description, + defaultValue: parameter.DefaultValue, + defaultIfOptionWithoutValue: parameter.DefaultIfOptionWithoutValue, + datatype: parameter.DataType, + precedence: parameter.Precedence, + type: parameter.Type, + allowMultipleValues: parameter.AllowMultipleValues, + choices: localizedChoices ?? parameter.Choices); + + localizedParameters.Add(localizedParameter); + } + return new ParameterDefinitionSet(localizedParameters); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateInfoReader.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateInfoReader.cs new file mode 100644 index 000000000000..60a2e2b14365 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateInfoReader.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + internal partial class TemplateInfo + { + internal class TemplateInfoReader + { + internal static TemplateInfo FromJObject(JsonObject entry) + { + string identity = entry.ToString(nameof(Identity)) ?? throw new ArgumentException($"{nameof(entry)} doesn't have {nameof(Identity)} property.", nameof(entry)); + string name = entry.ToString(nameof(Name)) ?? throw new ArgumentException($"{nameof(entry)} doesn't have {nameof(Name)} property.", nameof(entry)); + string mountPointUri = entry.ToString(nameof(MountPointUri)) ?? throw new ArgumentException($"{nameof(entry)} doesn't have {nameof(MountPointUri)} property.", nameof(entry)); + string configPlace = entry.ToString(nameof(ConfigPlace)) ?? throw new ArgumentException($"{nameof(entry)} doesn't have {nameof(ConfigPlace)} property.", nameof(entry)); + JsonNode? shortNameToken = entry.Get(nameof(ShortNameList)); + IEnumerable shortNames = shortNameToken.JTokenStringOrArrayToCollection([]); + + TemplateInfo info = new TemplateInfo(identity, name, shortNames, mountPointUri, configPlace) + { + Author = entry.ToString(nameof(Author)) + }; + JsonArray? classificationsArray = entry.Get(nameof(Classifications)); + if (classificationsArray != null) + { + List classifications = new List(); + foreach (JsonNode? item in classificationsArray) + { + if (item != null) + { + classifications.Add(item.GetValue()); + } + } + info.Classifications = classifications; + } + + info.DefaultName = entry.ToString(nameof(DefaultName)); + info.PreferDefaultName = entry.ToBool(nameof(PreferDefaultName)); + info.Description = entry.ToString(nameof(Description)); + info.GeneratorId = Guid.Parse(entry.ToString(nameof(GeneratorId))); + info.GroupIdentity = entry.ToString(nameof(GroupIdentity)); + info.Precedence = entry.ToInt32(nameof(Precedence)); + + info.LocaleConfigPlace = entry.ToString(nameof(LocaleConfigPlace)); + info.HostConfigPlace = entry.ToString(nameof(HostConfigPlace)); + info.ThirdPartyNotices = entry.ToString(nameof(ThirdPartyNotices)); + + JsonObject? baselineJObject = entry.Get(nameof(ITemplateInfo.BaselineInfo)); + Dictionary baselineInfo = new Dictionary(); + if (baselineJObject != null) + { + foreach (var item in baselineJObject) + { + var defaultOverrides = item.Value?.ToStringDictionary(propertyName: nameof(IBaselineInfo.DefaultOverrides)); + if (defaultOverrides is null) + { + continue; + } + + IBaselineInfo baseline = new BaselineInfo(defaultOverrides, item.Value.ToString(nameof(IBaselineInfo.Description))); + baselineInfo.Add(item.Key, baseline); + } + info.BaselineInfo = baselineInfo; + } + + //read parameters +#pragma warning disable CS0618 // Type or member is obsolete + JsonArray? parametersArray = entry.Get(nameof(Parameters)); +#pragma warning restore CS0618 // Type or member is obsolete + if (parametersArray != null) + { + List templateParameters = new List(); + foreach (JsonNode? item in parametersArray) + { + if (item is JsonObject jObj) + { + templateParameters.Add(ParameterFromJObject(jObj)); + } + } + info.ParameterDefinitions = new ParameterDefinitionSet(templateParameters); + } + + //read tags + // tags are just "name": "description" + // e.g.: "language": "C#" + JsonObject? tagsObject = entry.Get(nameof(TagsCollection)); + if (tagsObject != null) + { + Dictionary tags = new Dictionary(); + foreach (var item in tagsObject) + { + tags.Add(item.Key, item.Value?.GetValue() ?? string.Empty); + } + info.TagsCollection = tags; + } + + info.HostData = entry.ToString(nameof(info.HostData)); + JsonArray? postActionsArray = entry.Get(nameof(info.PostActions)); + if (postActionsArray != null) + { + List postActions = new List(); + foreach (JsonNode? item in postActionsArray) + { + if (item != null && Guid.TryParse(item.GetValue(), out Guid id)) + { + postActions.Add(id); + } + } + info.PostActions = postActions; + } + + //read parameters + JsonArray? constraintsArray = entry.Get(nameof(info.Constraints)); + if (constraintsArray != null) + { + List constraints = new List(); + foreach (JsonNode? item in constraintsArray) + { + string? type = item.ToString(nameof(TemplateConstraintInfo.Type)); + if (string.IsNullOrWhiteSpace(type)) + { + throw new ArgumentException($"{nameof(entry)} has {nameof(info.Constraints)} property which item doesn't have {nameof(TemplateConstraintInfo.Type)}.", nameof(entry)); + } + constraints.Add(new TemplateConstraintInfo(type!, item.ToString(nameof(TemplateConstraintInfo.Args)))); + } + info.Constraints = constraints; + } + + return info; + } + + /// + /// Parses from . + /// + /// + private static ITemplateParameter ParameterFromJObject(JsonObject jObject) + { + string? name = jObject.ToString(nameof(ITemplateParameter.Name)); + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"{nameof(ITemplateParameter.Name)} property should not be null or whitespace", nameof(jObject)); + } + + string type = jObject.ToString(nameof(ITemplateParameter.Type)) ?? "parameter"; + string dataType = jObject.ToString(nameof(ITemplateParameter.DataType)) ?? "string"; + string? description = jObject.ToString(nameof(ITemplateParameter.Description)); + + string? defaultValue = jObject.ToString(nameof(ITemplateParameter.DefaultValue)); + string? defaultIfOptionWithoutValue = jObject.ToString(nameof(ITemplateParameter.DefaultIfOptionWithoutValue)); + string? displayName = jObject.ToString(nameof(ITemplateParameter.DisplayName)); + bool isName = jObject.ToBool(nameof(ITemplateParameter.IsName)); + bool allowMultipleValues = jObject.ToBool(nameof(ITemplateParameter.AllowMultipleValues)); + + Dictionary? choices = null; + + if (dataType.Equals("choice", StringComparison.OrdinalIgnoreCase)) + { + choices = new Dictionary(StringComparer.OrdinalIgnoreCase); + JsonObject? cdToken = jObject.Get(nameof(ITemplateParameter.Choices)); + if (cdToken != null) + { + foreach (var cdPair in cdToken) + { + choices.Add( + cdPair.Key, + new ParameterChoice( + cdPair.Value.ToString(nameof(ParameterChoice.DisplayName)), + cdPair.Value.ToString(nameof(ParameterChoice.Description)))); + } + } + } + + TemplateParameterPrecedence precedence = jObject.ToTemplateParameterPrecedence(nameof(ITemplateParameter.Precedence)); + + return new CacheTemplateParameter( + new TemplateParameter(name!, type, dataType) + { + DisplayName = displayName, + Precedence = precedence, + IsName = isName, + DefaultValue = defaultValue, + DefaultIfOptionWithoutValue = defaultIfOptionWithoutValue, + Description = description, + AllowMultipleValues = allowMultipleValues, + Choices = choices + }); + } + + /// + /// This class is overload on controlling JSON serialization for template parameters in cache. + /// Not all the members are required to be serialized. + /// + private class CacheTemplateParameter : ITemplateParameter + { + private readonly ITemplateParameter _parameter; + + internal CacheTemplateParameter(ITemplateParameter parameter) + { + _parameter = parameter; + } + + public string? Description => _parameter.Description; + + [JsonPropertyName("Name")] + public string Name => _parameter.Name; + + [JsonPropertyName("Precedence")] + public TemplateParameterPrecedence Precedence => _parameter.Precedence; + + [JsonPropertyName("Type")] + public string Type => _parameter.Type; + + [JsonPropertyName("IsName")] + public bool IsName => _parameter.IsName; + + [JsonPropertyName("DefaultValue")] + public string? DefaultValue => _parameter.DefaultValue; + + [JsonPropertyName("DefaultIfOptionWithoutValue")] + public string? DefaultIfOptionWithoutValue => _parameter.DefaultIfOptionWithoutValue; + + [JsonPropertyName("DataType")] + public string DataType => _parameter.DataType; + + [JsonPropertyName("Choices")] + public IReadOnlyDictionary? Choices => _parameter.Choices; + + [JsonPropertyName("DisplayName")] + public string? DisplayName => _parameter.DisplayName; + + [JsonPropertyName("AllowMultipleValues")] + public bool AllowMultipleValues => _parameter.AllowMultipleValues; + + [Obsolete] + [JsonIgnore] + public TemplateParameterPriority Priority => _parameter.Priority; + + [Obsolete] + [JsonIgnore] + public string? Documentation => _parameter.Documentation; + + public bool Equals(ITemplateParameter other) => _parameter.Equals(other); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateMatchInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateMatchInfo.cs new file mode 100644 index 000000000000..eac7ad95904e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplateMatchInfo.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Edge.Template; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + [Obsolete("This implementation is deprecated, use " + nameof(TemplateMatchInfo) + " instead")] + internal class TemplateMatchInfoEx : ITemplateMatchInfo + { + private readonly List _matchDisposition; + + private readonly List _dispositionOfDefaults; + + public TemplateMatchInfoEx(ITemplateInfo info, IReadOnlyList matchDispositions) + : this(info) + { + if (matchDispositions != null) + { + foreach (MatchInfo disposition in matchDispositions) + { + AddDisposition(disposition); + } + } + } + + public TemplateMatchInfoEx(ITemplateInfo info) + { + Info = info; + _matchDisposition = new List(); + _dispositionOfDefaults = new List(); + } + + public ITemplateInfo Info { get; } + + public IReadOnlyList MatchDisposition => _matchDisposition; + + // Stores match info relative to default settings. + // These don't have to match for the template to be a match, but they can be used to filter matches + // in appropriate situations. + // For example, matching or non-matching on the default language should only be used as a final disambiguation. + // It shouldn't unconditionally disqualify a match. + public IReadOnlyList DispositionOfDefaults => _dispositionOfDefaults.ToList(); + + public bool IsMatch => MatchDisposition.Count > 0 && MatchDisposition.All(x => x.Kind != MatchKind.Mismatch); + + public bool IsPartialMatch => MatchDisposition.Any(x => x.Kind != MatchKind.Mismatch); + + public void AddDisposition(MatchInfo newDisposition) + { + if (newDisposition.Location == MatchLocation.DefaultLanguage) + { + _dispositionOfDefaults.Add(newDisposition); + } + else + { + _matchDisposition.Add(newDisposition); + } + } + } + + internal class TemplateMatchInfo : Abstractions.TemplateFiltering.ITemplateMatchInfo + { + private readonly List _matchDisposition = new List(); + + internal TemplateMatchInfo(ITemplateInfo info, IReadOnlyList matchDispositions) + : this(info) + { + if (matchDispositions != null) + { + foreach (Abstractions.TemplateFiltering.MatchInfo disposition in matchDispositions) + { + AddMatchDisposition(disposition); + } + } + } + + internal TemplateMatchInfo(ITemplateInfo info) + { + Info = info ?? throw new ArgumentNullException(nameof(info)); + } + + public ITemplateInfo Info { get; } + + public IReadOnlyList MatchDisposition => _matchDisposition; + + public void AddMatchDisposition(Abstractions.TemplateFiltering.MatchInfo newDisposition) + { + _matchDisposition.Add(newDisposition); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplatePackageManager.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplatePackageManager.cs new file mode 100644 index 000000000000..6bb1999d0c99 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Settings/TemplatePackageManager.cs @@ -0,0 +1,387 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.TemplateFiltering; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; +using Microsoft.TemplateEngine.Edge.BuiltInManagedProvider; + +namespace Microsoft.TemplateEngine.Edge.Settings +{ + /// + /// Manages all s available to the host. + /// Use this class to get all template packages and templates installed. + /// + public class TemplatePackageManager : IDisposable + { + private readonly IEngineEnvironmentSettings _environmentSettings; + private readonly SettingsFilePaths _paths; + private readonly ILogger _logger; + private readonly Scanner _installScanner; + private volatile TemplateCache? _userTemplateCache; + private Dictionary>>? _cachedSources; + private volatile bool _disposed; + + /// + /// Creates the instance. + /// + /// template engine environment settings. + public TemplatePackageManager(IEngineEnvironmentSettings environmentSettings) + { + _environmentSettings = environmentSettings; + _logger = environmentSettings.Host.LoggerFactory.CreateLogger(); + _paths = new SettingsFilePaths(environmentSettings); + _installScanner = new Scanner(environmentSettings); + } + + /// + /// Triggered every time when the list of s changes, this is triggered by . + /// + public event Action? TemplatePackagesChanged; + + /// + /// Returns with specified name. + /// + /// Name from . + /// + /// For default built-in providers use method instead. + public IManagedTemplatePackageProvider GetManagedProvider(string name) + { + EnsureProvidersLoaded(); + return _cachedSources!.Keys.OfType().FirstOrDefault(p => p.Factory.DisplayName == name); + } + + /// + /// Returns with specified . + /// + /// from of . + /// + /// For default built-in providers use method instead. + public IManagedTemplatePackageProvider GetManagedProvider(Guid id) + { + EnsureProvidersLoaded(); + return _cachedSources!.Keys.OfType().FirstOrDefault(p => p.Factory.Id == id); + } + + /// + /// Same as but filters only packages. + /// + /// Useful when doesn't trigger event. + /// A cancellation token to cancel the asynchronous operation. + /// The list of . + public async Task> GetManagedTemplatePackagesAsync(bool force, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + EnsureProvidersLoaded(); + return (await GetTemplatePackagesAsync(force, cancellationToken).ConfigureAwait(false)).OfType().ToList(); + } + + /// + /// Returns combined list of s that all s and s return. + /// caches the responses from s, to get non-cached response should be set to true. + /// Note that specifying will only return responses from already loaded providers. To reload providers, instantiate new instance of the . + /// + /// Useful when doesn't trigger event. + /// A cancellation token to cancel the asynchronous operation. + /// The list of s. + public async Task> GetTemplatePackagesAsync(bool force, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + EnsureProvidersLoaded(); + if (force) + { + foreach (var provider in _cachedSources!.Keys.ToList()) + { + _cachedSources[provider] = Task.Run(() => provider.GetAllTemplatePackagesAsync(default)); + } + } + + var sources = new List(); + foreach (KeyValuePair>> source in _cachedSources.OrderBy((p) => (p.Key.Factory as IPrioritizedComponent)?.Priority ?? 0)) + { + try + { + sources.AddRange(await source.Value.ConfigureAwait(false)); + } + catch (Exception ex) + { + _logger.LogError(LocalizableStrings.TemplatePackageManager_Error_FailedToGetTemplatePackages, source.Key.Factory.DisplayName, ex.Message); + _logger.LogDebug("Details: {0}", ex); + } + } + + return sources; + } + + public void Dispose() + { + _disposed = true; + if (_cachedSources == null) + { + return; + } + foreach (var provider in _cachedSources.Keys.OfType()) + { + provider.Dispose(); + } + } + + /// + /// Returns built-in of specified . + /// + /// scope managed by built-in provider. + /// which manages packages of . + public IManagedTemplatePackageProvider GetBuiltInManagedProvider(InstallationScope scope = InstallationScope.Global) + { + switch (scope) + { + case InstallationScope.Global: + return GetManagedProvider(GlobalSettingsTemplatePackageProviderFactory.FactoryId); + default: + break; + } + return GetManagedProvider(GlobalSettingsTemplatePackageProviderFactory.FactoryId); + } + + /// + /// Gets all templates based on current settings. + /// + /// + /// This call is cached. And can be invalidated by . + /// + public async Task> GetTemplatesAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var userTemplateCache = await UpdateTemplateCacheAsync(false, cancellationToken).ConfigureAwait(false); + return userTemplateCache.TemplateInfo; + } + + /// + /// Gets the templates filtered using and . + /// + /// The criteria for to be included to result collection. + /// The list of filters to be applied to templates. + /// A cancellation token to cancel the asynchronous operation. + /// The filtered list of templates with match information. + /// + /// GetTemplatesAsync(WellKnownSearchFilters.MatchesAllCriteria, new [] { WellKnownSearchFilters.NameFilter("myname") } - returns the templates which name or short name contains "myname".
+ /// GetTemplatesAsync(TemplateListFilter.MatchesAtLeastOneCriteria, new [] { WellKnownSearchFilters.NameFilter("myname"), WellKnownSearchFilters.NameFilter("othername") }) - returns the templates which name or short name contains "myname" or "othername".
+ ///
+ public async Task> GetTemplatesAsync(Func matchFilter, IEnumerable> filters, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + IReadOnlyList templates = await GetTemplatesAsync(cancellationToken).ConfigureAwait(false); + //TemplateListFilter.GetTemplateMatchInfo code should be moved to this method eventually, when no longer needed. +#pragma warning disable CS0618 // Type or member is obsolete. + return TemplateListFilter.GetTemplateMatchInfo(templates, matchFilter, filters.ToArray()).ToList(); +#pragma warning restore CS0618 // Type or member is obsolete + } + + /// + /// Deletes templates cache and rebuilds it. + /// Useful if user suspects cache is corrupted and wants to rebuild it. + /// + public Task RebuildTemplateCacheAsync(CancellationToken token) + { + token.ThrowIfCancellationRequested(); + return UpdateTemplateCacheAsync(true, token); + } + + /// + /// Helper method that returns that contains . + /// + public async Task GetTemplatePackageAsync(ITemplateInfo template, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + IReadOnlyList templatePackages = await GetTemplatePackagesAsync(false, cancellationToken).ConfigureAwait(false); + return templatePackages.Single(s => s.MountPointUri == template.MountPointUri); + } + + /// + /// Returns all contained by . + /// + /// The template package to get template from. + /// A cancellation token to cancel the asynchronous operation. + /// The enumerator to templates of the . + public async Task> GetTemplatesAsync(ITemplatePackage templatePackage, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var allTemplates = await GetTemplatesAsync(cancellationToken).ConfigureAwait(false); + return allTemplates.Where(t => t.MountPointUri == templatePackage.MountPointUri); + } + + /// + /// Returns managed template package matching and containing templates . + /// + /// The template package identifier. + /// The template package version, if null package version is not checked. + /// A cancellation token to cancel the asynchronous operation. + /// The managed template package and the containing templates. + /// Throws an exception when package . + public async Task<(IManagedTemplatePackage? Package, IEnumerable? Templates)> GetManagedTemplatePackageAsync(string packageIdentifier, string? packageVersion = null, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var templatePackages = await GetManagedTemplatePackagesAsync(false, cancellationToken).ConfigureAwait(false); + var foundPackage = templatePackages + .FirstOrDefault(tp => + { + if (tp?.Identifier == packageIdentifier) + { + return string.IsNullOrWhiteSpace(packageVersion) || tp.Version == packageVersion; + } + return false; + }); + + if (foundPackage != null) + { + var templates = await GetTemplatesAsync(foundPackage, cancellationToken).ConfigureAwait(false); + + return (foundPackage, templates); + } + + throw new InvalidOperationException(string.Format(LocalizableStrings.TemplatePackageManager_Error_FailedToFindPackage, packageIdentifier)); + } + + private void EnsureProvidersLoaded() + { + if (_cachedSources != null) + { + return; + } + + _cachedSources = new Dictionary>>(); + var providers = _environmentSettings.Components.OfType().Select(f => f.CreateProvider(_environmentSettings)); + foreach (var provider in providers) + { + provider.TemplatePackagesChanged += () => + { + // Events from providers may be in-flight when Dispose is called. Guard against + // updating _cachedSources or raising TemplatePackagesChanged on a disposed instance. + if (_disposed) + { + return; + } + _cachedSources[provider] = provider.GetAllTemplatePackagesAsync(default); + TemplatePackagesChanged?.Invoke(); + }; + _cachedSources[provider] = Task.Run(() => provider.GetAllTemplatePackagesAsync(default)); + } + } + + private async Task UpdateTemplateCacheAsync(bool needsRebuild, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + // Kick off gathering template packages, so parsing cache can happen in parallel. + Task> getTemplatePackagesTask = GetTemplatePackagesAsync(needsRebuild, cancellationToken); + if (_userTemplateCache is not TemplateCache cache) + { + try + { + _userTemplateCache = cache = new TemplateCache(_environmentSettings.Host.FileSystem.ReadObject(_paths.TemplateCacheFile)); + } + catch (FileNotFoundException) + { + // Don't log this, it's expected, we just don't want to do File.Exists... + cache = new TemplateCache(null); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to load templatecache.json."); + cache = new TemplateCache(null); + } + } + + if (cache.Version == null) + { + // Null version means, parsing cache failed. + needsRebuild = true; + } + + if (!needsRebuild && cache.Version != TemplateInfo.CurrentVersion) + { + _logger.LogDebug($"Template cache file version is {cache.Version}, but template engine is {TemplateInfo.CurrentVersion}, rebuilding cache."); + needsRebuild = true; + } + + if (!needsRebuild && cache.Locale != CultureInfo.CurrentUICulture.Name) + { + _logger.LogDebug($"Template cache locale is {cache.Locale}, but CurrentUICulture is {CultureInfo.CurrentUICulture.Name}, rebuilding cache."); + needsRebuild = true; + } + + var allTemplatePackages = await getTemplatePackagesTask.ConfigureAwait(false); + + var mountPoints = new Dictionary(); + + foreach (var package in allTemplatePackages) + { + mountPoints[package.MountPointUri] = package.LastChangeTime; + + // We can stop comparing, but we need to keep looping to fill mountPoints + if (!needsRebuild) + { + if (cache.MountPointsInfo.TryGetValue(package.MountPointUri, out var cachedLastChangeTime)) + { + if (package.LastChangeTime > cachedLastChangeTime) + { + needsRebuild = true; + } + } + else + { + needsRebuild = true; + } + } + } + cancellationToken.ThrowIfCancellationRequested(); + + // Check that some mountpoint wasn't removed... + if (!needsRebuild && !mountPoints.Keys.OrderBy(mp => mp).SequenceEqual(cache.MountPointsInfo.Keys.OrderBy(mp => mp))) + { + needsRebuild = true; + } + + // Cool, looks like everything is up to date, exit + if (!needsRebuild) + { + return cache; + } + + var scanResults = new ScanResult?[allTemplatePackages.Count]; + await Task.WhenAll(Enumerable.Range(0, allTemplatePackages.Count).Select(async index => + { + try + { + var scanResult = await _installScanner.ScanAsync(allTemplatePackages[index].MountPointUri, cancellationToken).ConfigureAwait(false); + scanResults[index] = scanResult; + } + catch (Exception ex) + { + _logger.LogWarning(LocalizableStrings.TemplatePackageManager_Error_FailedToScan, allTemplatePackages[index].MountPointUri, ex.Message); + _logger.LogDebug($"Stack trace: {ex.StackTrace}"); + } + })).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + cache = new TemplateCache(allTemplatePackages, scanResults, mountPoints, _environmentSettings); + foreach (var scanResult in scanResults) + { + scanResult?.Dispose(); + } + _userTemplateCache = cache; + + try + { + _environmentSettings.Host.FileSystem.WriteObject(_paths.TemplateCacheFile, cache, TemplateCacheJsonSerializerContext.Default.TemplateCache); + } + catch (Exception ex) + { + _logger.LogWarning(LocalizableStrings.TemplatePackageManager_Error_FailedToStoreCache, ex.Message); + _logger.LogDebug($"Stack trace: {ex.StackTrace}"); + } + + return cache; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/CreationResultStatus.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/CreationResultStatus.cs new file mode 100644 index 000000000000..8717d8ab8a52 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/CreationResultStatus.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Template +{ + /// + /// Status of the template instantiation. + /// + public enum CreationResultStatus + { + /// + /// The template was instantiated successfully. + /// + Success = 0, + + /// + /// The template instantiation failed. + /// + CreateFailed = unchecked((int)0x80020009), + + /// + /// The template instantiation failed. + /// + TemplateIssueDetected = unchecked((int)0x8002000A), + + /// + /// The mandatory parameters for template are missing. + /// + MissingMandatoryParam = unchecked((int)0x8002000F), + + /// + /// The values passed for template parameters are invalid. + /// + InvalidParamValues = unchecked((int)0x80020005), + + [Obsolete("not used.")] + OperationNotSpecified = unchecked((int)0x8002000E), + + /// + /// The template is not found. + /// + NotFound = unchecked((int)0x80020006), + + /// + /// The operation is cancelled. + /// + Cancelled = unchecked((int)0x80004004), + + /// + /// The operation is cancelled due to destructive changes to existing files are detected. + /// + DestructiveChangesDetected = unchecked((int)0x8002000D), + + /// + /// The host supplied conditions evaluation results did not match internal evaluation. + /// + CondtionsEvaluationMismatch = unchecked((int)0x80029C83) + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/EvaluatedInputParameterData.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/EvaluatedInputParameterData.cs new file mode 100644 index 000000000000..2789ed35a233 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/EvaluatedInputParameterData.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Parameters; + +namespace Microsoft.TemplateEngine.Edge.Template; + +/// +/// Type representing input data into that are able to hold information about externally evaluated conditions +/// on template parameters. +/// +public class EvaluatedInputParameterData : InputParameterData +{ + /// + /// Constructor for type, that allows specification of results of external evaluation of conditions. + /// + /// + /// A string converted value of parameter or null for explicit unset. It's possible to indicate missing of parameter on input via argument. + /// + /// + /// + /// + /// InputDataState.Unset indicates a situation that parameter was not specified on input (distinct situation from explicit null). + /// This would normally be achieved by not passing the parameter at all into the , however then it would not be possible + /// to specify the results of conditions calculations. + /// + public EvaluatedInputParameterData( + ITemplateParameter parameterDefinition, + object? value, + DataSource dataSource, + bool? isEnabledConditionResult, + bool? isRequiredConditionResult, + InputDataState inputDataState = InputDataState.Set) + : base(parameterDefinition, value, dataSource, inputDataState) + { + IsEnabledConditionResult = isEnabledConditionResult; + IsRequiredConditionResult = isRequiredConditionResult; + } + + /// + /// Externally (by the host) supplied result of the IsEnabledCondition on the template parameter. + /// + public bool? IsEnabledConditionResult { get; } + + /// + /// Externally (by the host) supplied result of the IsRequiredCondition on the template parameter. + /// + public bool? IsRequiredConditionResult { get; } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/FilteredTemplateEqualityComparer.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/FilteredTemplateEqualityComparer.cs new file mode 100644 index 000000000000..9e779339ab8a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/FilteredTemplateEqualityComparer.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Template +{ + [Obsolete("IFilteredTemplateInfo is obsolete")] + public class FilteredTemplateEqualityComparer : IEqualityComparer + { + public static IEqualityComparer Default { get; } = new FilteredTemplateEqualityComparer(); + + public bool Equals(IFilteredTemplateInfo x, IFilteredTemplateInfo y) + { + return ReferenceEquals(x.Info, y.Info) || (x != null && y != null && x.Info != null && y.Info != null && string.Equals(x.Info.Identity, y.Info.Identity, StringComparison.Ordinal)); + } + + public int GetHashCode(IFilteredTemplateInfo obj) + { + return obj?.Info?.Identity?.GetHashCode() ?? 0; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/IFilteredTemplateInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/IFilteredTemplateInfo.cs new file mode 100644 index 000000000000..3d7c1b375d9c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/IFilteredTemplateInfo.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Edge.Template +{ + [Obsolete("Use ITemplateMatchInfo instead")] + public interface IFilteredTemplateInfo + { + ITemplateInfo Info { get; } + + IReadOnlyList MatchDisposition { get; } + + bool IsMatch { get; } + + bool IsPartialMatch { get; } + + bool HasParameterMismatch { get; } + + bool IsParameterMatch { get; } + + bool HasInvalidParameterValue { get; } + + bool HasAmbiguousParameterMatch { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/IParameterSetBuilder.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/IParameterSetBuilder.cs new file mode 100644 index 000000000000..05940b90b659 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/IParameterSetBuilder.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Parameters; + +namespace Microsoft.TemplateEngine.Edge.Template; + +internal interface IParameterSetBuilder : IParameterDefinitionSet +{ + void SetParameterValue(ITemplateParameter parameter, object value, DataSource dataSource); + + void SetParameterEvaluation(ITemplateParameter parameter, EvaluatedInputParameterData evaluatedParameterData); + + bool HasParameterValue(ITemplateParameter parameter); + + bool CheckIsParametersEvaluationCorrect(IGenerator generator, ILogger logger, bool throwOnError, out IReadOnlyList paramsWithInvalidEvaluations); + + InputDataSet Build(bool evaluateConditions, IGenerator generator, ILogger logger); + + void SetParameterDefault( + IGenerator generator, + ITemplateParameter parameter, + IEngineEnvironmentSettings environment, + bool useHostDefaults, + bool isRequired, + List paramsWithInvalidValues); +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/ITemplateCreationResult.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/ITemplateCreationResult.cs new file mode 100644 index 000000000000..af41b9bcfd8c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/ITemplateCreationResult.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Edge.Template +{ + /// + /// Represents result of template instantiation / dry run via . />. + /// + public interface ITemplateCreationResult + { + /// + /// Result of template dry run. + /// Template dry run always performed prior to instantiation. + /// + ICreationEffects? CreationEffects { get; } + + /// + /// Error message, null if operation is successful. + /// Should be localized. + /// + string? ErrorMessage { get; } + + /// + /// The output directory used for template instantiation. + /// + string? OutputBaseDirectory { get; } + + /// + /// The result of template instantiation. + /// Null in case of dry run. + /// + ICreationResult? CreationResult { get; } + + /// + /// Status of template instantiation. + /// + CreationResultStatus Status { get; } + + /// + /// Processed template name. + /// + string TemplateFullName { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/ITemplateMatchInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/ITemplateMatchInfo.cs new file mode 100644 index 000000000000..28c8af44d09c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/ITemplateMatchInfo.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Edge.Template +{ + // Replacement for IFilteredTemplateInfo + [Obsolete("moved to " + nameof(Abstractions.TemplateFiltering) + " namespace")] + public interface ITemplateMatchInfo + { + ITemplateInfo Info { get; } + + IReadOnlyList MatchDisposition { get; } + + IReadOnlyList DispositionOfDefaults { get; } + + bool IsMatch { get; } + + bool IsPartialMatch { get; } + + void AddDisposition(MatchInfo newDisposition); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputDataSet.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputDataSet.cs new file mode 100644 index 000000000000..54faa0615a29 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputDataSet.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Parameters; + +namespace Microsoft.TemplateEngine.Edge.Template; + +/// +/// Data model for passing data into the . +/// +public class InputDataSet : IReadOnlyDictionary +{ + private readonly IReadOnlyDictionary _parametersData; + + /// + /// Creates new instance of the type. + /// + /// + /// + public InputDataSet(IParameterDefinitionSet parameters, IReadOnlyList parameterData) + { + _parametersData = parameterData.ToDictionary(d => d.ParameterDefinition, d => d); + ParameterDefinitionSet = new ParameterDefinitionSet(parameters.AsReadonlyDictionary()); + } + + /// + /// Creates new instance of the type. + /// To be used to convert legacy parameters dictionaries into this data model. + /// + /// + public InputDataSet(ITemplateInfo templateInfo) + : this(templateInfo, (IReadOnlyDictionary?)null) + { } + + /// + /// Creates new instance of the type. + /// To be used to convert legacy parameters dictionaries into this data model. + /// + /// + /// + public InputDataSet(ITemplateInfo templateInfo, IReadOnlyDictionary inputParameters) + : this(templateInfo, inputParameters.ToDictionary(p => p.Key, p => (object?)p.Value)) + { } + + private InputDataSet(ITemplateInfo templateInfo, IReadOnlyDictionary? inputParameters) + { + _parametersData = templateInfo.ParameterDefinitions.ToDictionary(p => p, p => + { + object? value = null; + bool isSet = inputParameters != null && inputParameters.TryGetValue(p.Name, out value); + return new InputParameterData(p, value, isSet ? DataSource.User : DataSource.NoSource, isSet ? InputDataStateUtil.GetInputDataState(value) : InputDataState.Unset); + }); + ParameterDefinitionSet = new ParameterDefinitionSet(templateInfo.ParameterDefinitions); + } + + /// + /// Indicates whether template creator should ignore evaluation results that does not match validation evaluation. + /// Warning will be logged and external evaluation results will be used. + /// + public bool ContinueOnMismatchedConditionsEvaluation { get; init; } + + /// + /// Descriptors of the parameters. + /// + public ParameterDefinitionSet ParameterDefinitionSet { get; } + + /// + public IEnumerable Keys => _parametersData.Keys; + + /// + public IEnumerable Values => _parametersData.Values; + + /// + public int Count => _parametersData.Count; + + /// + public InputParameterData this[ITemplateParameter key] => _parametersData[key]; + + /// + public bool ContainsKey(ITemplateParameter key) => _parametersData.ContainsKey(key); + + /// + public IEnumerator> GetEnumerator() => _parametersData.GetEnumerator(); + + /// + public bool TryGetValue(ITemplateParameter key, out InputParameterData value) => _parametersData.TryGetValue(key, out value); + + /// + IEnumerator IEnumerable.GetEnumerator() => _parametersData.GetEnumerator(); +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputDataSetExtensions.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputDataSetExtensions.cs new file mode 100644 index 000000000000..37b2e7f2ea56 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputDataSetExtensions.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Template; + +internal static class InputDataSetExtensions +{ + public static IParameterSetData ToParameterSetData(this InputDataSet inputData) + { + return new ParameterSetData( + inputData.ParameterDefinitionSet, + inputData.Values.Select(d => new ParameterData(d.ParameterDefinition, d.Value, d.DataSource, !(d is EvaluatedInputParameterData ed && ed.IsEnabledConditionResult == false))) + .ToList()); + } + + [Obsolete("IParameterSet should not be used - it is replaced with IParameterSetData", false)] + public static InputDataSet ToInputDataSet(this IParameterSet parameterSet) + { + IParameterDefinitionSet parametersDefinition = new ParameterDefinitionSet(parameterSet.ParameterDefinitions); + IReadOnlyList data = parameterSet.ResolvedValues.Select(p => + new InputParameterData(p.Key, p.Value, DataSource.User, InputDataStateUtil.GetInputDataState(p.Value))) + .ToList(); + return new InputDataSet(parametersDefinition, data); + } + + public static void VerifyInputData(this InputDataSet inputData) + { + if (inputData.Values.OfType().Any()) + { + ErrorOutOnMismatchedConditionEvaluation( + inputData.Values.Where(p => + !(p is EvaluatedInputParameterData evaluated && evaluated.IsEnabledConditionResult != null) ^ + string.IsNullOrEmpty(p.ParameterDefinition.Precedence.IsEnabledCondition)).ToList()); + + ErrorOutOnMismatchedConditionEvaluation( + inputData.Values.Where(p => + !(p is EvaluatedInputParameterData evaluated && evaluated.IsRequiredConditionResult != null) ^ + string.IsNullOrEmpty(p.ParameterDefinition.Precedence.IsRequiredCondition)).ToList()); + } + + inputData.Values.ForEach(VerifyConditions); + inputData.Values.ForEach(VerifyInputState); + } + + public static bool HasConditions(this InputDataSet inputData) + { + return inputData.ParameterDefinitionSet.Any(p => + !string.IsNullOrEmpty(p.Precedence.IsRequiredCondition) || + !string.IsNullOrEmpty(p.Precedence.IsEnabledCondition)); + } + + public static EvaluatedPrecedence GetEvaluatedPrecedence(this InputParameterData inputParameterData) + { + EvaluatedInputParameterData? dt = inputParameterData as EvaluatedInputParameterData; + + return inputParameterData.ParameterDefinition.Precedence.PrecedenceDefinition switch + { + PrecedenceDefinition.Required => EvaluatedPrecedence.Required, + // Conditionally required state is only set if enabled condition is not present + PrecedenceDefinition.ConditionalyRequired => dt!.IsRequiredConditionResult!.Value ? EvaluatedPrecedence.Required : EvaluatedPrecedence.Optional, + PrecedenceDefinition.Optional => EvaluatedPrecedence.Optional, + PrecedenceDefinition.Implicit => EvaluatedPrecedence.Implicit, + PrecedenceDefinition.ConditionalyDisabled => !dt!.IsEnabledConditionResult!.Value + ? EvaluatedPrecedence.Disabled + : + (dt.IsRequiredConditionResult.HasValue && dt.IsRequiredConditionResult.Value) || dt.ParameterDefinition.Precedence.IsRequired + ? EvaluatedPrecedence.Required : EvaluatedPrecedence.Optional, + PrecedenceDefinition.Disabled => EvaluatedPrecedence.Disabled, + _ => throw new ArgumentOutOfRangeException("PrecedenceDefinition"), + }; + } + + private static void ErrorOutOnMismatchedConditionEvaluation(IReadOnlyList offendingParameters) + { + if (offendingParameters.Any()) + { + throw new Exception( + string.Format(LocalizableStrings.EvaluatedInputDataSet_Error_MismatchedConditions, string.Join(", ", offendingParameters))); + } + } + + private static void VerifyConditions(InputParameterData inputParameterData) + { + if ( + inputParameterData is EvaluatedInputParameterData evaluatedInputParameterData + && + ( + string.IsNullOrEmpty(inputParameterData.ParameterDefinition.Precedence.IsEnabledCondition) ^ !evaluatedInputParameterData.IsEnabledConditionResult.HasValue + || + string.IsNullOrEmpty(inputParameterData.ParameterDefinition.Precedence.IsRequiredCondition) ^ !evaluatedInputParameterData.IsRequiredConditionResult.HasValue)) + { + throw new ArgumentException(string.Format(LocalizableStrings.EvaluatedInputParameterData_Error_ConditionsInvalid, inputParameterData.ParameterDefinition.Name)); + } + } + + private static void VerifyInputState(InputParameterData inputParameterData) + { + if (inputParameterData.InputDataState == InputDataState.Unset) + { + if (inputParameterData.Value != null) + { + throw new ArgumentException( + string.Format( + "It's disallowed to pass an input data value (even empty string) when it's tagged as InputDataState.Unset. Param: {0}", + inputParameterData.ParameterDefinition.Name)); + } + } + else if (InputDataStateUtil.GetInputDataState(inputParameterData.Value) != inputParameterData.InputDataState) + { + throw new ArgumentException( + string.Format( + "Param {0} has disallowed combination of input data value ({1}) and InputDataState ({2}).", + inputParameterData.ParameterDefinition.Name, + inputParameterData.Value, + inputParameterData.InputDataState)); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputDataState.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputDataState.cs new file mode 100644 index 000000000000..988d1f8a31af --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputDataState.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Template; + +/// +/// Enumeration indicating how should the engine treat incoming value (as ignored, explicitly unset or standard value). +/// +public enum InputDataState +{ + /// + /// Parameter is represented in input with nonempty value. + /// + Set, + + /// + /// Parameter is not represented in input. + /// + Unset, + + /// + /// Parameter is represented in input data with a null or empty value - this can e.g. indicate multichoice with no option selected. + /// In CLI this is represented by option with explicit null string ('dotnet new mytemplate --myoptionA ""'). + /// In TemplateCreator this is represented by explicit null value. + /// + ExplicitEmpty +} + +public static class InputDataStateUtil +{ + /// + /// Tags the input value with based on it's definition. + /// + /// + /// + public static InputDataState GetInputDataState(object? value) + { + // This is not and extension method as it's reach would be too broad (applicable to object) + + return value == null || (value is string str && string.IsNullOrEmpty(str)) + ? InputDataState.ExplicitEmpty + : InputDataState.Set; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputParameterData.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputParameterData.cs new file mode 100644 index 000000000000..a338d3a141d4 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/InputParameterData.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Parameters; + +namespace Microsoft.TemplateEngine.Edge.Template; + +/// +/// Type representing input data into . +/// +public class InputParameterData +{ + /// + /// Creates new instance of the type. + /// + /// + /// + /// + /// + public InputParameterData( + ITemplateParameter parameterDefinition, + object? value, + DataSource dataSource = DataSource.User, + InputDataState inputDataState = InputDataState.Set) + { + ParameterDefinition = parameterDefinition; + Value = value; + DataSource = dataSource; + InputDataState = inputDataState; + } + + /// + /// Descriptor of the parameter. + /// + public ITemplateParameter ParameterDefinition { get; } + + /// + /// Value of the parameter. + /// + public object? Value { get; } + + /// + /// Source of the parameter value. If supplied by the host - leave the default value of . + /// + public DataSource DataSource { get; } + + /// + /// Input data state - indicates how the actual value should be treated (ignored, regarded as explicitly unset value, etc.). + /// + public InputDataState InputDataState { get; } + + public override string ToString() => $"{ParameterDefinition}: {Value?.ToString() ?? ""}"; +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/MatchInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/MatchInfo.cs new file mode 100644 index 000000000000..4b6837e739f7 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/MatchInfo.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace Microsoft.TemplateEngine.Edge.Template +{ + [Obsolete("This struct is deprecated, use " + nameof(Abstractions.TemplateFiltering.MatchInfo) + " instead")] + [StructLayout(LayoutKind.Sequential)] + public struct MatchInfo + { + public MatchLocation Location; + + public MatchKind Kind; + + /// + /// stores canonical parameter name. + /// + public string InputParameterName; + + /// + /// stores parameter value. + /// + public string ParameterValue; + + /// + /// stores the exception message if there is an args parse error. + /// + public string AdditionalInformation; + + /// + /// stores the option for parameter as used in the host + /// for example dotnet CLI offers two options for Framework parameter: -f and --framework + /// if the user uses -f when executing command, contains -f. + /// + public string InputParameterFormat; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/MatchKind.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/MatchKind.cs new file mode 100644 index 000000000000..943638c83ed3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/MatchKind.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Template +{ + public enum MatchKind + { + Unspecified, // TODO: rename to "ParseError". Will have to be done for a major version release. + Exact, + Partial, // only used for name & name-type field matches + AmbiguousParameterValue, // when the value is a starts with match on more than one choice value. + InvalidParameterName, + InvalidParameterValue, + Mismatch, // only used for template language + SingleStartsWith + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/MatchLocation.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/MatchLocation.cs new file mode 100644 index 000000000000..c649f4aa7951 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/MatchLocation.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Template +{ + [Obsolete("use" + nameof(Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo.Name) + " instead")] + public enum MatchLocation + { + Unspecified, + Name, + ShortName, + Alias, // never used, alias expansion occurs prior to matching + Classification, + Language, // this is meant for the input language + Context, + OtherParameter, + Baseline, + DefaultLanguage, + Author + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/OrdinalIgnoreCaseMatchInfoComparer.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/OrdinalIgnoreCaseMatchInfoComparer.cs new file mode 100644 index 000000000000..9d7354303b78 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/OrdinalIgnoreCaseMatchInfoComparer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Template +{ + [Obsolete("This class is deprecated")] + public class OrdinalIgnoreCaseMatchInfoComparer : IEqualityComparer + { + public bool Equals(MatchInfo x, MatchInfo y) + { + return x.Kind == y.Kind + && x.Location == y.Location + && string.Equals(x.InputParameterName, y.InputParameterName, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.ParameterValue, y.ParameterValue, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(MatchInfo obj) + { + return new { a = obj.InputParameterName?.ToLowerInvariant(), b = obj.ParameterValue?.ToLowerInvariant(), obj.Kind, obj.Location }.GetHashCode(); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/ParameterSetBuilder.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/ParameterSetBuilder.cs new file mode 100644 index 000000000000..3d9d2f2c50d7 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/ParameterSetBuilder.cs @@ -0,0 +1,415 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Template +{ + internal class ParameterSetBuilder : ParameterDefinitionSet, IParameterSetBuilder, +#pragma warning disable CS0618 // Type or member is obsolete + IParameterSet +#pragma warning restore CS0618 // Type or member is obsolete + { + private readonly Dictionary _resolvedValues; + private InputDataSet? _result; + + internal ParameterSetBuilder(IReadOnlyDictionary parameters) : base(parameters) + { + _resolvedValues = parameters.ToDictionary(p => p.Value, p => new EvalData(p.Value)); + } + + internal ParameterSetBuilder(IParameterDefinitionSet parameters) : this(parameters.AsReadonlyDictionary()) + { } + + public static IParameterSetBuilder CreateWithDefaults(IGenerator generator, IParameterDefinitionSet parametersDefinition, IEngineEnvironmentSettings environment, string? name = null) + { + var result = CreateWithDefaults(generator, parametersDefinition, name, environment, out IReadOnlyList errors); + if (errors.Any()) + { + throw new Exception("ParameterDefinitionSet with errors encountered: " + errors.ToCsvString()); + } + + return result; + } + + public static IParameterSetBuilder CreateWithDefaults(IGenerator generator, IParameterDefinitionSet parametersDefinition, string? name, IEngineEnvironmentSettings environment, out IReadOnlyList paramsWithInvalidValues) + { + IParameterSetBuilder templateParams = new ParameterSetBuilder(parametersDefinition); + List paramsWithInvalidValuesList = new List(); + + foreach (ITemplateParameter param in templateParams) + { + if (param.IsName) + { + if (name != null) + { + templateParams.SetParameterValue(param, name, DataSource.NameParameter); + } + } + else + { + templateParams.SetParameterDefault( + generator, + param, + environment, + true, + param.Precedence.CanBeRequired, + paramsWithInvalidValuesList); + } + } + + paramsWithInvalidValues = paramsWithInvalidValuesList; + return templateParams; + } + + #region MyRegion IParameterSet members + //We want all IParameterSet members in single region - hence not following recommended ordering +#pragma warning disable SA1201 // Elements should appear in the correct order + IEnumerable IParameterSet.ParameterDefinitions => this; +#pragma warning restore SA1201 // Elements should appear in the correct order + + IDictionary IParameterSet.ResolvedValues => + _resolvedValues + .Where(p => p.Value.Value != null) + .ToDictionary(k => k.Key, k => k.Value.Value); + + bool IParameterSet.TryGetParameterDefinition(string name, out ITemplateParameter parameter) => TryGetValue(name, out parameter); + + #endregion /MyRegion IParameterSet members + + public void SetParameterValue(ITemplateParameter parameter, object value, DataSource dataSource) + { + _resolvedValues[parameter].SetValue(value, dataSource); + _result = null; + } + + public void SetParameterEvaluation(ITemplateParameter parameter, EvaluatedInputParameterData evaluatedParameterData) + { + if (!_resolvedValues.ContainsKey(parameter)) + { + return; + } + + var old = _resolvedValues[parameter]; + _resolvedValues[parameter] = new EvalData(evaluatedParameterData); + if (old.InputDataState != InputDataState.Unset) + { + _resolvedValues[parameter].SetValue(old.Value, old.DataSource); + } + + _result = null; + } + + public bool HasParameterValue(ITemplateParameter parameter) => _resolvedValues[parameter].InputDataState != InputDataState.Unset; + + public bool CheckIsParametersEvaluationCorrect(IGenerator generator, ILogger logger, bool throwOnError, out IReadOnlyList paramsWithInvalidEvaluations) + { + List evaluatedParameters = _resolvedValues.Values.ToList(); + List clonedParameters = evaluatedParameters.Select(v => v.Clone()).ToList(); + List invalidParams = new List(); + try + { + RunDatasetEvaluation(clonedParameters, generator, logger); + } + catch (Exception e) + { + logger.LogInformation(e, "Cross-check evaluation of host provided condition evaluations failed."); + if (throwOnError) + { + throw; + } + } + + foreach (var pair in evaluatedParameters.Zip(clonedParameters, (a, b) => (a, b))) + { + if (pair.a.IsEnabledConditionResult != pair.b.IsEnabledConditionResult || + pair.a.IsRequiredConditionResult != pair.b.IsRequiredConditionResult) + { + invalidParams.Add(pair.a.ParameterDefinition.Name); + } + } + + paramsWithInvalidEvaluations = invalidParams; + return invalidParams.Count == 0; + } + + public InputDataSet Build(bool evaluateConditions, IGenerator generator, ILogger logger) + { + if (_result == null) + { + if (evaluateConditions) + { + this.EvaluateConditionalParameters(generator, logger); + } + + _result = new InputDataSet( + this, + _resolvedValues.Select(p => p.Value.ToParameterData()).ToList()); + } + + return _result!; + } + + public void SetParameterDefault(IGenerator generator, ITemplateParameter parameter, IEngineEnvironmentSettings environment, bool useHostDefaults, bool isRequired, List paramsWithInvalidValues) + { + ITemplateEngineHost host = environment.Host; + if (useHostDefaults && host.TryGetHostParamDefault(parameter.Name, out string? hostParamValue) && hostParamValue != null) + { + object? resolvedValue = generator.ConvertParameterValueToType(environment, parameter, hostParamValue, out bool valueResolutionError); + if (!valueResolutionError) + { + if (resolvedValue is null) + { + throw new InvalidOperationException($"{nameof(resolvedValue)} cannot be null when {nameof(valueResolutionError)} is 'false'."); + } + this.SetParameterValue(parameter, resolvedValue, DataSource.HostDefault); + } + else + { + paramsWithInvalidValues.Add(parameter.Name); + } + } + // This for newly optional that does not have value set + else if (!isRequired && parameter.DefaultValue != null) + { + object? resolvedValue = generator.ConvertParameterValueToType(environment, parameter, parameter.DefaultValue, out bool valueResolutionError); + if (!valueResolutionError) + { + if (resolvedValue is null) + { + throw new InvalidOperationException($"{nameof(resolvedValue)} cannot be null when {nameof(valueResolutionError)} is 'false'."); + } + this.SetParameterValue(parameter, resolvedValue, DataSource.Default); + } + else + { + paramsWithInvalidValues.Add(parameter.Name); + } + } + } + + private static void RunDatasetEvaluation(List evaluatedParameters, IGenerator generator, ILogger logger) + { + Dictionary variables = + evaluatedParameters + .Where(p => p.Value != null) + .ToDictionary(p => p.ParameterDefinition.Name, p => p); + + IDictionary variableCollection = + variables.ToDictionary(p => p.Key, p => p.Value.Value!); + + EvaluateEnablementConditions(generator, evaluatedParameters, variableCollection, variables, logger); + EvaluateRequirementCondition(generator, evaluatedParameters, variableCollection, logger); + } + + private static void EvaluateEnablementConditions( + IGenerator generator, + IReadOnlyList parameters, + IDictionary variableCollection, + Dictionary variables, + ILogger logger) + { + Dictionary> parametersDependencies = new(); + + // First parameters traversal. + // - evaluate all IsEnabledCondition - and get the dependencies between the parameters during doing so + foreach (EvalData parameter in parameters) + { + if (!string.IsNullOrEmpty(parameter.ParameterDefinition.Precedence.IsEnabledCondition)) + { + HashSet referencedVariablesKeys = new HashSet(); + // Do not remove from the variable collection though - we want to capture all dependencies between parameters in the first traversal. + // Those will be bulk removed before second traversal (traversing only the required dependencies). + parameter.IsEnabledConditionResult = EvaluateParameterCondition( + parameter.ParameterDefinition.Precedence.IsEnabledCondition!, + parameter.ParameterDefinition.Name, + "IsEnabled", + generator, + variableCollection, + referencedVariablesKeys, + logger); + + if (referencedVariablesKeys.Any()) + { + parametersDependencies[parameter] = new HashSet(referencedVariablesKeys.Select(idx => variables[idx])); + } + } + } + + // No dependencies between parameters detected - no need to process further the second evaluation + if (parametersDependencies.Count == 0) + { + return; + } + + DirectedGraph parametersDependenciesGraph = new(parametersDependencies); + // Get the transitive closure of parameters that need to be recalculated, based on the knowledge of params that + IReadOnlyList disabledParameters = parameters.Where(p => p.IsEnabledConditionResult.HasValue && !p.IsEnabledConditionResult.Value).ToList(); + DirectedGraph parametersToRecalculate = + parametersDependenciesGraph.GetSubGraphDependentOnVertices(disabledParameters, includeSeedVertices: false); + + // Second traversal - for transitive dependencies of parameters that need to be disabled + if (parametersToRecalculate.TryGetTopologicalSort(out IReadOnlyList orderedParameters)) + { + disabledParameters.ForEach(p => variableCollection.Remove(p.ParameterDefinition.Name)); + + if (parametersDependenciesGraph.HasCycle(out var cycle)) + { + logger.LogWarning(LocalizableStrings.ConditionEvaluation_Warning_CyclicDependency, cycle.Select(p => p.ParameterDefinition.Name).ToCsvString()); + } + + foreach (EvalData parameter in orderedParameters) + { + bool isEnabled = EvaluateParameterCondition( + parameter.ParameterDefinition.Precedence.IsEnabledCondition!, + parameter.ParameterDefinition.Name, + "IsEnabled", + generator, + variableCollection, + null, + logger); + parameter.IsEnabledConditionResult = isEnabled; + if (!isEnabled) + { + variableCollection.Remove(parameter.ParameterDefinition.Name); + } + } + } + else if (parametersToRecalculate.HasCycle(out var cycle)) + { + throw new TemplateAuthoringException( + string.Format( + LocalizableStrings.ConditionEvaluation_Error_CyclicDependency, + cycle.Select(p => p.ParameterDefinition.Name).ToCsvString()), + "Conditional ParameterDefinitionSet"); + } + else + { + throw new Exception(LocalizableStrings.ConditionEvaluation_Error_TopologicalSort); + } + } + + private static bool EvaluateParameterCondition( + string condition, + string parameterName, + string conditionName, + IGenerator generator, + IDictionary variableCollection, + HashSet? referencedVariablesKeys, + ILogger logger) + { + if (!generator.TryEvaluateFromString(logger, condition, variableCollection, out bool result, out string evaluationError, referencedVariablesKeys)) + { + throw new TemplateAuthoringException( + string.Format( + LocalizableStrings.ConditionEvaluation_Error_MismatchedCondition, + conditionName, + parameterName, + condition, + evaluationError), + parameterName); + } + + return result; + } + + private static void EvaluateRequirementCondition( + IGenerator generator, + IReadOnlyList parameters, + IDictionary variableCollection, + ILogger logger) + { + foreach (EvalData parameter in parameters) + { + if (!string.IsNullOrEmpty(parameter.ParameterDefinition.Precedence.IsRequiredCondition)) + { + parameter.IsRequiredConditionResult = EvaluateParameterCondition( + parameter.ParameterDefinition.Precedence.IsRequiredCondition!, + parameter.ParameterDefinition.Name, + "IsRequired", + generator, + variableCollection, + null, + logger); + } + } + } + + private void EvaluateConditionalParameters(IGenerator generator, ILogger logger) + { + List evaluatedParameters = _resolvedValues.Values.ToList(); + RunDatasetEvaluation(evaluatedParameters, generator, logger); + } + + private class EvalData + { + public EvalData(ITemplateParameter parameterDefinition) + { + ParameterDefinition = parameterDefinition; + } + + public EvalData(EvaluatedInputParameterData other) + : this(other.ParameterDefinition, other.Value, other.IsEnabledConditionResult, other.IsRequiredConditionResult) + { + this.DataSource = DataSource.NoSource; + } + + private EvalData( + ITemplateParameter parameterDefinition, + object? value, + bool? isEnabledConditionResult, + bool? isRequiredConditionResult) + { + ParameterDefinition = parameterDefinition; + Value = value; + IsEnabledConditionResult = isEnabledConditionResult; + IsRequiredConditionResult = isRequiredConditionResult; + } + + public ITemplateParameter ParameterDefinition { get; } + + public InputDataState InputDataState { get; private set; } = InputDataState.Unset; + + public bool? IsEnabledConditionResult { get; set; } + + public bool? IsRequiredConditionResult { get; set; } + + public DataSource DataSource { get; private set; } = DataSource.NoSource; + + public object? Value { get; private set; } + + public void SetValue(object? value, DataSource source) + { + Value = value; + DataSource = source; + InputDataState = InputDataStateUtil.GetInputDataState(value); + } + + public override string ToString() => $"{ParameterDefinition}: {Value?.ToString() ?? ""}"; + + public EvaluatedInputParameterData ToParameterData() + { + return new EvaluatedInputParameterData( + this.ParameterDefinition, + this.Value, + DataSource, + this.IsEnabledConditionResult, + this.IsRequiredConditionResult, + InputDataState); + } + + public EvalData Clone() + { + var ds = DataSource; + return new EvalData(ParameterDefinition, Value, IsEnabledConditionResult, IsRequiredConditionResult) + { + DataSource = ds + }; + } + + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateCreationResult.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateCreationResult.cs new file mode 100644 index 000000000000..8280c1e3327f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateCreationResult.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Edge.Template +{ + internal class TemplateCreationResult : ITemplateCreationResult + { + /// + /// Creates . + /// + /// status of template creation. + /// template name. + /// localized error message. Should be set when is not . + /// + /// results of template creation. + /// output directory. + /// results of template dry run. + /// when is null or empty and is not . + internal TemplateCreationResult( + CreationResultStatus status, + string templateName, + string? localizedErrorMessage = null, + ICreationResult? creationOutputs = null, + string? outputBaseDir = null, + ICreationEffects? creationEffects = null) + { + if (string.IsNullOrWhiteSpace(templateName)) + { + throw new ArgumentException($"'{nameof(templateName)}' cannot be null or whitespace.", nameof(templateName)); + } + + Status = status; + ErrorMessage = localizedErrorMessage; + if (string.IsNullOrWhiteSpace(ErrorMessage) && status != CreationResultStatus.Success) + { + throw new ArgumentException($"{nameof(localizedErrorMessage)} cannot be null or empty when {nameof(status)} is not {CreationResultStatus.Success}", nameof(localizedErrorMessage)); + } + + TemplateFullName = templateName; + CreationResult = creationOutputs; + OutputBaseDirectory = outputBaseDir; + CreationEffects = creationEffects; + } + + public string? ErrorMessage { get; } + + public CreationResultStatus Status { get; } + + public string TemplateFullName { get; } + + public ICreationResult? CreationResult { get; } + + public string? OutputBaseDirectory { get; } + + public ICreationEffects? CreationEffects { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateCreator.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateCreator.cs new file mode 100644 index 000000000000..005e8edcc79f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateCreator.cs @@ -0,0 +1,555 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Template +{ + /// + /// The class instantiates and dry run given template with given parameters. + /// + public class TemplateCreator + { + private readonly IEngineEnvironmentSettings _environmentSettings; + private readonly ILogger _logger; + + public TemplateCreator(IEngineEnvironmentSettings environmentSettings) + { + _environmentSettings = environmentSettings; + _logger = _environmentSettings.Host.LoggerFactory.CreateLogger(); + } + + /// + /// Instantiates or dry runs the template. + /// + /// The template to run. + /// The name to use. Will be also used as in case it is empty and for is set to true. + /// Fallback name in case is null. + /// The output directory for instantiate template to. + /// The input parameters for the template. + /// If true, create the template even it overwrites existing files. + /// baseline configuration to use. + /// If true, only dry run will be performed - no actual actions will be done. + /// + /// +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public Task InstantiateAsync( + ITemplateInfo templateInfo, + string? name, + string? fallbackName, + string? outputPath, + IReadOnlyDictionary inputParameters, + bool forceCreation = false, + string? baselineName = null, + bool dryRun = false, + CancellationToken cancellationToken = default) +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + { + return InstantiateAsync( + templateInfo, + name, + fallbackName, + outputPath, + new InputDataSet(templateInfo, inputParameters), + forceCreation, + baselineName, + dryRun, + cancellationToken); + } + + /// + /// Instantiates or dry runs the template. + /// + /// The template to run. + /// The name to use. Will be also used as in case it is empty and for is set to true. + /// Fallback name in case is null. + /// The output directory for instantiate template to. + /// The input parameters for the template. + /// If true, create the template even it overwrites existing files. + /// baseline configuration to use. + /// If true, only dry run will be performed - no actual actions will be done. + /// + /// + public async Task InstantiateAsync( + ITemplateInfo templateInfo, + string? name, + string? fallbackName, + string? outputPath, + InputDataSet? inputParameters, + bool forceCreation = false, + string? baselineName = null, + bool dryRun = false, + CancellationToken cancellationToken = default) + { + _ = templateInfo ?? throw new ArgumentNullException(nameof(templateInfo)); + inputParameters?.VerifyInputData(); + inputParameters ??= new InputDataSet(templateInfo); + cancellationToken.ThrowIfCancellationRequested(); + + using ITemplate? template = await LoadTemplateAsync(templateInfo, baselineName, cancellationToken).ConfigureAwait(false); + + if (template == null) + { + return new TemplateCreationResult(CreationResultStatus.NotFound, templateInfo.Name, LocalizableStrings.TemplateCreator_TemplateCreationResult_Error_CouldNotLoadTemplate); + } + + ValidationUtils.LogValidationResults(_environmentSettings.Host.Logger, new[] { template }); + + if (!template.IsValid) + { + return new TemplateCreationResult( + CreationResultStatus.TemplateIssueDetected, template.Name, LocalizableStrings.TemplateCreator_TemplateCreationResult_Error_InvalidTemplate); + } + + string? realName = null; + if (!string.IsNullOrEmpty(name)) + { + realName = name; + } + else if (templateInfo.PreferDefaultName) + { + if (string.IsNullOrEmpty(templateInfo.DefaultName)) + { + return new TemplateCreationResult( + CreationResultStatus.TemplateIssueDetected, template.Name, LocalizableStrings.TemplateCreator_TemplateCreationResult_Error_NoDefaultName); + } + realName = templateInfo.DefaultName; + } + else + { + if (string.IsNullOrEmpty(fallbackName)) + { + if (!string.IsNullOrEmpty(templateInfo.DefaultName)) + { + realName = templateInfo.DefaultName; + } + } + else + { + realName = fallbackName; + } + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (string.IsNullOrWhiteSpace(realName)) + { + return new TemplateCreationResult(CreationResultStatus.MissingMandatoryParam, template.Name, "--name"); + } + if (template.IsNameAgreementWithFolderPreferred && string.IsNullOrEmpty(outputPath)) + { + outputPath = name; + } + string targetDir = !string.IsNullOrWhiteSpace(outputPath) ? outputPath! : _environmentSettings.Host.FileSystem.GetCurrentDirectory(); + Timing contentGeneratorBlock = Timing.Over(_logger, "Template content generation"); + try + { + ICreationResult? creationResult = null; + if (!dryRun) + { + _environmentSettings.Host.FileSystem.CreateDirectory(targetDir); + } + + // setup separate sets of parameters to be used for GetCreationEffects() and by CreateAsync(). + if (!TryCreateParameterSet(template, realName!, inputParameters, out IParameterSetData? effectParams, out TemplateCreationResult? resultIfParameterCreationFailed)) + { + //resultIfParameterCreationFailed is not null when TryCreateParameterSet is false + return resultIfParameterCreationFailed!; + } + if (effectParams is null) + { + throw new InvalidOperationException($"{nameof(effectParams)} cannot be null when {nameof(TryCreateParameterSet)} returns 'true'"); + } + + ICreationEffects creationEffects = await template.Generator.GetCreationEffectsAsync( + _environmentSettings, + template, + effectParams, + targetDir, + cancellationToken).ConfigureAwait(false); + IReadOnlyList changes = creationEffects.FileChanges; + IReadOnlyList destructiveChanges = changes.Where(x => x.ChangeKind != ChangeKind.Create).ToList(); + + if (!forceCreation && destructiveChanges.Count > 0) + { +#pragma warning disable CS0618 // Type or member is obsolete + if (!_environmentSettings.Host.OnPotentiallyDestructiveChangesDetected(changes, destructiveChanges)) +#pragma warning restore CS0618 // Type or member is obsolete + { + return new TemplateCreationResult( + CreationResultStatus.DestructiveChangesDetected, + template.Name, + LocalizableStrings.TemplateCreator_TemplateCreationResult_Error_DestructiveChanges, + null, + null, + creationEffects); + } + } + + if (!TryCreateParameterSet(template, realName!, inputParameters, out IParameterSetData? creationParams, out resultIfParameterCreationFailed)) + { + return resultIfParameterCreationFailed!; + } + + if (creationParams is null) + { + throw new InvalidOperationException($"{nameof(creationParams)} cannot be null when {nameof(TryCreateParameterSet)} returns 'true'"); + } + + if (!dryRun) + { + creationResult = await template.Generator.CreateAsync( + _environmentSettings, + template, + creationParams, + targetDir, + cancellationToken).ConfigureAwait(false); + } + return new TemplateCreationResult( + status: CreationResultStatus.Success, + templateName: template.Name, + creationOutputs: creationResult, + outputBaseDir: targetDir, + creationEffects: creationEffects); + } + catch (Exception cx) + { + string message = string.Join(Environment.NewLine, ExceptionMessages(cx)); + return new TemplateCreationResult( + status: cx is TemplateAuthoringException ? CreationResultStatus.TemplateIssueDetected : CreationResultStatus.CreateFailed, + templateName: template.Name, + localizedErrorMessage: string.Format(LocalizableStrings.TemplateCreator_TemplateCreationResult_Error_CreationFailed, message), + outputBaseDir: targetDir); + } + finally + { + contentGeneratorBlock.Dispose(); + } + } + + /// + /// Fully load template from . + /// usually comes from cache and is missing some information. + /// Calling this methods returns full information about template needed to instantiate template. + /// + /// Information about template. + /// Defines which baseline of template to load. + /// The cancellation token. + /// Fully loaded template or null if it fails to load template. + internal Task LoadTemplateAsync(ITemplateInfo info, string? baselineName, CancellationToken cancellationToken) + { + if (!_environmentSettings.Components.TryGetComponent(info.GeneratorId, out IGenerator? generator)) + { + return Task.FromResult((ITemplate?)null); + } + using (Timing.Over(_environmentSettings.Host.Logger, $"Template from config {info.MountPointUri}{info.ConfigPlace}")) + { + return generator!.LoadTemplateAsync(_environmentSettings, ToITemplateLocator(info), baselineName, cancellationToken); + } + } + + private static IEnumerable ExceptionMessages(Exception? e) + { + while (e != null) + { + yield return e.Message; + e = e.InnerException; + } + } + + private static IExtendedTemplateLocator ToITemplateLocator(ITemplateInfo templateInfo) + { + return new TemplateLocator(templateInfo.GeneratorId, templateInfo.MountPointUri, templateInfo.ConfigPlace) + { + LocaleConfigPlace = templateInfo.LocaleConfigPlace, + HostConfigPlace = templateInfo.HostConfigPlace, + }; + } + + private bool AnyParametersWithInvalidDefaultsUnresolved(IReadOnlyList defaultParamsWithInvalidValues, InputDataSet inputParameters, out IReadOnlyList invalidDefaultParameters) + { + invalidDefaultParameters = defaultParamsWithInvalidValues.Where(x => !inputParameters.ParameterDefinitionSet.ContainsKey(x)).ToList(); + return invalidDefaultParameters.Count > 0; + } + + private IParameterSetBuilder SetupDefaultParamValuesFromTemplateAndHostInternal(ITemplate template, string realName, out IReadOnlyList paramsWithInvalidValues) + { + return ParameterSetBuilder.CreateWithDefaults(template.Generator, template.ParameterDefinitions, realName, _environmentSettings, out paramsWithInvalidValues); + } + + private void ResolveUserParameters(ITemplate template, IParameterSetBuilder templateParamsBuilder, InputDataSet inputParameters, out IReadOnlyList paramsWithInvalidValues) + { + List tmpParamsWithInvalidValues = new List(); + paramsWithInvalidValues = tmpParamsWithInvalidValues; + + foreach (InputParameterData inputParam in inputParameters + .Where(p => p.Value.InputDataState != InputDataState.Unset && + !(p.Value is EvaluatedInputParameterData + { + IsEnabledConditionResult: false + })) + .Select(p => p.Value)) + { + if (templateParamsBuilder.TryGetValue(inputParam.ParameterDefinition.Name, out ITemplateParameter paramFromTemplate)) + { + if (inputParam.Value == null) + { + if (!string.IsNullOrEmpty(paramFromTemplate.DefaultIfOptionWithoutValue)) + { + object? resolvedValue = template.Generator.ConvertParameterValueToType(_environmentSettings, paramFromTemplate, paramFromTemplate.DefaultIfOptionWithoutValue!, out bool valueResolutionError); + if (!valueResolutionError) + { + if (resolvedValue is null) + { + throw new InvalidOperationException($"{nameof(resolvedValue)} cannot be null when {nameof(valueResolutionError)} is 'false'."); + } + templateParamsBuilder.SetParameterValue(paramFromTemplate, resolvedValue, DataSource.DefaultIfNoValue); + } + // don't fail on value resolution errors, but report them as authoring problems. + else + { + _logger.LogDebug($"Template {template.Identity} has an invalid DefaultIfOptionWithoutValue value for parameter {inputParam.ParameterDefinition.Name}"); // CodeQL [cs/privacy/suspicious-logging-arguments] False Positive: CodeQL wrongly detected "Identity" + } + } + else + { + if (string.IsNullOrEmpty(paramFromTemplate.Precedence.IsEnabledCondition)) + { + tmpParamsWithInvalidValues.Add(paramFromTemplate.Name); + } + // in case the IsEnabledCondition is configured - we'll check if parameter needed to be resolved + // after we evaluate it's enablement condition (and for disabled parameter we do not care) + } + } + else + { + object? resolvedValue = template.Generator.ConvertParameterValueToType(_environmentSettings, paramFromTemplate, inputParam.Value.ToString(), out bool valueResolutionError); + if (!valueResolutionError) + { + if (resolvedValue is null) + { + throw new InvalidOperationException($"{nameof(resolvedValue)} cannot be null when {nameof(valueResolutionError)} is 'false'."); + } + templateParamsBuilder.SetParameterValue(paramFromTemplate, resolvedValue, DataSource.User); + } + else + { + tmpParamsWithInvalidValues.Add(paramFromTemplate.Name); + } + } + } + } + } + + private InputDataSet? EvaluateConditionalParameters( + IParameterSetBuilder parametersBuilder, + InputDataSet inputParameters, + ITemplate template, + out IReadOnlyList paramsWithInvalidValues, + out bool isExternalEvaluationInvalid) + { + if (!inputParameters.HasConditions()) + { + paramsWithInvalidValues = []; + isExternalEvaluationInvalid = false; + return parametersBuilder.Build(false, template.Generator, _logger); + } + + bool isEvaluatedExternally = false; + foreach (EvaluatedInputParameterData evaluatedParameterData in inputParameters.Values.OfType()) + { + isEvaluatedExternally = true; + parametersBuilder.SetParameterEvaluation(evaluatedParameterData.ParameterDefinition, evaluatedParameterData); + } + + if (isEvaluatedExternally) + { + if (!parametersBuilder.CheckIsParametersEvaluationCorrect( + template.Generator, _logger, !inputParameters.ContinueOnMismatchedConditionsEvaluation, out paramsWithInvalidValues)) + { + _logger.LogInformation( + "Parameters conditions ('IsEnabled', 'IsRequired') evaluation supplied by host didn't match validation against internal evaluation for following parameters: [{0}]. Host requested to continue in such case: {1}", + paramsWithInvalidValues.ToCsvString(), + inputParameters.ContinueOnMismatchedConditionsEvaluation); + + if (!inputParameters.ContinueOnMismatchedConditionsEvaluation) + { + isExternalEvaluationInvalid = true; + return null; + } + } + } + + isExternalEvaluationInvalid = false; + InputDataSet evaluatedParameterSetData = parametersBuilder.Build(!isEvaluatedExternally, template.Generator, _logger); + List defaultParamsWithInvalidValues = new List(); + + // for params that changed to optional and do not have values - try get 'Default' value (as for required it's not obtained) + evaluatedParameterSetData.Values + .Where(v => + v.InputDataState != InputDataState.Set && + !string.IsNullOrEmpty(v.ParameterDefinition.Precedence.IsRequiredCondition) && + v is EvaluatedInputParameterData evaluated && + evaluated.GetEvaluatedPrecedence() != EvaluatedPrecedence.Disabled && + evaluated.GetEvaluatedPrecedence() != EvaluatedPrecedence.Required) + .ForEach(p => parametersBuilder.SetParameterDefault(template.Generator, p.ParameterDefinition, _environmentSettings, false, false, defaultParamsWithInvalidValues)); + + paramsWithInvalidValues = defaultParamsWithInvalidValues; + return evaluatedParameterSetData; + } + + private bool TryCreateParameterSet(ITemplate template, string realName, InputDataSet inputParameters, out IParameterSetData? templateParams, out TemplateCreationResult? failureResult) + { + // there should never be param errors here. If there are, the template is malformed, or the host gave an invalid value. +#pragma warning disable CS0618 // Type or member is obsolete - temporary until the method becomes internal. + IParameterSetBuilder parameterSetBuilder = SetupDefaultParamValuesFromTemplateAndHostInternal(template, realName, out IReadOnlyList defaultParamsWithInvalidValues); + + ResolveUserParameters(template, parameterSetBuilder, inputParameters, out IReadOnlyList userParamsWithInvalidValues); + + if (AnyParametersWithInvalidDefaultsUnresolved(defaultParamsWithInvalidValues, inputParameters, out IReadOnlyList defaultsWithUnresolvedInvalidValues) +#pragma warning restore CS0618 // Type or member is obsolete + || userParamsWithInvalidValues.Count > 0) + { + string message = string.Join(", ", new CombinedList(userParamsWithInvalidValues, defaultsWithUnresolvedInvalidValues)); + failureResult = new TemplateCreationResult(CreationResultStatus.InvalidParamValues, template.Name, message); + templateParams = null; + return false; + } + + InputDataSet? evaluatedParams = EvaluateConditionalParameters(parameterSetBuilder, inputParameters, template, out var paramsWithInvalidValues, out bool isExternalEvaluationInvalid); + if (paramsWithInvalidValues.Any()) + { + failureResult = new TemplateCreationResult(isExternalEvaluationInvalid ? CreationResultStatus.CondtionsEvaluationMismatch : CreationResultStatus.InvalidParamValues, template.Name, paramsWithInvalidValues.ToCsvString()); + templateParams = null; + return false; + } + + IEnumerable missingParams = evaluatedParams!.Values + .Where(v => v.GetEvaluatedPrecedence() == EvaluatedPrecedence.Required && v.InputDataState == InputDataState.Unset) + .Select(v => v.ParameterDefinition.Name); + + if (missingParams.Any()) + { + failureResult = new TemplateCreationResult(CreationResultStatus.MissingMandatoryParam, template.Name, string.Join(", ", missingParams)); + templateParams = null; + return false; + } + + templateParams = parameterSetBuilder.Build(false, template.Generator, _logger).ToParameterSetData(); + + failureResult = null; + return true; + } + + #region Obsolete members + + /// + /// Fully load template from . + /// usually comes from cache and is missing some information. + /// Calling this methods returns full information about template needed to instantiate template. + /// + /// Information about template. + /// Defines which baseline of template to load. + /// Fully loaded template or null if it fails to load template. +#pragma warning disable SA1202 // Elements should be ordered by access + [Obsolete("The method is obsolete and won't be replaced. Use InstantiateAsync to run the template.")] + public ITemplate? LoadTemplate(ITemplateInfo info, string? baselineName) +#pragma warning restore SA1202 // Elements should be ordered by access + { + return Task.Run(async () => await LoadTemplateAsync(info, baselineName, default).ConfigureAwait(false)).GetAwaiter().GetResult(); + } + + [Obsolete("The method is deprecated.")] + //This method should become internal once Cli help logic is refactored. + public void ReleaseMountPoints(ITemplate template) + { + } + + /// + /// Reads the parameters from the template and the host and setup their values in the return IParameterSet. + /// Host param values override template defaults. + /// + /// + /// + /// + /// + [Obsolete("This method is deprecated.")] + //This method should become internal once Cli help logic is refactored. + public IParameterSet SetupDefaultParamValuesFromTemplateAndHost(ITemplate templateInfo, string realName, out IReadOnlyList paramsWithInvalidValues) + { + return (IParameterSet)SetupDefaultParamValuesFromTemplateAndHostInternal(templateInfo, realName, out paramsWithInvalidValues); + } + + /// + /// The template params for which there are same-named input parameters have their values set to the corresponding input parameters value. + /// input parameters that do not have corresponding template params are ignored. + /// + /// + /// + /// + /// + [Obsolete("This method is deprecated.")] + //This method should become internal once Cli help logic is refactored. + public void ResolveUserParameters(ITemplate template, IParameterSet templateParams, IReadOnlyDictionary inputParameters, out IReadOnlyList paramsWithInvalidValues) + { + ResolveUserParameters(template, new LegacyParamSetWrapper(templateParams), templateParams.ToInputDataSet(), out paramsWithInvalidValues); + } + + /// + /// This class is to be used solely to hold backward compatibility of method. + /// Hence only the base and are implemented - no other methods are called in scope of . + /// + [Obsolete("Proxy for obsolete ResolveUserParameters method", false)] + private class LegacyParamSetWrapper : ParameterDefinitionSet, IParameterSetBuilder + { + private readonly IParameterSet _parameterSet; + + public LegacyParamSetWrapper(IParameterSet parameterSet) + : base(parameterSet.ParameterDefinitions) => _parameterSet = parameterSet; + + public bool CheckIsParametersEvaluationCorrect(IGenerator generator, ILogger logger, bool throwOnError, out IReadOnlyList paramsWithInvalidEvaluations) => + throw new NotImplementedException(); + + public InputDataSet Build(bool evaluateConditions, IGenerator generator, ILogger logger) => throw new NotImplementedException(); + + public void SetParameterDefault( + IGenerator generator, + ITemplateParameter parameter, + IEngineEnvironmentSettings environment, + bool useHostDefaults, + bool isRequired, + List paramsWithInvalidValues) => + throw new NotImplementedException(); + + public bool HasParameterValue(ITemplateParameter parameter) => throw new NotImplementedException(); + + public void SetParameterEvaluation(ITemplateParameter parameter, EvaluatedInputParameterData evaluatedParameterData) => throw new NotImplementedException(); + + public void SetParameterValue(ITemplateParameter parameter, object value, DataSource dataSource) => _parameterSet.ResolvedValues[parameter] = value; + } + #endregion + + private class TemplateLocator : IExtendedTemplateLocator + { + public TemplateLocator(Guid generatorId, string mountPointUri, string configPlace) + { + GeneratorId = generatorId; + MountPointUri = mountPointUri; + ConfigPlace = configPlace; + } + + public Guid GeneratorId { get; } + + public string MountPointUri { get; } + + public string ConfigPlace { get; } + + public string? LocaleConfigPlace { get; init; } + + public string? HostConfigPlace { get; init; } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateEqualityComparer.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateEqualityComparer.cs new file mode 100644 index 000000000000..2ff2e613e299 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateEqualityComparer.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Edge.Template +{ + [Obsolete("This class is deprecated.")] + public class TemplateEqualityComparer : IEqualityComparer + { + public static IEqualityComparer Default { get; } = new TemplateEqualityComparer(); + + public bool Equals(ITemplateInfo x, ITemplateInfo y) + { + return ReferenceEquals(x, y) || (x != null && y != null && string.Equals(x.Identity, y.Identity, StringComparison.Ordinal)); + } + + public int GetHashCode(ITemplateInfo obj) + { + return obj?.Identity?.GetHashCode() ?? 0; + } + } + + // Compares the templates, irrespective of the match result +} + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateMatchInfoEqualityComparer.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateMatchInfoEqualityComparer.cs new file mode 100644 index 000000000000..9ba3fcdd0c5c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/TemplateMatchInfoEqualityComparer.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Edge.Template +{ + [Obsolete("This class is deprecated.")] + public class TemplateMatchInfoEqualityComparer : IEqualityComparer + { + public static IEqualityComparer Default { get; } = new TemplateMatchInfoEqualityComparer(); + + public bool Equals(ITemplateMatchInfo x, ITemplateMatchInfo y) + { + return ReferenceEquals(x?.Info, y?.Info) || (x != null && y != null && x?.Info != null && y?.Info != null && string.Equals(x?.Info?.Identity, y?.Info?.Identity, StringComparison.Ordinal)); + } + + public int GetHashCode(ITemplateMatchInfo obj) + { + return obj?.Info?.Identity?.GetHashCode() ?? 0; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/WellKnownSearchFilters.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/WellKnownSearchFilters.cs new file mode 100644 index 000000000000..5758a0b6fc05 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/Template/WellKnownSearchFilters.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge.Template +{ + [Obsolete("Use " + nameof(Microsoft.TemplateEngine.Utils.WellKnownSearchFilters) + " instead")] + public static class WellKnownSearchFilters + { + public static Func NameFilter(string name) + { + return (template) => + { + if (string.IsNullOrEmpty(name)) + { + return new MatchInfo { Location = MatchLocation.Name, Kind = MatchKind.Partial }; + } + + int nameIndex = template.Name.IndexOf(name, StringComparison.OrdinalIgnoreCase); + + if (nameIndex == 0 && template.Name.Length == name.Length) + { + return new MatchInfo { Location = MatchLocation.Name, Kind = MatchKind.Exact }; + } + + bool hasShortNamePartialMatch = false; + + foreach (string shortName in template.ShortNameList) + { + int shortNameIndex = shortName.IndexOf(name, StringComparison.OrdinalIgnoreCase); + + if (shortNameIndex == 0 && shortName.Length == name.Length) + { + return new MatchInfo { Location = MatchLocation.ShortName, Kind = MatchKind.Exact }; + } + + hasShortNamePartialMatch |= shortNameIndex > -1; + } + + if (nameIndex > -1) + { + return new MatchInfo { Location = MatchLocation.Name, Kind = MatchKind.Partial }; + } + + if (hasShortNamePartialMatch) + { + return new MatchInfo { Location = MatchLocation.ShortName, Kind = MatchKind.Partial }; + } + + return new MatchInfo { Location = MatchLocation.Name, Kind = MatchKind.Mismatch }; + }; + } + + // This being case-insensitive depends on the dictionaries on the cache tags being declared as case-insensitive + public static Func ContextFilter(string inputContext) + { + string? context = inputContext?.ToLowerInvariant(); + + return (template) => + { + if (string.IsNullOrEmpty(context)) + { + return null; + } + if (template.GetTemplateType()?.Equals(context, StringComparison.OrdinalIgnoreCase) ?? false) + { + return new MatchInfo { Location = MatchLocation.Context, Kind = MatchKind.Exact }; + } + else + { + return new MatchInfo { Location = MatchLocation.Context, Kind = MatchKind.Mismatch }; + } + }; + } + + /// + /// Creates predicate for matching the template and given tag value. + /// If the template contains the tag , it is exact match, otherwise mismatch. + /// If the template has no tags defined, it is a mismatch. + /// If is null or empty the method returns null. + /// + /// tag to filter by. + /// A predicate that returns if the given template matches . + public static Func TagFilter(string tagFilter) + { + return (template) => + { + if (string.IsNullOrWhiteSpace(tagFilter)) + { + return null; + } + if (template.Classifications?.Contains(tagFilter, StringComparer.OrdinalIgnoreCase) ?? false) + { + return new MatchInfo { Location = MatchLocation.Classification, Kind = MatchKind.Exact }; + } + return new MatchInfo { Location = MatchLocation.Classification, Kind = MatchKind.Mismatch }; + }; + } + + // This being case-insensitive depends on the dictionaries on the cache tags being declared as case-insensitive + // Note: This is specifically designed to provide match info against a user-input language. + // All dealings with the host-default language should be separate from this. + public static Func LanguageFilter(string language) + { + return (template) => + { + if (string.IsNullOrEmpty(language)) + { + return null; + } + + if (template.GetLanguage()?.Equals(language, StringComparison.OrdinalIgnoreCase) ?? false) + { + return new MatchInfo { Location = MatchLocation.Language, Kind = MatchKind.Exact }; + } + else + { + return new MatchInfo { Location = MatchLocation.Language, Kind = MatchKind.Mismatch }; + } + }; + } + + public static Func BaselineFilter(string baselineName) + { + return (template) => + { + if (string.IsNullOrEmpty(baselineName)) + { + return null; + } + + if (template.BaselineInfo != null && template.BaselineInfo.ContainsKey(baselineName)) + { + return new MatchInfo { Location = MatchLocation.Baseline, Kind = MatchKind.Exact }; + } + else + { + return new MatchInfo { Location = MatchLocation.Baseline, Kind = MatchKind.Mismatch }; + } + }; + } + + [Obsolete("Use TagsFilter instead")] + public static Func ClassificationsFilter(string name) + { + return (template) => + { + if (string.IsNullOrEmpty(name)) + { + return null; + } + + string[] parts = name.Split('/'); + + if (template.Classifications != null) + { + bool allParts = true; + bool anyParts = false; + + foreach (string part in parts) + { + if (!template.Classifications.Contains(part, StringComparer.OrdinalIgnoreCase)) + { + allParts = false; + } + else + { + anyParts = true; + } + } + + anyParts &= parts.Length == template.Classifications.Count; + + if (allParts || anyParts) + { + return new MatchInfo { Location = MatchLocation.Classification, Kind = allParts ? MatchKind.Exact : MatchKind.Partial }; + } + } + + return null; + }; + } + + /// + /// Creates predicate for matching the template and given author value. + /// + /// author to use for match. + /// A predicate that returns if the given template matches defined author. + public static Func AuthorFilter(string author) + { + return (template) => + { + if (string.IsNullOrWhiteSpace(author)) + { + return null; + } + + if (string.IsNullOrWhiteSpace(template.Author)) + { + return new MatchInfo { Location = MatchLocation.Author, Kind = MatchKind.Mismatch }; + } + + int authorIndex = template.Author!.IndexOf(author, StringComparison.OrdinalIgnoreCase); + + if (authorIndex == 0 && template.Author.Length == author.Length) + { + return new MatchInfo { Location = MatchLocation.Author, Kind = MatchKind.Exact }; + } + + if (authorIndex > -1) + { + return new MatchInfo { Location = MatchLocation.Author, Kind = MatchKind.Partial }; + } + + return new MatchInfo { Location = MatchLocation.Author, Kind = MatchKind.Mismatch }; + }; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/TemplateConstraintManager.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/TemplateConstraintManager.cs new file mode 100644 index 000000000000..6a8ada2e6caf --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/TemplateConstraintManager.cs @@ -0,0 +1,239 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; + +namespace Microsoft.TemplateEngine.Edge +{ + /// + /// Manages evaluation of constraints for the templates. + /// + public class TemplateConstraintManager : IDisposable + { + private readonly ILogger _logger; + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private readonly Dictionary> _templateConstrains = new Dictionary>(); + + public TemplateConstraintManager(IEngineEnvironmentSettings engineEnvironmentSettings) + { + _logger = engineEnvironmentSettings.Host.LoggerFactory.CreateLogger(); + + var constraintFactories = engineEnvironmentSettings.Components.OfType(); + _logger.LogDebug($"Found {constraintFactories.Count()} constraints factories, initializing."); + foreach (var constraintFactory in constraintFactories) + { + _templateConstrains[constraintFactory.Type] = Task.Run(() => constraintFactory.CreateTemplateConstraintAsync(engineEnvironmentSettings, _cancellationTokenSource.Token)); + } + + } + + /// + /// Returns the list of initialized s. + /// Only returns the list of that were initialized successfully. + /// The constraints which failed to be initialized are skipped and warning is logged. + /// + /// if given, only returns the list of constraints defined in the templates. + /// + /// The list of successfully initialized s. + public async Task> GetConstraintsAsync(IEnumerable? templates = null, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + IEnumerable<(string Type, Task Task)> constraintsToInitialize; + if (templates?.Any() ?? false) + { + List uniqueConstraints = templates.SelectMany(ti => ti.Constraints.Select(c => c.Type)).Distinct().ToList(); + constraintsToInitialize = _templateConstrains.Where(kvp => uniqueConstraints.Contains(kvp.Key)).Select(kvp => (kvp.Key, kvp.Value)); + } + else + { + constraintsToInitialize = _templateConstrains.Select(kvp => (kvp.Key, kvp.Value)); + } + + try + { + _logger.LogDebug($"Waiting for {constraintsToInitialize.Count()} to be initialized."); + await CancellableWhenAll(constraintsToInitialize.Select(c => c.Task), cancellationToken).ConfigureAwait(false); + _logger.LogDebug($"{constraintsToInitialize.Count()} constraints were initialized."); + return constraintsToInitialize.Select(c => c.Task.Result).ToList(); + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception) + { + foreach (var constraint in constraintsToInitialize) + { + if (constraint.Task.IsFaulted || constraint.Task.IsCanceled) + { + _logger.LogWarning(LocalizableStrings.TemplateConstraintManager_Error_FailedToInitialize, constraint.Type, constraint.Task.Exception.Message); + _logger.LogDebug($"Details: {constraint.Task.Exception}."); + } + } + _logger.LogDebug($"{constraintsToInitialize.Count(c => c.Task.Status == TaskStatus.RanToCompletion)} constraints were initialized."); + return constraintsToInitialize + .Where(c => c.Task.Status == TaskStatus.RanToCompletion) + .Select(c => c.Task.Result) + .ToList(); + } + + } + + /// + /// Evaluates the constraints with given for given args . + /// + /// constraint type to evaluate. + /// arguments to use for evaluation. + /// + /// indicating if constraint is met, or details why the constraint is not met. + public async Task EvaluateConstraintAsync(string type, string? args, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (!_templateConstrains.TryGetValue(type, out Task task)) + { + _logger.LogDebug($"The constraint '{type}' is unknown."); + return TemplateConstraintResult.CreateInitializationFailure(type, string.Format(LocalizableStrings.TemplateConstraintManager_Error_UnknownType, type)); + } + + if (!task.IsCompleted) + { + try + { + _logger.LogDebug($"The constraint '{type}' is not initialized, waiting for initialization."); + await CancellableWhenAll(new[] { task }, cancellationToken).ConfigureAwait(false); + _logger.LogDebug($"The constraint '{type}' is initialized successfully."); + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception) + { + //handled below + } + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (task.IsFaulted || task.IsCanceled) + { + var exception = task.Exception is not null ? task.Exception.InnerException ?? task.Exception : task.Exception; + _logger.LogDebug($"The constraint '{type}' failed to be initialized, details: {exception}."); + return TemplateConstraintResult.CreateInitializationFailure(type, string.Format(LocalizableStrings.TemplateConstraintManager_Error_FailedToInitialize, type, exception?.Message)); + } + + try + { + return task.Result.Evaluate(args); + } + catch (Exception e) + { + _logger.LogDebug($"The constraint '{type}' failed to be evaluated for the args '{args}', details: {e}."); + return TemplateConstraintResult.CreateEvaluationFailure(task.Result, string.Format(LocalizableStrings.TemplateConstraintManager_Error_FailedToEvaluate, type, args, e.Message)); + } + + } + + /// + /// Evaluates the constraints with given . + /// The method doesn't throw when the constraint is failed to be evaluated, returns with status instead. + /// + /// the list of templates to evaluate constraints for given templates. + /// + /// indicating if constraint is met, or details why the constraint is not met. + public async Task Result)>> EvaluateConstraintsAsync(IEnumerable templates, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var requiredConstraints = templates.SelectMany(t => t.Constraints).Select(c => c.Type).Distinct(); + var tasksToWait = new List(); + foreach (var constraintType in requiredConstraints) + { + if (!_templateConstrains.TryGetValue(constraintType, out Task task)) + { + //handled below + continue; + } + tasksToWait.Add(task); + } + + if (tasksToWait.Any(t => !t.IsCompleted)) + { + try + { + var notCompletedTasks = tasksToWait.Where(t => !t.IsCompleted); + _logger.LogDebug($"The constraint(s) are not initialized, waiting for initialization."); + await CancellableWhenAll(notCompletedTasks, cancellationToken).ConfigureAwait(false); + _logger.LogDebug($"The constraint(s) are initialized successfully."); + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception) + { + //handled below + } + } + cancellationToken.ThrowIfCancellationRequested(); + + List<(ITemplateInfo, IReadOnlyList)> evaluationResult = new(); + foreach (ITemplateInfo template in templates) + { + List constraintResults = new(); + foreach (var constraint in template.Constraints) + { + if (!_templateConstrains.TryGetValue(constraint.Type, out Task task)) + { + _logger.LogDebug($"The constraint '{constraint.Type}' is unknown."); + constraintResults.Add(TemplateConstraintResult.CreateInitializationFailure(constraint.Type, string.Format(LocalizableStrings.TemplateConstraintManager_Error_UnknownType, constraint.Type))); + continue; + } + + if (task.IsFaulted || task.IsCanceled) + { + var exception = task.Exception is not null ? task.Exception.InnerException ?? task.Exception : task.Exception; + _logger.LogDebug($"The constraint '{constraint.Type}' failed to be initialized, details: {exception}."); + constraintResults.Add(TemplateConstraintResult.CreateInitializationFailure(constraint.Type, string.Format(LocalizableStrings.TemplateConstraintManager_Error_FailedToInitialize, constraint.Type, exception?.Message))); + continue; + } + + try + { + constraintResults.Add(task.Result.Evaluate(constraint.Args)); + } + catch (Exception e) + { + _logger.LogDebug($"The constraint '{constraint.Type}' failed to be evaluated for the args '{constraint.Args}', details: {e}."); + constraintResults.Add(TemplateConstraintResult.CreateEvaluationFailure(_templateConstrains[constraint.Type].Result, string.Format(LocalizableStrings.TemplateConstraintManager_Error_FailedToEvaluate, constraint.Type, constraint.Args, e.Message))); + } + } + evaluationResult.Add((template, constraintResults)); + } + return evaluationResult; + } + + /// + public void Dispose() + { + _cancellationTokenSource.Cancel(); + } + + private async Task CancellableWhenAll(IEnumerable tasks, CancellationToken cancellationToken) + { + await Task.WhenAny( + Task.WhenAll(tasks), + Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(false); + + if (cancellationToken.IsCancellationRequested) + { + throw new TaskCanceledException(); + } + + //throws exceptions + await Task.WhenAll(tasks).ConfigureAwait(false); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/TemplateListFilter.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/TemplateListFilter.cs new file mode 100644 index 000000000000..3c5f8b6f60a2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/TemplateListFilter.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.Edge.Template; + +namespace Microsoft.TemplateEngine.Edge +{ + public static class TemplateListFilter + { + /// + /// Exact match criteria - the templates should match all filters. + /// + /// + [Obsolete("Use " + nameof(Utils.WellKnownSearchFilters.MatchesAllCriteria) + " instead")] + public static Func ExactMatchFilter => x => x.IsMatch; + + /// + /// Partial match criteria - the templates should match at least one of the filters. + /// + /// + [Obsolete("Use " + nameof(Utils.WellKnownSearchFilters.MatchesAtLeastOneCriteria) + " instead")] + public static Func PartialMatchFilter => x => x.IsPartialMatch; + + [Obsolete("Use " + nameof(TemplatePackageManager.GetTemplatesAsync) + " instead")] + public static IReadOnlyCollection FilterTemplates(IReadOnlyList templateList, bool exactMatchesOnly, params Func[] filters) + { + HashSet matchingTemplates = new HashSet(FilteredTemplateEqualityComparer.Default); + + foreach (ITemplateInfo template in templateList) + { + List matchInformation = new List(); + + foreach (Func filter in filters) + { + MatchInfo? result = filter(template); + + if (result.HasValue) + { + matchInformation.Add(result.Value); + } + } + + FilteredTemplateInfo info = new FilteredTemplateInfo(template, matchInformation); + + if (info.IsMatch || (!exactMatchesOnly && info.IsPartialMatch)) + { + matchingTemplates.Add(info); + } + } + +#if NETFRAMEWORK + return matchingTemplates.ToList(); +#else + return matchingTemplates; +#endif + } + + /// + /// Gets matching information for templates for provided filters. + /// + /// The templates to be filtered. + /// The criteria of template to be filtered. + /// The list of filters to be applied. + /// The filtered list of templates with matches information. + /// + /// GetTemplateMatchInfo(templates, TemplateListFilter.ExactMatchFilter, WellKnownSearchFilters.NameFilter("myname") - returns the templates which name or short name contains "myname".
+ /// GetTemplateMatchInfo(templates, TemplateListFilter.PartialMatchFilter, WellKnownSearchFilters.NameFilter("myname"), WellKnownSearchFilters.NameFilter("othername") - returns the templates which name or short name contains "myname" or "othername".
+ ///
+ [Obsolete("Use " + nameof(TemplatePackageManager.GetTemplatesAsync) + " instead")] + public static IReadOnlyCollection GetTemplateMatchInfo(IReadOnlyList templateList, Func matchFilter, params Func[] filters) + { + HashSet matchingTemplates = new HashSet(Template.TemplateMatchInfoEqualityComparer.Default); + + foreach (ITemplateInfo template in templateList) + { + List matchInformation = new List(); + + foreach (Func filter in filters) + { + MatchInfo? result = filter(template); + + if (result.HasValue) + { + matchInformation.Add(result.Value); + } + } + + ITemplateMatchInfo info = new TemplateMatchInfoEx(template, matchInformation); + if (matchFilter(info)) + { + matchingTemplates.Add(info); + } + } + +#if NETFRAMEWORK + return matchingTemplates.ToList(); +#else + return matchingTemplates; +#endif + } + + //TODO: we cannot remove the method below as CLI needs it due to it changes ITemplateInfo before filtering. Once CLI is refactored, this method can be removed. + [Obsolete("Use " + nameof(TemplatePackageManager.GetTemplatesAsync) + " instead")] + public static IReadOnlyCollection GetTemplateMatchInfo(IReadOnlyList templateList, Func matchFilter, params Func[] filters) + { + HashSet matchingTemplates = new HashSet(TemplateMatchInfoEqualityComparer.Default); + + foreach (ITemplateInfo template in templateList) + { + List matchInformation = new List(); + + foreach (Func filter in filters) + { + Abstractions.TemplateFiltering.MatchInfo? result = filter(template); + if (result != null) + { + matchInformation.Add(result); + } + } + + Abstractions.TemplateFiltering.ITemplateMatchInfo info = new TemplateMatchInfo(template, matchInformation); + if (matchFilter(info)) + { + matchingTemplates.Add(info); + } + } + +#if NETFRAMEWORK + return matchingTemplates.ToList(); +#else + return matchingTemplates; +#endif + } + + private class TemplateMatchInfoEqualityComparer : IEqualityComparer + { + internal static IEqualityComparer Default { get; } = new TemplateMatchInfoEqualityComparer(); + + public bool Equals(Abstractions.TemplateFiltering.ITemplateMatchInfo x, Abstractions.TemplateFiltering.ITemplateMatchInfo y) + { + return ReferenceEquals(x?.Info, y?.Info) || (x != null && y != null && x?.Info != null && y?.Info != null && string.Equals(x?.Info?.Identity, y?.Info?.Identity, StringComparison.Ordinal)); + } + + public int GetHashCode(Abstractions.TemplateFiltering.ITemplateMatchInfo obj) + { + return obj?.Info?.Identity?.GetHashCode() ?? 0; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/ValidationUtils.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/ValidationUtils.cs new file mode 100644 index 000000000000..03c1903d08ed --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/ValidationUtils.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Edge +{ + internal static class ValidationUtils + { + internal static void LogValidationResults(ILogger logger, IReadOnlyList templates) + { + foreach (IScanTemplateInfo template in templates) + { + string templateDisplayName = GetTemplateDisplayName(template); + logger.LogDebug("Found template {0}", templateDisplayName); + + ValidateTemplate(logger, template, templateDisplayName); + + foreach (KeyValuePair locator in template.Localizations) + { + ILocalizationLocator localizationInfo = locator.Value; + ValidateLocalization(logger, templateDisplayName, localizationInfo); + } + + if (!template.IsValid) + { + logger.LogError(LocalizableStrings.Validation_InvalidTemplate, templateDisplayName); + } + foreach (ILocalizationLocator invalidLoc in template.Localizations.Values.Where(li => !li.IsValid)) + { + logger.LogWarning(LocalizableStrings.Validation_InvalidTemplateLoc, invalidLoc.Locale, templateDisplayName); + } + } + } + + internal static void LogValidationResults(ILogger logger, IReadOnlyList templates) + { + foreach (ITemplate template in templates) + { + string templateDisplayName = GetTemplateDisplayName(template); + logger.LogDebug("Found template {0}", templateDisplayName); + + ValidateTemplate(logger, template, templateDisplayName); + + ILocalizationLocator? localizationInfo = template.Localization; + + if (localizationInfo != null) + { + ValidateLocalization(logger, templateDisplayName, localizationInfo); + } + + if (!template.IsValid) + { + logger.LogError(LocalizableStrings.Validation_InvalidTemplate, templateDisplayName); + } + if (!template.Localization?.IsValid ?? false) + { + logger.LogWarning(LocalizableStrings.Validation_InvalidTemplateLoc, template.Localization!.Locale, templateDisplayName); + } + } + } + + private static void ValidateTemplate(ILogger logger, IValidationInfo template, string templateDisplayName) + { + LogValidationEntries( + logger, + string.Format(LocalizableStrings.Validation_Error_Header, templateDisplayName), + template.ValidationErrors, + IValidationEntry.SeverityLevel.Error); + LogValidationEntries( + logger, + string.Format(LocalizableStrings.Validation_Warning_Header, templateDisplayName), + template.ValidationErrors, + IValidationEntry.SeverityLevel.Warning); + LogValidationEntries( + logger, + string.Format(LocalizableStrings.Validation_Info_Header, templateDisplayName), + template.ValidationErrors, + IValidationEntry.SeverityLevel.Info); + } + + private static void ValidateLocalization(ILogger logger, string templateDisplayName, ILocalizationLocator localizationInfo) + { + LogValidationEntries( + logger, + string.Format(LocalizableStrings.Validation_LocError_Header, templateDisplayName, localizationInfo.Locale), + localizationInfo.ValidationErrors, + IValidationEntry.SeverityLevel.Error); + LogValidationEntries( + logger, + string.Format(LocalizableStrings.Validation_LocWarning_Header, templateDisplayName, localizationInfo.Locale), + localizationInfo.ValidationErrors, + IValidationEntry.SeverityLevel.Warning); + LogValidationEntries( + logger, + string.Format(LocalizableStrings.Validation_LocInfo_Header, templateDisplayName, localizationInfo.Locale), + localizationInfo.ValidationErrors, + IValidationEntry.SeverityLevel.Info); + } + + private static string GetTemplateDisplayName(ITemplateMetadata template) + { + string templateName = string.IsNullOrEmpty(template.Name) ? "" : template.Name; + return $"'{templateName}' ({template.Identity})"; + } + + private static string PrintError(IValidationEntry error) => $" [{error.Severity}][{error.Code}] {error.ErrorMessage}"; + + private static void LogValidationEntries(ILogger logger, string header, IReadOnlyList errors, IValidationEntry.SeverityLevel severity) + { + Action log = severity switch + { + IValidationEntry.SeverityLevel.None => (s) => throw new NotSupportedException($"{IValidationEntry.SeverityLevel.None} severity is not supported."), + IValidationEntry.SeverityLevel.Info => (s) => logger.LogDebug(s), + IValidationEntry.SeverityLevel.Warning => (s) => logger.LogWarning(s), + IValidationEntry.SeverityLevel.Error => (s) => logger.LogError(s), + _ => throw new InvalidOperationException($"{severity} is not expected value for {nameof(IValidationEntry.SeverityLevel)}."), + }; + + if (!errors.Any(e => e.Severity == severity)) + { + return; + } + + StringBuilder sb = new(); + sb.AppendLine(header); + foreach (IValidationEntry error in errors.Where(e => e.Severity == severity)) + { + sb.AppendLine(PrintError(error)); + } + log(sb.ToString()); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/VirtualEnvironment.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/VirtualEnvironment.cs new file mode 100644 index 000000000000..a908c0512d6f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/VirtualEnvironment.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Edge +{ + /// + /// Virtualized implementation of . + /// Allows to overwrite and/or add environment variables not defined in physical environment. + /// + public class VirtualEnvironment : DefaultEnvironment + { + /// + /// Creates new instance of . + /// + /// Variables to be considered as environment variables. They have precedence over physical environment variables. + /// If set to true - variables from are added. + public VirtualEnvironment(IReadOnlyDictionary? virtualEnvironment, bool includeRealEnvironment) + : base(MergeEnvironmentVariables(virtualEnvironment, includeRealEnvironment)) + { } + + private static IReadOnlyDictionary MergeEnvironmentVariables( + IReadOnlyDictionary? virtualEnvironment, bool includeRealEnvironment) + { + Dictionary variables = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (includeRealEnvironment) + { + variables.Merge(FetchEnvironmentVariables()); + } + + if (virtualEnvironment != null) + { + variables.Merge(virtualEnvironment); + } + + return variables; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.cs.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.cs.xlf new file mode 100644 index 000000000000..efde981a7eab --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.cs.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + Podmínky parametru obsahují cyklickou závislost: [{0}], která brání deterministickému vyhodnocení. + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + Nepovedlo se vyhodnotit podmínku {0} u parametru {1} (text podmínky: {2}, chyba vyhodnocení: {3}) – podmínka může být poškozená nebo odkazované parametry nemají výchozí ani explicitní hodnoty. + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + Neočekávaná vnitřní chyba : Nelze provést topologické řazení závislostí parametrů, které pravděpodobně nemají cyklické závislosti. + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + Podmínky parametru obsahují cyklickou závislost: [{0}]. S aktuálními hodnotami parametrů je možné deterministicky vyhodnotit parametry, takže pokračujeme dále. Šablona by se ale měla zkontrolovat, protože vytvoření instance s různými parametry může vést k chybě. + {0} is the dependency chain + + + '{0}' should not contain empty items + {0} nesmí obsahovat prázdné položky. + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + Argumenty nebyly zadány. Měl by být zadán alespoň jeden argument. + + + + '{0}' does not contain valid items. + {0} neobsahuje platné položky. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + {0} není platný kód JSON. + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + {0} by mělo být pole objektů. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + {0} není platné pole JSON. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + {0} není platný řetězec nebo pole JSON. + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + {0} není platná verze nebo rozsah verzí. + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + Zkontrolujte konfiguraci omezení v souboru template.json. + + + + Environment variables + Proměnné prostředí + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + Pokus o předání výsledku externího vyhodnocení podmínek parametrů pro parametry, které nemají v šabloně nastavenou odpovídající podmínku (atributy IsEnabled nebo IsRequired nejsou vyplněné podmínkou) nebo selhání předání výsledků podmínky pro parametry s podmínkami v šabloně. Parametry s chybou: {0} + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + Pokus o předání výsledku externího vyhodnocení podmínek parametrů pro parametry, které nemají v šabloně nastavenou odpovídající podmínku (atributy IsEnabled nebo IsRequired nejsou vyplněné podmínkou) nebo selhání předání výsledků podmínky pro parametry s podmínkami v šabloně. Urážecí parametry: {0} + + + + The folder {0} doesn't exist. + Složka {0} neexistuje. + + + + Check the constraint configuration in template.json. + Zkontrolujte konfiguraci omezení v souboru template.json. + + + + latest version + nejnovější verze + small letters, string is used in the sentence + + + version {0} + verze {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + Balíček {0} může nainstalovat několik různých instalačních programů. Určete název instalačního programu, který se má použít. + + + + {0} is already installed. + {0} už je nainstalováno. + + + + {0} cannot be installed. + Balíček {0} nelze nainstalovat. + + + + {0} is already installed, it will be replaced with {1}. + Balíček {0} je už nainstalovaný, nahradí se za {1}. + + + + {0} was successfully uninstalled. + Balíček {0} se úspěšně odinstaloval. + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + Nepodařilo se přečíst seznam nainstalovaných balíčků šablon v {0}. Příčinou může být poškození souboru. Zkontrolujte prosím tento soubor ručně a opravte chyby ve struktuře JSON, nebo soubor odeberte, abyste vymazali seznam nainstalovaných balíčků a znovu je nainstalovali. Podrobnosti o chybě: {1} + + + + '{0}' does not have mandatory property '{1}'. + {0} nemá povinnou vlastnost {1}. + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + Spuštění šablony na {0} (verze: {1}) se nepodporuje, podporovaní hostitelé jsou: {2}. + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + Hostitel modulu šablon + + + + Host defined parameters + Parametry definované hostitelem + + + + Found package is vulnerable source: {0} + Nalezený balíček je ohroženým zdrojem: {0} + + + + Failed to load the NuGet source {0}. + Nepovedlo se načíst zdroj NuGet {0}. + + + + Failed to load NuGet sources configured for the folder {0}. + Nepovedlo se načíst zdroje NuGet nakonfigurované pro složku {0}. + + + + Failed to read package information from NuGet source {0}. + Nepovedlo se přečíst informace o balíčku ze zdroje NuGet {0}. + + + + File {0} already exists. + Soubor {0} již existuje. + + + + No NuGet sources are defined or enabled. + Nejsou definované ani povolené žádné zdroje NuGet. + + + + The checked package {0} is vulnerable. + Zkontrolovaný balíček {0} je ohrožený. + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + Po neúspěšném stažení se nepovedlo odebrat soubor {0}. Pokud existuje, odeberte ho ručně. + + + + Failed to download {0} from NuGet feed {1}. + Nepovedlo se stáhnout {0} z informačního kanálu NuGet {1}. + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + Nepovedlo se načíst zdroj Nuget {0}: zdroj není platný. Při dalším zpracování se přeskočí. + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + Zdroje NuGet {0} jsou nezabezpečené a nebudou se prohledávat. Pokud chcete zahrnout tyto zdroje pro vyhledávání, použijte --force. + + + + {0} is not found in NuGet feeds {1}. + Balíček {0} se nenašel v informačních kanálech NuGet {1}. + + + + Failed to copy package {0} to {1}. + Nepovedlo se zkopírovat balíček {0} do {1}. + + + + Failed to read content of package {0}. + Nepovedlo se přečíst obsah balíčku {0}. + + + + File {0} already exists. + Soubor {0} již existuje. + + + + Failed to download {0} from {1}. + Nepovedlo se stáhnout soubor {0} ze zdroje {1}. + + + + Failed to install the package {0}. +Details: {1}. + Balíček {0} se nepovedlo nainstalovat. +Podrobnosti: {1} + + + + The install request {0} cannot be processed by installer {1}. + Instalační program {1} nemohl zpracovat požadavek na instalaci balíčku {0}. + + + + The NuGet package {0} is invalid. + Balíček NuGet {0} je neplatný. + + + + The configured NuGet sources are invalid: {0}. + Nakonfigurované zdroje NuGet jsou neplatné: {0}. + + + + No NuGet sources are configured. + Nejsou nakonfigurované žádné zdroje NuGet. + + + + The operation was cancelled. + Operace se zrušila. + + + + {0} was not found in NuGet feeds {1}. + Balíček {0} se nenašel v informačních kanálech NuGet {1}. + + + + The package {0} is not supported by installer {1}. + Instalační program {1} nepodporuje balíček {0}. + + + + Failed to uninstall the package {0}. +Details: {1}. + Balíček {0} se nepovedlo odinstalovat. +Podrobnosti: {1} + + + + Failed to check the update for the package {0}. +Details: {1}. + U balíčku {0} se nepovedlo zkontrolovat aktualizace. +Podrobnosti: {1}. + + + + The requested package {0} has vulnerabilities. + Požadovaný balíček {0} obsahuje ohrožení zabezpečení. + + + + The checked package {0} has vulnerabilities. + Zkontrolovaný balíček {0} obsahuje ohrožení zabezpečení. + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + {0} není platný název operačního systému. Povolené hodnoty jsou: {1}. + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + Spuštění šablony na {0} se nepodporuje. Podporované operační systémy jsou: {1}. + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + Operační systém + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + Umístění balíčku šablony {0} se buď nepodporuje, nebo neexistuje. + + + + '{0}' is not a valid semver version. + {0} není platná verze semver. + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + Hostitel poskytl několik komponent ISdkInfoProvider ({0}), takže nejde správně inicializovat SdkVersionConstraint. + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + Hostitel neposkytl žádnou komponentu ISdkInfoProvider. Vlastnost SdkVersionConstraint nelze správně inicializovat. + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + Spuštění šablony v aktuální verzi sady .NET SDK ({0}) se nepodporuje. Podporované verze: {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + Verze sady .NET SDK + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + Omezení {0} se nepodařilo vyhodnotit pro argumenty {1}, podrobnosti: {2}. + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + Inicializace omezení {0} se nezdařila: {1}. + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + Omezení {0} je neznámé. + {0} is constraint type + + + Could not load template. + Nepovedlo se nahrát šablonu. + + + + Failed to create template. +Details: {0} + Nepovedlo se vytvořit šablonu. +Podrobnosti: {0}. + + + + Destructive changes detected. + Rozpoznaly se destruktivní změny. + + + + The template is invalid and cannot be instantiated. + Šablona je neplatná a nelze vytvořit instanci. + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + Vytvoření šablony se nezdařilo: název šablony není zadán. Konfigurace šablony nenakonfiguruje výchozí název, který lze použít pokud není zadán název. Při vytváření instance nebo konfiguraci výchozího názvu v konfiguraci šablony zadejte název šablony. + + + + Failed to load host data in {0} at {1}. + Nepovedlo se načíst data hostitele v {0} v {1}. + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + Načtení balíčku s identifikátorem {0} se nezdařilo. + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + Nepovedlo se načíst balíčky šablon z poskytovatele {0}. +Podrobnosti: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + Balíček {0} se nepovedlo zkontrolovat. +Podrobnosti: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + Nepovedlo se uložit mezipaměť šablony. Podrobnosti: {0} +Mezipaměť šablony bude při příštím spuštění znovu vytvořena. + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +Následující šablony používají stejnou identitu '{0}': +{1} +Bude použita šablona z{2}. Chcete-li tento konflikt vyřešit, odinstalujte konfliktní balíčky šablon. + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {0} '{1}' od '{2}' + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + Šablona {0} obsahuje následující chyby ověření: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + Šablona {0} obsahuje následující ověřovací zprávy: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + Načtení šablony {0} se nezdařilo: šablona není platná. + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + Nepodařilo se načíst lokalizaci {0} u šablony {1}: Lokalizační soubor není platný. Lokalizace bude přeskočena. + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + Šablona {0} má v lokalizaci „{1}“ následující chyby ověřování: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + Šablona {0} má v lokalizaci „{1}“ následující ověřovací zprávy: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + Šablona {0} má v lokalizaci „{1}“ následující upozornění ověření: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + Šablona {0} obsahuje následující upozornění ověření: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + Hostitel poskytl několik komponent IWorkloadsInfoProvider ({0}), takže nejde správně inicializovat workloadConstraint. + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + Hostitel neposkytl žádnou komponentu IWorkloadsInfoProvider. Omezení WorkloadConstraint nelze správně inicializovat. + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + Spuštěná šablona se nepodporuje – nejsou nainstalované požadované volitelné úlohy. Podporované úlohy: {0}. Aktuálně nainstalované volitelné úlohy: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + Úloha + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + Komponenta IWorkloadsInfoProvider poskytnutá hostitelem poskytla několik duplicitních úloh (duplicitní úlohy: {0}). Duplicity budou přeskočeny. + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.de.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.de.xlf new file mode 100644 index 000000000000..e93e5cae3246 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.de.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + Parameterbedingungen enthalten eine zyklische Abhängigkeit [{0}], welche die deterministische Auswertung verhindert. + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + Fehler beim Auswerten der Bedingung {0} für Parameter {1} (Bedingungstext: {2}, Auswertungsfehler: {3}) – Die Bedingung ist möglicherweise falsch formatiert, oder die referenzierten Parameter weisen weder Standard- noch explizite Werte auf. + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + Unerwarteter interner Fehler: Die topologische Sortierung der Parameterabhängigkeiten, die anscheinend keine zyklischen Abhängigkeiten aufweisen, kann nicht durchgeführt werden. + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + Parameterbedingungen enthalten eine zyklische Abhängigkeit [{0}]. Mit aktuellen Werten von Parametern ist es möglich, Parameter deterministisch auszuwerten, fahren Sie also fort. Die Vorlage sollte jedoch überprüft werden, da die Instanziierung mit unterschiedlichen Parametern zu Fehlern führen kann. + {0} is the dependency chain + + + '{0}' should not contain empty items + „{0}“ darf keine leeren Elemente enthalten. + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + Argument(e) wurden nicht angegeben. Es muss mindestens ein Argument angegeben werden. + + + + '{0}' does not contain valid items. + „{0}“ enthält keine gültigen Elemente. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + „{0}“ ist kein gültiger JSON-Code. + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + „{0}“ muss ein Array von Objekten sein. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + „{0}“ ist kein gültiges JSON-Array. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + „{0}“ ist keine gültige JSON-Zeichenfolge oder kein gültiges Array. + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + „{0}“ ist keine gültige Version oder kein gültiger Versionsbereich. + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + Überprüfen Sie die Einschränkungskonfiguration in „template.json“. + + + + Environment variables + Umgebungsvariablen + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + Es wird versucht, das Ergebnis der externen Auswertung von Parameterbedingungen für einen oder mehrere Parameter zu übergeben, für die in der Vorlage kein geeigneter Bedingungssatz festgelegt ist (IsEnabled- oder IsRequired-Attribute, die nicht mit einer Bedingung aufgefüllt sind), oder ein Fehler beim Übergeben der Bedingungsergebnisse für Parameter mit Bedingung(en) in der Vorlage. Problematische Parameter: {0}. + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + Es wird versucht, das Ergebnis der externen Auswertung von Parameterbedingungen für einen oder mehrere Parameter zu übergeben, für die in der Vorlage kein geeigneter Bedingungssatz festgelegt ist (IsEnabled- oder IsRequired-Attribute, die nicht mit einer Bedingung aufgefüllt sind), oder ein Fehler beim Übergeben der Bedingungsergebnisse für Parameter mit Bedingung(en) in der Vorlage. Problematische(r) Parameter: {0}. + + + + The folder {0} doesn't exist. + Der Ordner \"{0}\" ist nicht vorhanden. + + + + Check the constraint configuration in template.json. + Überprüfen Sie die Einschränkungskonfiguration in „template.json“. + + + + latest version + Aktuelle Version + small letters, string is used in the sentence + + + version {0} + Version {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + \"{0}\" kann von mehreren Installern installiert werden. Geben Sie den Namen des zu verwendenden Installers an. + + + + {0} is already installed. + \"{0}\" ist bereits installiert. + + + + {0} cannot be installed. + \"{0}\" kann nicht installiert werden. + + + + {0} is already installed, it will be replaced with {1}. + {0} ist bereits installiert, und wird durch {1} ersetzt. + + + + {0} was successfully uninstalled. + \"{0}\" wurde erfolgreich deinstalliert. + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + Fehler beim Lesen der Liste der installierten Vorlagenpakete unter {0}. Möglicherweise ist die Datei beschädigt. Überprüfen Sie diese Datei manuell und beheben Sie die Fehler in der JSON-Struktur oder entfernen Sie die Datei, um die Liste der installierten Pakete zu löschen und sie erneut zu installieren. Fehlerdetails: {1}. + + + + '{0}' does not have mandatory property '{1}'. + „{0}“ weist keine obligatorische Eigenschaft „{1}“ auf. + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + Das Ausführen einer Vorlage auf {0} (Version: {1}) wird nicht unterstützt. Unterstützte Hosts sind: {2}. + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + Vorlagen-Engine-Host + + + + Host defined parameters + Vom Host definierte Parameter + + + + Found package is vulnerable source: {0} + Das gefundene Paket ist eine anfällige Quelle: {0} + + + + Failed to load the NuGet source {0}. + Fehler beim Laden der NuGet-Quelle \"{0}\". + + + + Failed to load NuGet sources configured for the folder {0}. + Fehler beim Laden der für den Ordner \"{0}\" konfigurierten NuGet-Quellen. + + + + Failed to read package information from NuGet source {0}. + Fehler beim Lesen von Paketinformationen aus der NuGet-Quelle \"{0}\". + + + + File {0} already exists. + Die Datei \"{0}\" ist bereits vorhanden. + + + + No NuGet sources are defined or enabled. + Es sind keine NuGet-Quellen definiert oder aktiviert. + + + + The checked package {0} is vulnerable. + Das überprüfte Paket {0} ist anfällig. + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + Fehler beim Entfernen von {0} nach dem fehlerhaften Download. Entfernen Sie die Datei manuell, falls vorhanden. + + + + Failed to download {0} from NuGet feed {1}. + Fehler beim Herunterladen von \"{0}\" aus dem NuGet-Feed {1}. + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + Fehler beim Laden der NuGet-Quelle {0}: die Quelle ist ungültig. Sie wird bei der weiteren Verarbeitung übersprungen. + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + Die NuGet-Quellen {0} sind unsicher und werden nicht durchsucht. Wenn Sie diese Quellen für die Suche einschließen möchten, verwenden Sie "--force". + + + + {0} is not found in NuGet feeds {1}. + \"{0}\" wurde in NuGet-Feeds \"{1}\" nicht gefunden. + + + + Failed to copy package {0} to {1}. + Fehler beim Kopieren des Pakets \"{0}\" in {1}. + + + + Failed to read content of package {0}. + Der Inhalt des Pakets \"{0}\" konnte nicht gelesen werden. + + + + File {0} already exists. + Die Datei \"{0}\" ist bereits vorhanden. + + + + Failed to download {0} from {1}. + Fehler beim Herunterladen von \"{0}\" von \"{1}\". + + + + Failed to install the package {0}. +Details: {1}. + Fehler beim Installieren des Pakets \"{0}\". +Details: {1}. + + + + The install request {0} cannot be processed by installer {1}. + Die Installationsanforderung \"{0}\" kann vom Installer \"{1}\" nicht verarbeitet werden. + + + + The NuGet package {0} is invalid. + Das NuGet-Paket \"{0}\" ist ungültig. + + + + The configured NuGet sources are invalid: {0}. + Die konfigurierten NuGet-Quellen sind ungültig: {0}. + + + + No NuGet sources are configured. + Es sind keine NuGet-Quellen konfiguriert. + + + + The operation was cancelled. + Der Vorgang wurde abgebrochen. + + + + {0} was not found in NuGet feeds {1}. + \"{0}\" wurde in NuGet-Feeds \"{1}\" nicht gefunden. + + + + The package {0} is not supported by installer {1}. + Das Paket \"{0}\" wird vom Installer \"{1}\" nicht unterstützt. + + + + Failed to uninstall the package {0}. +Details: {1}. + Fehler beim Deinstallieren des Pakets \"{0}\". +Details: {1}. + + + + Failed to check the update for the package {0}. +Details: {1}. + Fehler beim Überprüfen des Updates für das Paket \"{0}\". +Details: {1}. + + + + The requested package {0} has vulnerabilities. + Das angeforderte Paket {0} weist Sicherheitsrisiken auf. + + + + The checked package {0} has vulnerabilities. + Das überprüfte Paket {0} weist Sicherheitsrisiken auf. + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + „{0}“ ist kein gültiger Betriebssystemname. Zulässige Werte sind: {1}. + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + Das Ausführen einer Vorlage auf {0} wird nicht unterstützt. Unterstützte Betriebssysteme: {1}. + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + Betriebssystem + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + Der Speicherort des Vorlagenpakets {0} wird nicht unterstützt oder ist nicht vorhanden. + + + + '{0}' is not a valid semver version. + „{0}“ ist keine gültige Serverversion. + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + Der Host hat mehrere ISdkInfoProvider-Komponenten ({0}) bereitgestellt, daher kann „SdkVersionConstraints“ nicht ordnungsgemäß initialisiert werden. + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + Der Host hat keine ISdkInfoProvider-Komponente bereitgestellt. „SdkVersionConstraint“ kann nicht ordnungsgemäß initialisiert werden. + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + Das Ausführen einer Vorlage für die aktuelle .NET SDK-Version ({0}) wird nicht unterstützt. Unterstützte Version(en): {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + .NET SDK-Version + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + Die Einschränkung „{0}“ konnte nicht für die Argumente „{1}“ ausgewertet werden. Details: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + Die Einschränkung „{0}“ konnte nicht initialisiert werden: {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + Die Einschränkung „{0}“ ist unbekannt. + {0} is constraint type + + + Could not load template. + Die Vorlage konnte nicht geladen werden. + + + + Failed to create template. +Details: {0} + Fehler beim Erstellen der Vorlage. +Details: {0}. + + + + Destructive changes detected. + Es wurden destruktive Änderungen erkannt. + + + + The template is invalid and cannot be instantiated. + Die Vorlage ist ungültig und kann nicht instanziiert werden. + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + Fehler beim Erstellen der Vorlage: Der Vorlagenname ist nicht angegeben. Bei der Vorlagenkonfiguration wird kein Standardname konfiguriert, der verwendet werden kann, wenn kein Name angegeben wird. Geben Sie den Namen für die Vorlage an, wenn Sie einen Standardnamen in der Vorlagenkonfiguration instanziieren oder konfigurieren. + + + + Failed to load host data in {0} at {1}. + Fehler beim Laden von Host Daten in {0} bei {1}. + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + Fehler beim Abrufen des Pakets mit dem Bezeichner "{0}". + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + Fehler beim Abrufen von Vorlagenpaketen vom Anbieter “{0}”. +Details: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + Fehler beim Scannen von “{0}“. +Details: {1}. + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + Fehler beim Speichern des Vorlagencaches. Details: {0} +Der Vorlagencache wird bei der nächsten Ausführung neu erstellt. + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +Die folgenden Vorlagen verwenden die gleiche Identität "{0}": +{1} +Die Vorlage aus "{2}" wird verwendet. Deinstallieren Sie die in Konflikt stehenden Vorlagenpakete, um diesen Konflikt zu beheben. + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {0} "{1}" von "{2}" + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + Die Vorlage {0} weist die folgenden Überprüfungsfehler auf: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + Die Vorlage {0} hat die folgenden Validierungsmeldungen: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + Vorlage {0} konnte nicht geladen werden: Die Vorlage ist ungültig. + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + Lokalisierung „{0}“ der Vorlage {1} konnte nicht geladen werden: Die Lokalisierungsdatei ist ungültig. Die Lokalisierung wird übersprungen. + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + Die Vorlage {0} weist die folgenden Überprüfungsfehler in Lokalisierung „{1}“ auf: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + Die Vorlage {0} enthält die folgenden Validierungsmeldungen in Lokalisierung „{1}“: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + Die Vorlage {0} enthält die folgenden Validierungswarnungen in der Lokalisierung „{1}“: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + Die Vorlage {0} weist die folgenden Überprüfungswarnungen auf: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + Der Host hat mehrere IWorkloadsInfoProvider-Komponenten ({0}) bereitgestellt, daher kann „WorkloadConstraint“ nicht ordnungsgemäß initialisiert werden. + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + Vom Host wurde keine IWorkloadsInfoProvider-Komponente bereitgestellt. „WorkloadConstraint“ kann nicht ordnungsgemäß initialisiert werden. + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + Das Ausführen einer Vorlage wird nicht unterstützt. Erforderliche optionale Workloads sind nicht installiert. Unterstützte Workloads: {0}. Derzeit installierte optionale Workloads: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + Arbeitsauslastung + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + Die vom Host bereitgestellte Komponente „IWorkloadsInfoProvider“ hat einige duplizierte Workloads bereitgestellt (Duplikate: {0}). Duplikate werden übersprungen. + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.es.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.es.xlf new file mode 100644 index 000000000000..6bf1b24dc97b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.es.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + Las condiciones del parámetro contienen una dependencia cíclica: [{0}] que impide la evaluación determinista. + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + No se pudo evaluar la condición {0} del parámetro {1} (texto de la condición: {2}, error de evaluación: {3}): la condición podría estar mal formada o los parámetros referenciados no tienen valores predeterminados ni explícitos. + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + Error interno inesperado: no se puede realizar un orden topológico de dependencias de parámetros que no parecen tener dependencias cíclicas. + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + Las condiciones del parámetro contienen una dependencia cíclica: [{0}]. Con los valores actuales de los parámetros, es posible evaluar los parámetros de forma determinista, por lo que es posible continuar. Sin embargo, la plantilla debe revisarse porque la creación de una instancia con diferentes parámetros puede dar lugar a un error. + {0} is the dependency chain + + + '{0}' should not contain empty items + '{0}' no debe contener elementos vacíos + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + No se especificaron argumentos. Debe especificarse al menos un argumento. + + + + '{0}' does not contain valid items. + '{0}' no contiene elementos válidos. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + '{0}' no es un JSON válido. + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + '{0}' debe ser una matriz de objetos. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + '{0}' no es una matriz JSON válida. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + '{0}' no es una cadena o matriz JSON válida. + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + '{0}' no es una versión o un rango de versiones válidos. + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + Compruebe la configuración de restricciones en template.json. + + + + Environment variables + Variables de entorno + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + Se ha intentado pasar el resultado de la evaluación externa de condiciones de parámetros para los parámetros que no tienen establecida una condición adecuada en la plantilla (atributos IsEnabled o IsRequired no rellenados con condición), o ha habido un error al pasar los resultados de la condición para los parámetros con condiciones en la plantilla. Parámetros incorrectos: {0}. + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + Se ha intentado pasar el resultado de la evaluación externa de condiciones de parámetros para los parámetros que no tienen establecida una condición adecuada en la plantilla (atributos IsEnabled o IsRequired no rellenados con condición), o ha habido un error al pasar los resultados de la condición para los parámetros con condiciones en la plantilla. Parámetro(s) incorrecto(s): {0}. + + + + The folder {0} doesn't exist. + La carpeta de origen {0} no existe. + + + + Check the constraint configuration in template.json. + Compruebe la configuración de restricciones en template.json. + + + + latest version + la versión más reciente + small letters, string is used in the sentence + + + version {0} + versión {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + Se puede instalar {0} con varios instaladores. Especifique el nombre del instalador que va a usar. + + + + {0} is already installed. + {0} ya se ha instalado. + + + + {0} cannot be installed. + No se puede instalar {0}. + + + + {0} is already installed, it will be replaced with {1}. + {0} ya está instalado, se reemplazará por {1}. + + + + {0} was successfully uninstalled. + {0} se desinstaló correctamente. + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + No se pudo leer la lista de paquetes de plantillas instalados en {0}. Puede deberse a que el archivo está dañado. Revise este archivo manualmente y corrija los errores en la estructura JSON o quite el archivo para borrar la lista de paquetes instalados en la lista y volver a instalarlos. Detalles del error: {1}. + + + + '{0}' does not have mandatory property '{1}'. + '{0}' no tiene la propiedad obligatoria '{1}'. + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + No se admite la ejecución de la plantilla en {0} (versión: {1}), los hosts admitidos son: {2}. + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + Host del motor de plantillas + + + + Host defined parameters + Parámetros definidos por el host + + + + Found package is vulnerable source: {0} + El paquete encontrado es de un origen vulnerable: {0} + + + + Failed to load the NuGet source {0}. + No se pudo cargar el origen de NuGet {0} + + + + Failed to load NuGet sources configured for the folder {0}. + No se pudieron cargar los orígenes de NuGet configurados para la carpeta {0}. + + + + Failed to read package information from NuGet source {0}. + No se pudo leer la información del paquete del origen de NuGet {0}. + + + + File {0} already exists. + El archivo {0} ya existe. + + + + No NuGet sources are defined or enabled. + No hay ninguna fuente de NuGet definida o habilitada. + + + + The checked package {0} is vulnerable. + El paquete comprobado {0} es vulnerable. + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + No se pudo quitar {0} después del error de descarga. Quite el archivo manualmente si existe. + + + + Failed to download {0} from NuGet feed {1}. + No se pudo descargar {0} de la fuente de NuGet {1} + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + No se pudo cargar el origen de NuGet {0}: el origen no es válido. Se omitirá en un proceso posterior. + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + Los orígenes de NuGet {0} no son seguros y no se buscarán. Si desea incluir esos orígenes para la búsqueda, use --force. + + + + {0} is not found in NuGet feeds {1}. + No se encuentra {0} en las fuentes de NuGet {1}. + + + + Failed to copy package {0} to {1}. + No se pudo copiar el paquete {0} a {1}. + + + + Failed to read content of package {0}. + No se pudo leer el contenido del paquete {0} + + + + File {0} already exists. + El archivo {0} ya existe. + + + + Failed to download {0} from {1}. + No se pudo descargar {0} desde {1}. + + + + Failed to install the package {0}. +Details: {1}. + No se pudo instalar el paquete {0}. +Detalles: {1}. + + + + The install request {0} cannot be processed by installer {1}. + El instalador {1} no puede procesar la solicitud de instalación {0}. + + + + The NuGet package {0} is invalid. + El paquete NuGet {0} no es válido. + + + + The configured NuGet sources are invalid: {0}. + Los orígenes de NuGet configurados no son válidos: {0}. + + + + No NuGet sources are configured. + No hay ningún origen NuGet configurado. + + + + The operation was cancelled. + Se canceló la operación. + + + + {0} was not found in NuGet feeds {1}. + No se encontró {0} en las fuentes de NuGet {1}. + + + + The package {0} is not supported by installer {1}. + El instalador {1} no admite el paquete {0}. + + + + Failed to uninstall the package {0}. +Details: {1}. + No se pudo desinstalar el paquete {0}. +Detalles: {1}. + + + + Failed to check the update for the package {0}. +Details: {1}. + No se pudo comprobar la actualización del paquete {0}. +Detalles: {1}. + + + + The requested package {0} has vulnerabilities. + El paquete solicitado {0} tiene vulnerabilidades. + + + + The checked package {0} has vulnerabilities. + El paquete comprobado {0} tiene vulnerabilidades. + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + '{0}' no es un nombre de sistema operativo válido. Los valores permitidos son: {1}. + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + No se admite la ejecución de una plantilla en {0}; el sistema operativo admitido son: {1}. + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + Sistema operativo + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + La ubicación del paquete de plantilla {0} no se admite o no existe. + + + + '{0}' is not a valid semver version. + '{0}' no es una versión de SemVer válida. + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + Hay varios componentes 'ISdkInfoProvider' proporcionados por el host ({0}), por lo que 'SdkVersionConstraint' no se puede inicializar correctamente. + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + No hay ningún componente 'ISdkInfoProvider' proporcionado por el host. 'SdkVersionConstraint' no se puede inicializar correctamente. + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + No se admite la ejecución de la plantilla en la versión actual del SDK de .NET ({0}). Versiones admitidas: {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + Versión del SDK de .NET + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + No se pudo evaluar la restricción “{0}” para los argumentos “{1}”, detalles: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + No se pudo inicializar la restricción “{0}”: {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + La restricción “{0}” es desconocida. + {0} is constraint type + + + Could not load template. + No se pudo cargar la plantilla. + + + + Failed to create template. +Details: {0} + No se pudo crear la plantilla. +Detalles: {0}. + + + + Destructive changes detected. + Cambios destructivos detectados. + + + + The template is invalid and cannot be instantiated. + La plantilla no es válida y no se puede crear una instancia. + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + Error al crear la plantilla: no se especificó el nombre de la plantilla. La configuración de la plantilla no configura un nombre predeterminado que se pueda usar cuando no se especifique el nombre. Especifique el nombre de la plantilla al crear una instancia o configurar un nombre predeterminado en la configuración de la plantilla. + + + + Failed to load host data in {0} at {1}. + No se pudieron cargar los datos de host en {0} en {1}. + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + Ha ocurrido un error al recuperar el paquete con el identificador "{0}". + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + No se pudieron recuperar los paquetes de plantillas del proveedor '{0}'. +Detalles: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + Error al examinar {0}. +Detalles: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + No se pudo almacenar la caché de las plantillas. Detalles: {0} +La caché de las plantillas se volverá a crear en la siguiente ejecución. + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +Las siguientes plantillas usan la misma identidad "{0}": +{1} +Se usará la plantilla de "{2}". Para resolver este conflicto, desinstale los paquetes de plantillas en conflicto. + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {0} "{1}" de "{2}" + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + La plantilla {0} tiene los siguientes errores de validación: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + La plantilla {0} tiene los siguientes mensajes de validación: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + No se pudo cargar la plantilla {0}: la plantilla no es válida. + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + No se pudo cargar la localización “{0}” de la plantilla {1}: el archivo de localización no es válido. Se omitirá la localización. + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + La plantilla {0} tiene los siguientes errores de validación en la localización "{1}": + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + La plantilla {0} tiene los siguientes mensajes de validación en la localización "{1}": + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + La plantilla {0} tiene las siguientes advertencias de validación en la localización "{1}": + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + La plantilla {0} tiene las siguientes advertencias de validación: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + Hay varios componentes 'IWorkloadsInfoProvider' proporcionados por el host ({0}), por lo que 'WorkloadConstraint' no se puede inicializar correctamente. + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + No hay ningún componente 'IWorkloadsInfoProvider' proporcionado por el host. 'WorkloadConstraint' no se puede inicializar correctamente. + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + No se admite la ejecución de la plantilla. Las cargas de trabajo opcionales necesarias no están instaladas. Cargas de trabajo admitidas: {0}. Cargas de trabajo opcionales instaladas actualmente: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + Carga de trabajo + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + El componente 'IWorkloadsInfoProvider' proporcionado por el host proporcionó algunas cargas de trabajo duplicadas (duplicados: {0}). Se omitirán los duplicados. + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.fr.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.fr.xlf new file mode 100644 index 000000000000..969a2fd2053f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.fr.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + Les conditions de paramètre contiennent une dépendance cyclique : [{0}] qui empêche l'évaluation déterministe. + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + Nous n’avons pas pu évaluer la condition {0} sur le paramètre {1} (texte de condition : {2}, erreur d’évaluation : {3}) - la condition est peut-être incorrecte ou les paramètres référencés n’ont pas de valeurs par défaut ni explicites. + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + Erreur interne inattendue – impossible d'effectuer un tri topologique des dépendances de paramètres qui ne semblent pas avoir de dépendances cycliques. + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + Les conditions de paramètre contiennent une dépendance cyclique : [{0}]. Avec les valeurs actuelles des paramètres, il est possible d'évaluer les paramètres de manière déterministe – donc aller plus loin. Cependant, le modèle doit être revu car l'instanciation avec des paramètres différents peut entraîner des erreurs. + {0} is the dependency chain + + + '{0}' should not contain empty items + « {0} » ne doit pas contenir d’éléments vides + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + Le ou les arguments n’ont pas été spécifiés. Au moins un argument doit être spécifié. + + + + '{0}' does not contain valid items. + '{0}' ne contient pas d’éléments valides. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + '{0}' n'est pas une JSON valide. + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + '{0}' doit être un groupe d’objets. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + '{0}' n’est pas un groupe JSON valide. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + '{0}' n’est pas un tableau ou un groupe JSON valide. + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + '{0}' n’est pas une version valide ou une plage de version. + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + Vérifiez la configuration de contrainte dans le modèle .json. + + + + Environment variables + Variables d'environnement + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + Tentative de réussite du résultat de l'évaluation externe des conditions de paramètres pour le ou les paramètres dont la condition appropriée n'est pas définie dans le modèle (attributs IsEnabled ou IsRequired non renseignés avec la condition) ou échec de la transmission des résultats de la condition pour les paramètres avec condition(s) dans modèle. Paramètres incriminés : {0}. + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + Tentative de réussite du résultat de l'évaluation externe des conditions de paramètres pour le ou les paramètres dont la condition appropriée n'est pas définie dans le modèle (attributs IsEnabled ou IsRequired non renseignés avec la condition) ou échec de la transmission des résultats de la condition pour les paramètres avec condition(s) dans modèle. Paramètre(s) incriminé(s) : {0}. + + + + The folder {0} doesn't exist. + Le dossier {0} n’existe pas. + + + + Check the constraint configuration in template.json. + Vérifiez la configuration de contrainte dans le modèle .json. + + + + latest version + Dernière version + small letters, string is used in the sentence + + + version {0} + version {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + {0} peut être installé par plusieurs programmes d’installation. Spécifiez le nom du programme d’installation à utiliser. + + + + {0} is already installed. + {0} est déjà installé. + + + + {0} cannot be installed. + {0} ne peut pas être installé. + + + + {0} is already installed, it will be replaced with {1}. + {0} est déjà installé, il sera remplacé par {1}. + + + + {0} was successfully uninstalled. + {0} a été désinstallé. + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + Impossible de lire la liste des packages de modèles installés sur {0}. Cela peut être dû au fait que le fichier est corrompu. Veuillez examiner ce fichier manuellement et corriger les erreurs dans la structure JSON, ou supprimer le fichier pour effacer la liste des packages installés et les réinstaller à nouveau. Détails de l'erreur : {1}. + + + + '{0}' does not have mandatory property '{1}'. + '{0}' n’a pas de propriété obligatoire '{1}'. + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + L’exécution du modèle sur {0} (version : {1}) n’est pas prise en charge, les hôtes pris en charge sont : {2}. + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + Hôte du moteur de modèle + + + + Host defined parameters + Paramètres définis par l’hôte + + + + Found package is vulnerable source: {0} + Le package trouvé est une source vulnérable : {0} + + + + Failed to load the NuGet source {0}. + Échec du chargement de la source NuGet {0}. + + + + Failed to load NuGet sources configured for the folder {0}. + Échec du chargement des sources NuGet configurées pour le dossier {0} + + + + Failed to read package information from NuGet source {0}. + Échec de la lecture des informations de package à partir de la source NuGet {0}. + + + + File {0} already exists. + Le fichier {0} existe déjà. + + + + No NuGet sources are defined or enabled. + Aucune source NuGet n’est définie ou activée. + + + + The checked package {0} is vulnerable. + Le package {0} vérifié est vulnérable. + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + Échec de la suppression de {0} après l’échec du téléchargement. Supprimez le fichier manuellement s’il existe. + + + + Failed to download {0} from NuGet feed {1}. + Échec du téléchargement de {0} à partir du flux NuGet {1}. + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + Échec du chargement de la source NuGet {0} : la source n’est pas valide. Il sera ignoré en cours de traitement. + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + Les sources NuGet {0} ne sont pas sécurisées et ne seront pas recherchées. Si vous souhaitez inclure ces sources pour la recherche, utilisez --force. + + + + {0} is not found in NuGet feeds {1}. + {0} est introuvable dans les flux NuGet {1}. + + + + Failed to copy package {0} to {1}. + Échec de la copie du package {0} sur {1}. + + + + Failed to read content of package {0}. + Échec de la lecture du contenu du package {0}. + + + + File {0} already exists. + Le fichier {0} existe déjà. + + + + Failed to download {0} from {1}. + Échec du téléchargement de {0} à partir de {1}. + + + + Failed to install the package {0}. +Details: {1}. + Échec de l’installation du package {0}. +Détails : {1}. + + + + The install request {0} cannot be processed by installer {1}. + La demande d’installation {0} ne peut pas être traitée par le programme d’installation {1}. + + + + The NuGet package {0} is invalid. + Le package NuGet {0} est non valide. + + + + The configured NuGet sources are invalid: {0}. + Les sources NuGet configurées sont non valides : {0}. + + + + No NuGet sources are configured. + Aucune source NuGet n’est configurée. + + + + The operation was cancelled. + L'opération a été annulée. + + + + {0} was not found in NuGet feeds {1}. + {0} est introuvable dans les flux NuGet {1}. + + + + The package {0} is not supported by installer {1}. + Le package {0} n’est pas pris en charge par le programme d’installation {1}. + + + + Failed to uninstall the package {0}. +Details: {1}. + Échec de la désinstallation du package {0}. +Détails : {1}. + + + + Failed to check the update for the package {0}. +Details: {1}. + Échec de la vérification de la mise à jour du package {0}. +Détails : {1}. + + + + The requested package {0} has vulnerabilities. + Le package {0} demandé présente des vulnérabilités. + + + + The checked package {0} has vulnerabilities. + Le package {0} vérifié présente des vulnérabilités. + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + '{0}' n’est pas un nom de système d’exploitation valide. Les valeurs autorisées sont : {1}. + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + L’exécution du modèle sur {0} n’est pas prise en charge, le ou les systèmes d’exploitation pris en charge sont : {1}. + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + Système d'exploitation + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + L’emplacement du package de modèles {0} n’est pas pris en charge ou n’existe pas. + + + + '{0}' is not a valid semver version. + « {0} » n’est pas une version SemVer valide. + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + Plusieurs composants « ISdkInfoProvider » fournis par l’hôte ({0}), par conséquent, « SdkVersionConstraint » ne peut pas être initialisé correctement. + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + Aucun composant « ISdkInfoProvider » fourni par l’hôte. « SdkVersionConstraint » ne peut pas être initialisé correctement. + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + L’exécution du modèle sur la version actuelle du SDK .NET ({0}) n’est pas prise en charge. Versions prises en charge : {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + Version du Kit de développement logiciel (SDK) .NET + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + La contrainte '{0}' n’a pas pu être évaluée pour les arguments '{1} ', détails : {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + La contrainte '{0}' n’a pas pu s'initialiser : {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + La contrainte '{0}' est inconnue. + {0} is constraint type + + + Could not load template. + Impossible de charger le modèle. + + + + Failed to create template. +Details: {0} + Échec de la création du modèle. +Détails : {0}. + + + + Destructive changes detected. + Modifications destructrices détectées. + + + + The template is invalid and cannot be instantiated. + Le modèle n'est pas valide et ne peut pas être instancié. + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + Échec de la création du modèle : le nom du modèle n’est pas spécifié. La configuration du modèle ne configure pas un nom par défaut qui peut être utilisé lorsque le nom n’est pas spécifié. Spécifiez le nom du modèle lors de l’instanciation ou configurez un nom par défaut dans la configuration du modèle. + + + + Failed to load host data in {0} at {1}. + Échec du chargement des données de l’hôte dans la {0} à {1}. + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + Nous n’avons pas pu récupérer le package avec l’identificateur '{0}'. + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + Échec de la récupération des packages de modèles à partir du fournisseur '{0}'. +Détails : {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + Échec de l’analyse de {0}. +Détails : {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + Échec du stockage du cache de modèles. Détails : {0} +Le cache de modèles sera recréé lors de la prochaine exécution. + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +Les modèles suivants utilisent la même identité '{0}': +{1} +Le modèle de '{2}' sera utilisé. Pour résoudre ce conflit, désinstallez les packages de modèles en conflit. + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {0} «{1}» de «{2}» + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + Le modèle {0} comporte les erreurs de validation suivantes : + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + Le modèle {0} contient les messages de validation suivants : + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + Échec du chargement du modèle {0} : le modèle n'est pas valide. + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + Échec du chargement de la localisation '{0}' du modèle {1} : le fichier de localisation n'est pas valide. La localisation sera ignorée. + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + Le modèle {0} comporte les erreurs de validation suivantes dans la localisation "{1}" : + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + Le modèle {0} contient les messages de validation suivants dans la localisation '{1}' : + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + Le modèle {0} comporte les avertissements de validation suivants dans la localisation "{1}" : + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + Le modèle {0} contient les avertissements de validation suivants : + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + Plusieurs composants « IWorkloadsInfoProvider » fournis par l’hôte ({0}). « WorkloadConstraint » ne peut donc pas être initialisé correctement. + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + Aucun composant « IWorkloadsInfoProvider » fourni par l’hôte. « WorkloadConstraint » ne peut pas être correctement initialisé. + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + L’exécution du modèle n’est pas prise en charge. Les charges de travail facultatives requises ne sont pas installées. Charges de travail prises en charge : {0}. Charges de travail facultatives actuellement installées : {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + Charge de travail + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + Le composant « IWorkloadsInfoProvider » fourni par l’hôte a fourni des charges de travail dupliquées (doublons : {0}). Les doublons seront ignorés. + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.it.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.it.xlf new file mode 100644 index 000000000000..5f3c46424ec2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.it.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + Le condizioni del parametro contengono una dipendenza ciclica: [{0}] che impedisce la valutazione deterministica. + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + Non è stato possibile valutare la condizione {0} nel parametro {1} (testo della condizione: {2}, errore di valutazione: {3}). La condizione potrebbe essere in formato non corretto o i parametri a cui viene fatto riferimento non hanno valori predefiniti né espliciti. + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + Errore interno imprevisto. Non è possibile eseguire l'ordinamento topologico delle dipendenze dei parametri che non sembrano avere dipendenze cicliche. + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + Le condizioni dei parametri contengono una dipendenza ciclica: [{0}]. Con i valori correnti dei parametri è possibile valutare deterministicamente i parametri, quindi continuare. È tuttavia necessario verificare il modello perché la creazione di un'istanza con parametri diversi può causare errori. + {0} is the dependency chain + + + '{0}' should not contain empty items + '{0}' non deve contenere elementi vuoti + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + Argomenti non specificati. È necessario specificare almeno un argomento. + + + + '{0}' does not contain valid items. + '{0}' non contiene elementi validi. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + '{0}' non è un codice JSON valido. + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + '{0}' deve essere una matrice di oggetti. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + '{0}' non è una matrice JSON valida. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + '{0}' non è una matrice o una stringa JSON valida. + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + '{0}' non è un intervallo di versioni o una versione validi. + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + Controllare la configurazione del vincolo in template.json. + + + + Environment variables + Variabili di ambiente + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + Tentativo di passare il risultato della valutazione esterna delle condizioni dei parametri per i parametri per i quali non è impostata una condizione appropriata nel modello (attributi IsEnabled o IsRequired non popolati con la condizione) oppure un errore durante il passaggio dei risultati della condizione per i parametri con condizioni nel modello. Parametri incriminati: {0}. + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + Tentativo di passare il risultato della valutazione esterna delle condizioni dei parametri per i parametri per i quali non è impostata una condizione appropriata nel modello (attributi IsEnabled o IsRequired non popolati con la condizione) oppure un errore durante il passaggio dei risultati della condizione per i parametri con condizioni nel modello. Parametri incriminati: {0}. + + + + The folder {0} doesn't exist. + La cartella {0} non esiste. + + + + Check the constraint configuration in template.json. + Controllare la configurazione del vincolo in template.json. + + + + latest version + ultima versione + small letters, string is used in the sentence + + + version {0} + versione {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + {0} può essere installato da diversi programmi di installazione. Specificare il nome del programma di installazione da utilizzare. + + + + {0} is already installed. + {0} è già installato. + + + + {0} cannot be installed. + {0} non può essere installato. + + + + {0} is already installed, it will be replaced with {1}. + {0} è già installato, verrà sostituito con {1}. + + + + {0} was successfully uninstalled. + {0} è stato disinstallato. + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + Non è stato possibile leggere l'elenco dei pacchetti di modelli installati in {0}. È possibile che il file sia danneggiato. Esaminare manualmente il file e correggere gli errori nella struttura JSON oppure rimuovere il file per cancellare l'elenco dei pacchetti installati e reinstallarli. Dettagli dell'errore: {1}. + + + + '{0}' does not have mandatory property '{1}'. + '{0}' non ha la proprietà obbligatoria '{1}'. + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + L'esecuzione del modello in {0} (versione: {1}) non è supportata. Gli host supportati sono: {2}. + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + Host del motore del modello + + + + Host defined parameters + Parametri definiti dall'host + + + + Found package is vulnerable source: {0} + Il pacchetto trovato è un'origine vulnerabile: {0} + + + + Failed to load the NuGet source {0}. + Non è stato possibile caricare l'origine NuGet {0}. + + + + Failed to load NuGet sources configured for the folder {0}. + Non è stato possibile caricare le origini NuGet configurate per la cartella {0}. + + + + Failed to read package information from NuGet source {0}. + Non è stato possibile leggere le informazioni sul pacchetto dall'origine NuGet {0}. + + + + File {0} already exists. + Il file {0} esiste già. + + + + No NuGet sources are defined or enabled. + Nessuna origine NuGet definita o abilitata. + + + + The checked package {0} is vulnerable. + Il pacchetto controllato {0} è vulnerabile. + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + Non è stato possibile rimuovere {0} dopo il download non riuscito. Rimuovere manualmente il file se esiste. + + + + Failed to download {0} from NuGet feed {1}. + Non è stato possibile scaricare {0} dal feed NuGet {1}. + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + Non è stato possibile caricare l'origine NuGet {0}: l'origine non è valida. Verrà ignorata in elaborazioni successive. + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + Le origini NuGet {0} non sono sicure e non verranno eseguite ricerche. Per includere tali origini per la ricerca, usare --force. + + + + {0} is not found in NuGet feeds {1}. + {0} non è stato trovato nei feed NuGet {1}. + + + + Failed to copy package {0} to {1}. + Non è stato possibile copiare il pacchetto {0} in {1}. + + + + Failed to read content of package {0}. + Non è stato possibile leggere il contenuto del pacchetto {0}. + + + + File {0} already exists. + Il file {0} esiste già. + + + + Failed to download {0} from {1}. + Non è stato possibile scaricare {0} da {1}. + + + + Failed to install the package {0}. +Details: {1}. + Non è stato possibile installare il pacchetto {0}. +Dettagli: {1}. + + + + The install request {0} cannot be processed by installer {1}. + La richiesta di installazione {0} non può essere elaborata dal programma di installazione di {1}. + + + + The NuGet package {0} is invalid. + Il pacchetto NuGet {0} non è valido. + + + + The configured NuGet sources are invalid: {0}. + Le origini NuGet configurate non sono valide: {0}. + + + + No NuGet sources are configured. + Nessuna origine NuGet configurata. + + + + The operation was cancelled. + Operazione annullata. + + + + {0} was not found in NuGet feeds {1}. + {0} non è stato trovato nei feed NuGet {1}. + + + + The package {0} is not supported by installer {1}. + Il pacchetto {0} non è supportato dal programma di installazione {1}. + + + + Failed to uninstall the package {0}. +Details: {1}. + Non è stato possibile disinstallare il pacchetto {0}. +Dettagli: {1}. + + + + Failed to check the update for the package {0}. +Details: {1}. + Non è possibile verificare la presenza dell'aggiornamento per il pacchetto {0}. +Dettagli: {1}. + + + + The requested package {0} has vulnerabilities. + Il pacchetto richiesto {0} presenta vulnerabilità. + + + + The checked package {0} has vulnerabilities. + Il pacchetto controllato {0} presenta vulnerabilità. + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + '{0}' non è un nome di sistema operativo valido. I valori consentiti sono: {1}. + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + L'esecuzione del modello in {0}non è supportata. Gli host supportati sono: {1}. + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + Sistema operativo + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + Il percorso {0} del pacchetto del modello non è supportato oppure non esiste. + + + + '{0}' is not a valid semver version. + '{0}' non è una versione semver valida. + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + Non è possibile inizializzare correttamente 'SdkVersionConstraint' perché sono stati forniti più componenti 'ISdkInfoProvider' dall'host ({0}). + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + Nessun componente 'ISdkInfoProvider' fornito dall'host. 'SdkVersionConstraint' non può essere inizializzato correttamente. + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + L'esecuzione del modello nella versione corrente di .NET SDK ({0}) non è supportata. Versioni supportate: {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + Versione .NET SDK + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + Impossibile valutare il vincolo '{0}' per gli argomenti '{1}', dettagli: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + Impossibile inizializzare il vincolo '{0}': {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + Vincolo '{0}' sconosciuto. + {0} is constraint type + + + Could not load template. + Non è stato possibile caricare il modello. + + + + Failed to create template. +Details: {0} + Non è stato possibile creare il modello. +Dettagli: {0}. + + + + Destructive changes detected. + Rilevate modifiche distruttive. + + + + The template is invalid and cannot be instantiated. + Il modello non è valido e non è possibile crearne un'istanza. + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + Non è stato possibile creare il modello: il nome del modello non è specificato. La configurazione del modello non configura un nome predefinito che può essere utilizzato quando il nome non è specificato. Specificare il nome del modello durante la creazione di un'istanza o la configurazione di un nome predefinito nella configurazione del modello. + + + + Failed to load host data in {0} at {1}. + Non è stato possibile caricare i dati dell'host in {0} in {1}. + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + Non è stato possibile recuperare il pacchetto con identificatore '{0}'. + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + Non è stato possibile recuperare i pacchetti di modelli dal provider '{0}'. +Dettagli: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + Non è stato possibile analizzare {0}. +Dettagli: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + Non è stato possibile archiviare la cache dei modelli. Dettagli: {0} +La cache dei modelli verrà ricreata alla prossima esecuzione. + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +I modelli seguenti usano la stessa identità '{0}': +{1} +Verrà utilizzato il modello di '{2}'. Per risolvere il conflitto, disinstallare i pacchetti di modelli in conflitto. + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {0} '{1}' da '{2}' + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + Il modello {0} presenta gli errori di convalida seguenti: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + Il modello {0} contiene i messaggi di convalida seguenti: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + Impossibile caricare il modello {0}: il modello non è valido. + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + Impossibile caricare la localizzazione '{0}' del modello {1}: il file di localizzazione non è valido. La localizzazione verrà ignorata. + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + Il modello {0} presenta gli errori di convalida seguenti nella localizzazione '{1}': + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + Il modello {0} presenta i messaggi di convalida seguenti nella localizzazione '{1}': + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + Il modello {0} presenta le avvertenze di convalida seguenti nella localizzazione '{1}': + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + Il modello {0} contiene gli avvisi di convalida seguenti: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + Più componenti 'IWorkloadsInfoProvider' forniti dall'host ({0}), pertanto 'WorkloadConstraint' non può essere inizializzato correttamente. + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + Nessun componente 'IWorkloadsInfoProvider' fornito dall'host. 'WorkloadConstraint' non può essere inizializzato correttamente. + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + L'esecuzione del modello non è supportata. I carichi di lavoro facoltativi necessari non sono installati. Carichi di lavoro supportati: {0}. Carichi di lavoro facoltativi attualmente installati: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + Carico di lavoro + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + Il componente 'IWorkloadsInfoProvider' fornito dall'host ha fornito alcuni carichi di lavoro duplicati (duplicati: {0}). I duplicati verranno ignorati. + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.ja.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.ja.xlf new file mode 100644 index 000000000000..0c8438184776 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.ja.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + パラメーター条件には、循環依存関係 [{0}] が含まれており、これが決定論的評価を妨げています。 + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + パラメーター {1} の条件 {0} を評価できませんでした (条件テキスト: {2}、 評価エラー: {3}) - 条件の形式に誤りがあるか、参照されたパラメーターに規定値も明示的な値もない可能性があります。 + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + 予期しない内部エラー - 循環依存関係を持っていないように見えるパラメーター依存関係のトポロジカルソートを実行できません。 + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + パラメーター条件に循環依存関係が含まれています: [{0}]。パラメーターの現在の値を使用すると、パラメーターを決定論的に評価できるため、続行します。ただし、異なるパラメーターを持つインスタンス化によってエラーが発生する可能性があるため、テンプレートを確認する必要があります。 + {0} is the dependency chain + + + '{0}' should not contain empty items + '{0}' に空の項目を含めることはできません + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + 引数が指定されていません。少なくとも 1 つの引数を指定する必要があります。 + + + + '{0}' does not contain valid items. + '{0}' には有効な項目が含まれていません。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + '{0}' は有効な JSON ではありません。 + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + '{0}' はオブジェクトの配列である必要があります。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + '{0}' は有効な JSON 配列ではありません。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + '{0}' は有効な JSON 文字列または配列ではありません。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + '{0}' は有効なバージョンまたはバージョンの範囲ではありません。 + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + template.json の制約の構成を確認してください。 + + + + Environment variables + 環境変数 + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + テンプレートに適切な条件が設定されていない (IsEnabled 属性または IsRequired 属性に条件が設定されていない) パラメーターのパラメーター条件の外部評価の結果を渡そうとしたか、テンプレートに条件があるパラメーターの条件の結果を渡せませんでした。問題のあるパラメーター: {0}。 + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + テンプレートに適切な条件が設定されていない (IsEnabled 属性または IsRequired 属性に条件が設定されていない) パラメーターのパラメーター条件の外部評価の結果を渡そうとしたか、テンプレートに条件があるパラメーターの条件の結果を渡せませんでした。問題のあるパラメーター: {0}。 + + + + The folder {0} doesn't exist. + フォルダー {0} が存在しません。 + + + + Check the constraint configuration in template.json. + template.json の制約の構成を確認してください。 + + + + latest version + 最新バージョン + small letters, string is used in the sentence + + + version {0} + バージョン {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + {0} は複数のインストーラーでインストールできます。使用するインストーラー名を指定します。 + + + + {0} is already installed. + {0} は既にインストールされています。 + + + + {0} cannot be installed. + {0} をインストールできません。 + + + + {0} is already installed, it will be replaced with {1}. + {0} は既にインストールされているため、 {1} に置き換えられます。 + + + + {0} was successfully uninstalled. + {0} が正常にアンインストールされました。 + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + {0}でインストールされているテンプレート パッケージの一覧を読み取ることができませんでした。ファイルが破損していることが原因である可能性があります。このファイルを手動で確認し、JSON 構造のエラーを修正するか、ファイルを削除してインストール済みパッケージの一覧を消去して再インストールしてください。エラーの詳細: {1}。 + + + + '{0}' does not have mandatory property '{1}'. + '{0}' には必須のプロパティ '{1}' がありません。 + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + {0} (バージョン: {1}) でのテンプレートの実行はサポートされていません。サポートされているホストは {2} です。 + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + テンプレート エンジン ホスト + + + + Host defined parameters + ホスト定義パラメーター + + + + Found package is vulnerable source: {0} + 見つかったパッケージは脆弱なソースです: {0} + + + + Failed to load the NuGet source {0}. + NuGet ソース {0} の読み込みに失敗しました。 + + + + Failed to load NuGet sources configured for the folder {0}. + フォルダー {0} に対して構成された NuGet ソースを読み込むことができませんでした。 + + + + Failed to read package information from NuGet source {0}. + NuGet ソース {0} からパッケージ情報を読み取れませんでした。 + + + + File {0} already exists. + ファイル {0} は既に存在します。 + + + + No NuGet sources are defined or enabled. + NuGet ソースが定義されていないか、有効になっていません。 + + + + The checked package {0} is vulnerable. + チェックされたパッケージ {0} は脆弱です。 + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + ダウンロードに失敗した {0} を削除できませんでした。ファイルが存在する場合は手動で削除してください。 + + + + Failed to download {0} from NuGet feed {1}. + NuGet フィード{0}から {1} をダウンロードできませんでした。 + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + NuGet ソース {0} の読み込みに失敗しました: このソースが有効ではありません。今後の処理ではスキップされます。 + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + NuGet ソース {0} は安全ではないため、検索されません。これらのソースを検索に含める場合は、--force を使用してください。 + + + + {0} is not found in NuGet feeds {1}. + {0} が NuGet フィードに見つかりません{1}。 + + + + Failed to copy package {0} to {1}. + {0} から {1} にパッケージをコピーできませんでした。 + + + + Failed to read content of package {0}. + パッケージ {0} のコンテンツを読み取れませんでした。 + + + + File {0} already exists. + ファイル {0} は既に存在します。 + + + + Failed to download {0} from {1}. + {1} からの {0} のダウンロードに失敗しました。 + + + + Failed to install the package {0}. +Details: {1}. + パッケージ {0} をインストールできませんでした。 +詳細: {1}。 + + + + The install request {0} cannot be processed by installer {1}. + インストール要求 {0} をインストーラー {1} で処理できません。 + + + + The NuGet package {0} is invalid. + NuGet パッケージ {0} が無効です。 + + + + The configured NuGet sources are invalid: {0}. + 構成済みの NuGet ソースが無効です: {0}。 + + + + No NuGet sources are configured. + 構成された NuGet ソースがありません。 + + + + The operation was cancelled. + 処理が取り消されました。 + + + + {0} was not found in NuGet feeds {1}. + {0} が NuGet フィード {1} で見つかりません。 + + + + The package {0} is not supported by installer {1}. + このパッケージ {0}はインストーラー {1} でサポートされていません + + + + Failed to uninstall the package {0}. +Details: {1}. + パッケージ {0} をアンインストールできませんでした。 +詳細: {1}。 + + + + Failed to check the update for the package {0}. +Details: {1}. + パッケージ {0} の更新プログラムを確認できませんでした。 +詳細: {1}。 + + + + The requested package {0} has vulnerabilities. + 要求されたパッケージ {0} には脆弱性があります。 + + + + The checked package {0} has vulnerabilities. + チェックされたパッケージ {0} には脆弱性があります。 + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + '{0}' は有効なオペレーティング システム名ではありません。使用可能な値は {1} です。 + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + {0} でのテンプレートの実行はサポートされていません。サポートされている OS は {1} です。 + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + オペレーティング システム + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + テンプレート パッケージの場所 {0} がサポートされていないか、または存在しません。 + + + + '{0}' is not a valid semver version. + '{0}' は有効なセマンティック バージョンではありません。 + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + ホスト ({0}) によって複数の 'ISdkInfoProvider' コンポーネントが提供されたため、'SdkVersionConstraint' を正しく初期化できません。 + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + ホストから 'ISdkInfoProvider' コンポーネントが提供されていません。'SdkVersionConstraint' を正しく初期化できません。 + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + 現在の .NET SDK バージョン ({0}) でテンプレートを実行することはサポートされていません。サポートされているバージョン: {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + .NET SDK バージョン + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + 引数 `{1}` の `{0}` を評価できませんでした。詳細: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + 制約 '{0}' を初期化できませんでした: {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + 制約 '{0}' が不明です。 + {0} is constraint type + + + Could not load template. + テンプレートを読み込めませんでした。 + + + + Failed to create template. +Details: {0} + テンプレートを作成できませんでした。 +詳細: {0} + + + + Destructive changes detected. + 重大な変更が検出されました。 + + + + The template is invalid and cannot be instantiated. + テンプレートが無効であり、インスタンス化できません。 + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + テンプレートを作成できませんでした: テンプレート名が指定されていません。テンプレート構成では、名前が指定されていない場合に使用できる既定の名前は構成されません。インスタンス化時にテンプレートの名前を指定するか、テンプレート構成で既定の名前を構成します。 + + + + Failed to load host data in {0} at {1}. + {1} の {0} でホスト データを読み込めませんでした。 + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + 識別子 '{0}' のパッケージを取得できませんでした。 + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + プロバイダー '{0}' からテンプレート パッケージを取得できませんでした。 +詳細: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + {0} をスキャンできませんでした。 +詳細: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + テンプレート キャッシュを保存できませんでした。詳細: {0} +テンプレート キャッシュは次回の実行時に再作成されます。 + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +次のテンプレートは同じ ID '{0}' を使用しています: +{1} +'{2}' からのテンプレートが使用されます。この競合を解決するには、競合するテンプレート パッケージをアンインストールしてください。 + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + '{2}' からの {0} '{1}' + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + テンプレート {0} には、次の検証エラーがあります: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + テンプレート {0} には、次の検証メッセージがあります: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + テンプレート {0} を読み込めませんでした: テンプレートが無効です。 + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + '{0}' ローカリゼーションテンプレート {1} をインストールできませんでした: ローカライズ ファイルが無効です。ローカライズはスキップされます。 + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + テンプレート {0} では、'{1}' ローカリゼーションで次の検証エラーが発生します。 + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + テンプレート {0} には、'{1}' ローカリゼーションで次の検証メッセージがあります: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + テンプレート {0} には、'{1}' ローカリゼーションで次の検証警告があります。 + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + テンプレート {0} には、次の検証警告があります: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + ホスト ({0}) によって複数の 'IWorkloadsInfoProvider' コンポーネントが提供されたため、'IWorkloadsInfoProvider' を正しく初期化できません。 + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + ホストから 'IWorkloadsInfoProvider' コンポーネントが提供されていません。'WorkloadConstraint' を正しく初期化できません。 + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + テンプレートの実行はサポートされていません - 必要なオプションのワークロードがインストールされていません。サポートされているワークロード: {0}。現在インストールされているオプションのワークロード: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + ワークロード + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + ホストによって提供された 'IWorkloadsInfoProvider' コンポーネントにより、いくつかの重複したワークロードが提供されました (重複: {0})。重複はスキップされます。 + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.ko.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.ko.xlf new file mode 100644 index 000000000000..7618c0a15be4 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.ko.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + 매개 변수 조건에 결정적 평가를 방지하는 순환 종속성 [{0}]이(가) 포함되어 있습니다. + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + {1} 매개 변수의 {0} 조건을 평가하지 못했습니다(조건 텍스트: {2}, 평가 오류: {3}). - 조건 형식이 잘못되었거나, 참조된 매개 변수에 기본값이나 명시적 값이 없을 수 있습니다. + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + 예기치 않은 내부 오류입니다. 순환 종속성이 없는 것으로 보이는 매개 변수 종속성의 토폴로지 정렬을 수행할 수 없습니다. + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + 매개 변수 조건에 [{0}] 순환 종속성이 포함되어 있습니다. 매개 변수의 현재 값을 사용하여 매개 변수를 결정적으로 평가할 수 있으므로 계속 진행합니다. 그러나 다른 매개 변수가 있는 인스턴스화는 오류가 발생할 수 있으므로 템플릿을 검토해야 합니다. + {0} is the dependency chain + + + '{0}' should not contain empty items + '{0}'에는 빈 항목이 없어야 합니다. + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + 인수를 지정하지 않았습니다. 인수를 하나 이상 지정해야 합니다. + + + + '{0}' does not contain valid items. + '{0}'에 유효한 항목이 없습니다. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + '{0}'은(는) 유효한 JSON이 아닙니다. + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + '{0}'은(는) 개체 배열이어야 합니다. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + '{0}'은(는) 유효한 JSON 배열이 아닙니다. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + '{0}'은(는) 유효한 JSON 문자열 또는 배열이 아닙니다. + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + '{0}'은(는) 올바른 버전 또는 버전 범위가 아닙니다. + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + template.json에서 제약 조건 구성을 확인합니다. + + + + Environment variables + 환경 변수 + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + 템플릿에 적절한 조건이 설정되지 않은(IsEnabled 또는 IsRequired 특성이 조건으로 채워지지 않음) 매개 변수에 대한 매개 변수 조건의 외부 평가 결과를 전달하려고 시도했거나 템플릿에 조건이 있는 매개 변수에 대한 조건 결과를 전달하지 못했습니다. 잘못된 매개 변수: {0}. + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + 템플릿에 적절한 조건이 설정되지 않은(IsEnabled 또는 IsRequired 특성이 조건으로 채워지지 않음) 매개 변수에 대한 매개 변수 조건의 외부 평가 결과를 전달하려고 시도했거나 템플릿에 조건이 있는 매개 변수에 대한 조건 결과를 전달하지 못했습니다. 잘못된 매개 변수: {0}. + + + + The folder {0} doesn't exist. + 폴더 {0}이(가) 존재하지 않습니다. + + + + Check the constraint configuration in template.json. + template.json에서 제약 조건 구성을 확인합니다. + + + + latest version + 최신 버전 + small letters, string is used in the sentence + + + version {0} + 버전 {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + {0}은(는) 여러 설치 관리자가 설치할 수 있습니다. 사용할 설치 관리자 이름을 지정합니다. + + + + {0} is already installed. + {0}이(가) 이미 설치되어 있습니다. + + + + {0} cannot be installed. + {0}을(를) 설치할 수 없습니다. + + + + {0} is already installed, it will be replaced with {1}. + {0}이(가) 이미 설치되어 있으므로 {1}(으)로 바뀝니다. + + + + {0} was successfully uninstalled. + {0}이(가) 성공적으로 제거되었습니다. + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + {0}에서 설치된 템플릿 패키지 목록을 읽지 못했습니다. 파일이 손상되었기 때문일 수 있습니다. 이 파일을 수동으로 검토하고 JSON 구조의 오류를 수정하거나 파일을 제거하여 설치된 패키지 목록의 목록을 지우고 다시 설치하세요. 오류 세부 정보: {1}. + + + + '{0}' does not have mandatory property '{1}'. + '{0}'에 '{1}' 필수 속성이 없습니다. + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + {0}(버전: {1})에서 템플릿을 실행할 수 없습니다. 지원되는 호스트는 {2}입니다. + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + 템플릿 엔진 호스트 + + + + Host defined parameters + 호스트 정의 매개 변수 + + + + Found package is vulnerable source: {0} + 찾은 패키지가 취약한 소스입니다({0}). + + + + Failed to load the NuGet source {0}. + NuGet 원본 {0}을(를) 로드하지 못했습니다. + + + + Failed to load NuGet sources configured for the folder {0}. + 폴더 {0}에 대해 구성된 NuGet 원본을 로드하지 못했습니다. + + + + Failed to read package information from NuGet source {0}. + NuGet 원본 {0}에서 패키지 정보를 읽지 못했습니다. + + + + File {0} already exists. + 파일 {0}이(가) 이미 있습니다. + + + + No NuGet sources are defined or enabled. + NuGet 원본이 정의되거나 사용하도록 설정되지 않았습니다. + + + + The checked package {0} is vulnerable. + 확인한 패키지 {0}이(가) 취약합니다. + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + 다운로드 실패 후{0} 제거에 실패했습니다. 파일이 있으면 수동으로 제거하세요. + + + + Failed to download {0} from NuGet feed {1}. + NuGet 피드 {1}에서 {0}을(를) 다운로드하지 못했습니다. + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + NuGet 원본 {0}을(를) 로드하지 못했습니다. 원본이 유효하지 않습니다. 추가 처리에서 건너뛰세요. + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + {0} NuGet 소스가 안전하지 않으므로 검색되지 않습니다. 해당 소스를 검색에 포함하려면 --force를 사용하세요. + + + + {0} is not found in NuGet feeds {1}. + {0}을(를) NuGet 피드 {1}에서 찾을 수 없습니다. + + + + Failed to copy package {0} to {1}. + 패키지 {0}을(를) {1}(으)로 복사하지 못했습니다. + + + + Failed to read content of package {0}. + 패키지 {0}의 콘텐츠를 읽지 못했습니다. + + + + File {0} already exists. + 파일 {0}이(가) 이미 있습니다. + + + + Failed to download {0} from {1}. + {1}에서 {0}을(를) 다운로드하지 못했습니다. + + + + Failed to install the package {0}. +Details: {1}. + 패키지 {0}을(를) 설치하지 못했습니다. +세부 정보: {1}. + + + + The install request {0} cannot be processed by installer {1}. + 설치 요청 {0}은(는) 설치 관리자 {1}에서 처리할 수 없습니다. + + + + The NuGet package {0} is invalid. + NuGet 패키지 {0}이(가) 잘못되었습니다. + + + + The configured NuGet sources are invalid: {0}. + 구성된 NuGet 원본이 잘못되었습니다. {0}. + + + + No NuGet sources are configured. + NuGet 원본이 구성되지 않았습니다. + + + + The operation was cancelled. + 작업이 취소되었습니다. + + + + {0} was not found in NuGet feeds {1}. + {0}을(를) NuGet 피드 {1}에서 찾을 수 없습니다. + + + + The package {0} is not supported by installer {1}. + 설치 관리자 {1}이(가) 패키지 {0}를(을) 지원하지 않습니다. + + + + Failed to uninstall the package {0}. +Details: {1}. + 패키지 {0}을(를) 설치하지 못했습니다. +세부 정보: {1}. + + + + Failed to check the update for the package {0}. +Details: {1}. + 패키지 {0}에 대한 업데이트를 확인하지 못했습니다. +세부 정보: {1}. + + + + The requested package {0} has vulnerabilities. + 요청한 패키지 {0}에 취약성이 있습니다. + + + + The checked package {0} has vulnerabilities. + 확인한 패키지 {0}에 취약성이 있습니다. + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + '{0}'은(는) 올바른 운영 체제 이름이 아닙니다. 허용되는 값은 {1}입니다. + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + {0}에서 템플릿을 실행할 수 없습니다. 지원되는 OS는 {1}입니다. + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + 운영 체제 + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + 템플릿 패키지 위치 {0}이(가) 지원되지 않거나 존재하지 않습니다. + + + + '{0}' is not a valid semver version. + '{0}'(는) 유효한 셈버 버전이 아닙니다. + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + 호스트에서 제공하는 여러 'ISdkInfoProvider' 구성 요소({0})가 존재합니다. 따라서 'SdkVersionConstraint'를 제대로 초기화할 수 없습니다. + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + 호스트에서 제공하는 'ISdkInfoProvider' 구성 요소가 없습니다. 'SdkVersionConstraint'를 제대로 초기화할 수 없습니다. + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + 현재 .NET SDK 버전({0})에서 실행 중인 템플릿은 지원되지 않습니다. 지원되는 버전: {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + .NET SDK 버전 + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + '{0}' 제약 조건을 '{1}' 인수에 대해 평가하지 못했습니다. 세부 정보: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + 제약 조건 '{0}'을(를) 초기화하지 못했습니다. {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + 제약 조건 '{0}'을(를) 알 수 없습니다. + {0} is constraint type + + + Could not load template. + 템플릿을 로드할 수 없습니다. + + + + Failed to create template. +Details: {0} + 템플릿을 만들지 못했습니다. +세부 정보: {0} + + + + Destructive changes detected. + 파괴적 변경이 감지되었습니다. + + + + The template is invalid and cannot be instantiated. + 템플릿이 유효하지 않아 인스턴스화할 수 없습니다. + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + 템플릿을 만들지 못했습니다. 템플릿 이름이 지정되지 않았습니다. 템플릿 구성이 이름 미지정 시 사용할 수 있는 기본 이름을 구성하지 않습니다. 인스턴스화할 때 템플릿의 이름을 지정하거나 템플릿 구성에서 기본 이름을 구성하세요. + + + + Failed to load host data in {0} at {1}. + {0}에서 {1}의 호스트 데이터를 로드하지 못했습니다. + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + 식별자가 '{0}'인 패키지를 검색하지 못했습니다. + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + 공급자 '{0}'에서 템플릿 패키지를 검색하지 못했습니다. +세부 정보: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + {0}을(를) 검사하지 못했습니다. +세부 정보: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + 템플릿 캐시를 저장하지 못했습니다. 세부 정보: {0} +템플릿 캐시는 다음에 실행될 때 다시 만들어집니다. + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +다음 템플릿은 동일한 ID '{0}'을(를) 사용합니다: +{1} +'{2}'의 템플릿이 사용됩니다. 이 충돌을 해결하려면 충돌하는 템플릿 패키지를 제거하세요. + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {2}의 {0} '{1}' + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + 템플릿 {0}에 다음 유효성 검사 오류가 있습니다. + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + 템플릿 {0}에 다음 유효성 검사 메시지가 있습니다. + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + 템플릿 {0}을(를) 로드하지 못했습니다. 템플릿이 유효하지 않습니다. + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + 템플릿 {1}의 '{0}' 지역화를 로드하지 못했습니다. 지역화 파일이 유효하지 않습니다. 지역화는 생략합니다. + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + 템플릿 {0}의 ‘{1}’ 지역화에 다음 유효성 검사 오류가 있습니다. + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + 템플릿 {0}의 ‘{1}’ 지역화에 다음 유효성 검사 메시지가 있습니다. + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + 템플릿 {0}의 ‘{1}’ 지역화에 다음 유효성 검사 경고가 있습니다. + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + 템플릿 {0}에 다음 유효성 검사 경고가 있습니다. + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + 호스트에서 제공하는 여러 'IWorkloadsInfoProvider' 구성 요소({0})가 존재합니다. 따라서 'WorkloadConstraint'를 제대로 초기화할 수 없습니다. + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + 호스트에서 제공하는 'IWorkloadsInfoProvider' 구성 요소가 없습니다. 'WorkloadConstraint'를 제대로 초기화할 수 없습니다. + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + 템플릿 실행은 지원되지 않습니다. 필수 선택적 워크로드가 설치되지 않았습니다. 지원되는 워크로드: {0}. 현재 설치된 선택적 워크로드: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + 워크로드 + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + 호스트에서 제공하는 'IWorkloadsInfoProvider' 구성 요소에서 중복된 작업(중복: {0})을 제공했습니다. 중복 항목은 건너뜁니다. + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.pl.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.pl.xlf new file mode 100644 index 000000000000..fc9b4fd2653c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.pl.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + Warunki parametru zawierają zależność cykliczną: [{0}], która uniemożliwia ocenę deterministyczną. + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + Nie można obliczyć warunku {0} w przypadku parametru {1} (tekst warunku: {2}, błąd oceny: {3}) — warunek może być źle sformułowany lub parametry, do których się odwołuje, nie mają wartości domyślnych ani jawnych. + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + Nieoczekiwany błąd wewnętrzny — nie można wykonać topologicznego sortowania zależności parametrów, które prawdopodobnie nie mają zależności cyklicznych. + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + Warunki parametru zawierają zależność cykliczną: [{0}]. Przy bieżących wartościach parametrów można deterministycznie ocenić parametry, a więc dalsze postępowanie. Należy jednak przejrzeć szablon, ponieważ utworzenie wystąpienia z różnymi parametrami może doprowadzić do błędu. + {0} is the dependency chain + + + '{0}' should not contain empty items + Konfiguracja „{0}” nie powinna zawierać pustych elementów + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + Nie określono argumentów. Należy określić co najmniej jeden argument. + + + + '{0}' does not contain valid items. + Element „{0}” nie zawiera prawidłowych elementów. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + Element „{0}” nie jest prawidłowym kodem JSON. + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + Element „{0}” powinien być tablicą obiektów. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + Element „{0}” nie jest prawidłową tablicą JSON. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + Element „{0}” nie jest prawidłowym ciągiem lub tablicą JSON. + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + „{0}” nie jest prawidłową wersją lub zakresem wersji. + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + Sprawdź konfigurację ograniczenia w pliku template.json. + + + + Environment variables + Zmienne środowiskowe + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + Próba przekazania wyniku zewnętrznej oceny warunków parametrów dla parametrów, które nie mają ustawionego odpowiedniego warunku w szablonie (atrybuty IsEnabled lub IsRequired nie są wypełnione warunkiem) lub niepowodzenie przekazania wyników warunku dla parametrów z warunkami w szablonie. Parametry powodujące problemy: {0}. + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + Próba przekazania wyniku zewnętrznej oceny warunków parametrów dla parametrów, które nie mają ustawionego odpowiedniego warunku w szablonie (atrybuty IsEnabled lub IsRequired nie są wypełnione warunkiem) lub niepowodzenie przekazania wyników warunku dla parametrów z warunkami w szablonie. Parametry powodujące problemy: {0}. + + + + The folder {0} doesn't exist. + Folder{0} nie istnieje. + + + + Check the constraint configuration in template.json. + Sprawdź konfigurację ograniczenia w pliku template.json. + + + + latest version + najnowszą wersją + small letters, string is used in the sentence + + + version {0} + wersja: {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + {0} mogą być instalowane przez kilka instalatorów. Określ nazwę instalatora, która ma być używana. + + + + {0} is already installed. + Już zainstalowano: {0}. + + + + {0} cannot be installed. + Nie można zainstalować pakietu {0}. + + + + {0} is already installed, it will be replaced with {1}. + Pakiet {0} jest już zainstalowany. Zostanie on zastąpiony pakietem {1}. + + + + {0} was successfully uninstalled. + Pomyślnie odinstalowano pakiet {0}. + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + W lokalizacji {0} nie można odczytać listy zainstalowanych pakietów szablonów. Może to być spowodowane uszkodzeniem pliku. Przejrzyj ten plik ręcznie i napraw błędy w strukturze kodu JSON lub usuń plik, aby wyczyścić listę zainstalowanych pakietów i zainstalować je ponownie. Szczegóły błędu: {1}. + + + + '{0}' does not have mandatory property '{1}'. + Element „{0}” nie ma obowiązkowej właściwości „{1}”. + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + Uruchamianie szablonu w {0} (wersja: {1}) nie jest obsługiwane, obsługiwane hosty to:{2}. + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + Host aparatu szablonów + + + + Host defined parameters + Parametry zdefiniowane przez hosta + + + + Found package is vulnerable source: {0} + Znaleziony pakiet jest źródłem podatnym na zagrożenia: {0} + + + + Failed to load the NuGet source {0}. + Nie można załadować źródła pakietu NuGet {0}. + + + + Failed to load NuGet sources configured for the folder {0}. + Nie można załadować źródeł pakietu NuGet skonfigurowanych dla folderu {0} + + + + Failed to read package information from NuGet source {0}. + Nie można odczytać informacji o pakiecie ze źródła pakietu NuGet {0}. + + + + File {0} already exists. + Plik {0} już istnieje. + + + + No NuGet sources are defined or enabled. + Nie zdefiniowano ani nie włączono źródeł pakietu NuGet. + + + + The checked package {0} is vulnerable. + Zaznaczony {0} pakietu jest podatny na zagrożenia. + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + Nie można usunąć pakietu {0} po nieudanym pobraniu. Usuń plik ręcznie, jeśli istnieje. + + + + Failed to download {0} from NuGet feed {1}. + Nie można pobrać {0} z kanału informacyjnego pakietu NuGet {1}. + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + Nie można załadować źródła pakietu NuGet {0}: źródło jest nieprawidłowe. Zostanie ono pominięte podczas dalszego przetwarzania. + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + Źródła NuGet {0} są niezabezpieczone i nie będą przeszukiwane. Jeśli chcesz uwzględnić te źródła do wyszukiwania, użyj opcji --force. + + + + {0} is not found in NuGet feeds {1}. + Nie znaleziono pakietu {0} w kanałach informacyjnych pakietu NuGet {1}. + + + + Failed to copy package {0} to {1}. + Nie można skopiować pakietu {0} do {1}. + + + + Failed to read content of package {0}. + Nie można odczytać zawartości pakietu {0}. + + + + File {0} already exists. + Plik {0} już istnieje. + + + + Failed to download {0} from {1}. + Nie można pobrać {0} z {1}. + + + + Failed to install the package {0}. +Details: {1}. + Nie można zainstalować pakietu {0}. +Szczegóły: {1}. + + + + The install request {0} cannot be processed by installer {1}. + Nie można przetworzyć żądania instalacji {0} przez instalator {1}. + + + + The NuGet package {0} is invalid. + Pakiet NuGet {0} jest nieprawidłowy. + + + + The configured NuGet sources are invalid: {0}. + Skonfigurowane źródła pakietu NuGet są nieprawidłowe: {0}. + + + + No NuGet sources are configured. + Nie skonfigurowano źródeł pakietu NuGet. + + + + The operation was cancelled. + Operacja została anulowana. + + + + {0} was not found in NuGet feeds {1}. + Nie znaleziono pakietu {0} w kanałach informacyjnych pakietu NuGet {1}. + + + + The package {0} is not supported by installer {1}. + Pakiet {0} nie jest obsługiwany przez instalator {1}. + + + + Failed to uninstall the package {0}. +Details: {1}. + Nie można odinstalować pakietu {0}. +Szczegóły: {1}. + + + + Failed to check the update for the package {0}. +Details: {1}. + Nie można sprawdzić aktualizacji dla pakietu {0}. +Szczegóły: {1}. + + + + The requested package {0} has vulnerabilities. + Żądany pakiet {0} ma luki w zabezpieczeniach. + + + + The checked package {0} has vulnerabilities. + Sprawdzony pakiet {0} ma luki w zabezpieczeniach. + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + „{0}” nie jest prawidłową nazwą systemu operacyjnego. Dozwolone wartości to: {1}. + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + Uruchamianie szablonu w {0} nie jest obsługiwane. Obsługiwany system operacyjny to: {1}. + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + System operacyjny + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + Lokalizacja pakietu szablonów {0} nie jest obsługiwana lub nie istnieje. + + + + '{0}' is not a valid semver version. + „{0}” nie jest prawidłową wersją semver. + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + Host dostarczył wiele składników „IWorkloadsInfoProvider” ({0}), dlatego nie można poprawnie zainicjować elementu „SdkVersionConstraint”. + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + Host nie dostarczył żadnego składnika „ISdkInfoProvider”. Nie można poprawnie zainicjować elementu „SdkVersionConstraint”. + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + Uruchamianie szablonu w bieżącej wersji zestawu .NET SDK ({0}) nie jest obsługiwane. Obsługiwane wersje: {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + Wersja zestawu .NET SDK + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + Nie można ocenić ograniczenia „{0}” dla argumentów „{1}”; szczegóły: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + Nie można zainicjować ograniczenia „{0}”: {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + Ograniczenie „{0}” jest nieznane. + {0} is constraint type + + + Could not load template. + Nie można załadować szablonu. + + + + Failed to create template. +Details: {0} + Nie można utworzyć szablonu. +Szczegóły: {0}. + + + + Destructive changes detected. + Wykryto niszczące zmiany. + + + + The template is invalid and cannot be instantiated. + Szablon jest nieprawidłowy i nie można utworzyć jego wystąpienia. + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + Nie można utworzyć szablonu: nie określono nazwy szablonu. Konfiguracja szablonu nie konfiguruje nazwy domyślnej, która może być używana, gdy nazwa nie jest określona. Określ nazwę szablonu podczas tworzenia wystąpienia lub konfigurowania nazwy domyślnej w konfiguracji szablonu. + + + + Failed to load host data in {0} at {1}. + Nie udało się załadować danych hosta w klasie {0} z {1}. + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + Nie można pobrać pakietu o identyfikatorze „{0}”. + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + Nie można pobrać pakietów szablonów od dostawcy „{0}”. +Szczegóły: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + Nie można zeskanować {0}. +Szczegóły: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + Nie można zapisać pamięci podręcznej szablonów. Szczegóły: {0} +Pamięć podręczna szablonów zostanie ponownie utworzona przy następnym uruchomieniu. + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +Następujące szablony używają tej samej tożsamości '{0}': +{1} +Szablon '{2}' zostanie użyty. Aby rozwiązać ten konflikt, odinstaluj niezgodne pakiety szablonów. + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {0} '{1}' z '{2}' + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + W przypadku szablonu {0} występują następujące błędy walidacji: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + W przypadku szablonu {0} występują następujące komunikaty dotyczące walidacji: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + Nie można załadować szablonu {0}: szablon jest nieprawidłowy. + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + W lokalizacji „{0}” nie można załadować szablonu {1}: plik lokalizacji jest nieprawidłowy. Lokalizacja zostanie pominięta. + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + W przypadku szablonu {0} w lokalizacji „{1}” występują następujące błędy walidacji: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + W przypadku szablonu {0} w lokalizacji „{1}” występują następujące komunikaty dotyczące walidacji: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + W przypadku szablonu {0} w lokalizacji „{1}” występują następujące ostrzeżenia dotyczące walidacji: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + W przypadku szablonu {0} występują następujące ostrzeżenia dotyczące walidacji: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + Host dostarczył wiele składników „IWorkloadsInfoProvider” ({0}), dlatego nie można poprawnie zainicjować elementu „WorkloadConstraint”. + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + Brak składnika „IWorkloadsInfoProvider” dostarczonego przez hosta. Nie można poprawnie zainicjować elementu „WorkloadConstraint”. + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + Uruchamianie szablonu nie jest obsługiwane — nie zainstalowano wymaganych opcjonalnych obciążeń. Obsługiwane obciążenia: {0}. Aktualnie zainstalowane opcjonalne obciążenia: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + Obciążenie + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + Składnik „IWorkloadsInfoProvider” dostarczony przez hosta zapewnił kilka zduplikowanych obciążeń (duplikaty: {0}). Duplikaty zostaną pominięte. + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.pt-BR.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.pt-BR.xlf new file mode 100644 index 000000000000..ec58b481f952 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.pt-BR.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + As condições de parâmetro contêm dependência cíclica: [{0}] que está impedindo a avaliação determinística. + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + Falha ao avaliar a condição {0} no parâmetro {1} (texto da condição: {2}, erro de avaliação: {3}) - a condição pode estar malformada ou os parâmetros referenciados não possuem valores padrão nem explícitos. + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + Erro interno inesperado - não é possível executar o tipo topológico de dependências de parâmetro que parecem não ter dependências cíclicas. + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + As condições de parâmetro contêm dependência cíclica: [{0}]. Com os valores atuais de parâmetros, é possível avaliar parâmetros de forma determinística, portanto, continuar. No entanto, o modelo deve ser revisado, pois a instanciação com parâmetros diferentes pode levar a um erro. + {0} is the dependency chain + + + '{0}' should not contain empty items + '{0}' não deve conter itens vazios + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + Os argumentos não foram especificados. Pelo menos um argumento deve ser especificado. + + + + '{0}' does not contain valid items. + '{0}' não contém itens válidos. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + '{0}' não é um JSON válido. + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + '{0}' deve ser uma matriz de objetos. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + '{0}' não é uma matriz JSON válida. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + '{0}' não é uma string ou matriz JSON válida. + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + '{0}' não é uma versão ou intervalo de versões válido. + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + Verifique a configuração de restrição em template.json. + + + + Environment variables + Variáveis de ambiente + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + Tentativa de passar o resultado da avaliação externa das condições de parâmetros para parâmetro(s) que não têm a condição apropriada definida no modelo (atributos IsEnabled ou IsRequired não preenchidos com a condição) ou uma falha ao passar os resultados da condição para parâmetros com condições no modelo. Parâmetros ofensivos: {0}. + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + Tentativa de passar o resultado da avaliação externa das condições de parâmetros para parâmetros que não têm a condição apropriada definida no modelo (atributos IsEnabled ou IsRequired não preenchidos com a condição) ou uma falha ao passar os resultados da condição para parâmetros com condição(ões) no modelo. Parâmetro(s) ofensivo(s): {0}. + + + + The folder {0} doesn't exist. + A pasta {0} não existe. + + + + Check the constraint configuration in template.json. + Verifique a configuração de restrição em template.json. + + + + latest version + versão mais recente + small letters, string is used in the sentence + + + version {0} + versão {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + {0} pode ser instalado por vários instaladores. Especifique o nome do instalador a ser utilizado. + + + + {0} is already installed. + {0} já está instalado. + + + + {0} cannot be installed. + {0} não pode ser instalado. + + + + {0} is already installed, it will be replaced with {1}. + {0} já está instalado, será substituída por {1}. + + + + {0} was successfully uninstalled. + {0} foi desinstalado com sucesso. + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + Falha ao ler a lista de pacotes de modelos instalados em {0}. Pode ser porque o arquivo está corrompido. Examine esse arquivo manualmente e corrija os erros na estrutura JSON ou remova o arquivo para limpar a lista de pacotes instalados e reinstalá-los novamente. Detalhes do erro: {1}. + + + + '{0}' does not have mandatory property '{1}'. + '{0}' não tem propriedade obrigatória '{1}'. + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + A execução do modelo em {0} (versão: {1}) não é suportada, os hosts suportados é/são: {2}. + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + Host do mecanismo de modelo + + + + Host defined parameters + Parâmetros definidos pelo host + + + + Found package is vulnerable source: {0} + O pacote encontrado é uma fonte vulnerável: {0} + + + + Failed to load the NuGet source {0}. + Falha no carregamento da fonte NuGet {0}. + + + + Failed to load NuGet sources configured for the folder {0}. + Falha no carregamento de fontes NuGet configuradas para a pasta {0}. + + + + Failed to read package information from NuGet source {0}. + Falha na leitura das informações do pacote da fonte NuGet {0}. + + + + File {0} already exists. + O arquivo {0} já existe. + + + + No NuGet sources are defined or enabled. + Nenhuma fonte NuGet está definida ou habilitada. + + + + The checked package {0} is vulnerable. + O pacote verificado {0} está vulnerável. + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + Falha ao remover {0} após o download falhar. Remova o arquivo manualmente, caso existir. + + + + Failed to download {0} from NuGet feed {1}. + Falha no download {0} de NuGet feed {1}. + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + Falha no carregamento da fonte NuGet {0}: a fonte não é válida. Ela será ignorada num processamento posterior. + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + As fontes do NuGet {0} não são seguras e não serão pesquisadas. Se você quiser incluir essas fontes para pesquisa, use --force. + + + + {0} is not found in NuGet feeds {1}. + {0} não é encontrado no NuGet feeds {1}. + + + + Failed to copy package {0} to {1}. + Falha em copiar pacote {0} para {1}. + + + + Failed to read content of package {0}. + Falha na leitura do conteúdo da embalagem {0}. + + + + File {0} already exists. + O arquivo {0} já existe. + + + + Failed to download {0} from {1}. + Falha no download {0} de {1}. + + + + Failed to install the package {0}. +Details: {1}. + Falha ao instalar o pacote {0}. +Detalhes: {1}. + + + + The install request {0} cannot be processed by installer {1}. + O pedido de instalação {0} não pode ser processado pelo instalador {1}. + + + + The NuGet package {0} is invalid. + O pacote NuGet {0} é inválido. + + + + The configured NuGet sources are invalid: {0}. + A fonte NuGet configurado é inválido: {0}. + + + + No NuGet sources are configured. + Nenhuma fonte NuGet foi configurado. + + + + The operation was cancelled. + A operação foi cancelada. + + + + {0} was not found in NuGet feeds {1}. + {0} não foi encontrado no NuGet feeds {1}. + + + + The package {0} is not supported by installer {1}. + O pacote{0} não é suportado pelo instalador {1}. + + + + Failed to uninstall the package {0}. +Details: {1}. + Falha ao desinstalar o pacote {0}. +Detalhes: {1}. + + + + Failed to check the update for the package {0}. +Details: {1}. + Falha ao verificar a atualização do pacote {0}, +Detalhes: {1}. + + + + The requested package {0} has vulnerabilities. + O pacote solicitado {0} tem vulnerabilidades. + + + + The checked package {0} has vulnerabilities. + O pacote verificado {0} tem vulnerabilidades. + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + '{0}' não é um nome de sistema operacional válido. Os valores permitidos são: {1}. + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + A execução do modelo em {0} não é compatível, o sistema operacional compatível é/são: {1}. + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + Sistema Operacional + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + O local do pacote de modelo {0} não é suportado ou não existe. + + + + '{0}' is not a valid semver version. + '{0}' não é uma versão válida semver. + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + Vários componentes 'ISdkInfoProvider' fornecidos pelo host ({0}), portanto, 'SdkVersionConstraint' não pode ser inicializado corretamente. + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + Nenhum componente 'ISdkInfoProvider' fornecido pelo host. 'SdkVersionConstraint' não pode ser inicializado corretamente. + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + A execução do modelo na versão atual do .NET SDK ({0}) não é compatível. Versão(s) suportada: {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + Versão do SDK do .NET + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + A restrição '{0}' não pôde ser avaliada para os argumentos '{1}', detalhes: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + A restrição '{0}' falhou para inicializar: {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + A restrição '{0}' é desconhecida. + {0} is constraint type + + + Could not load template. + Não foi possível carregar o modelo. + + + + Failed to create template. +Details: {0} + Falha ao criar o modelo. +Detalhes: {0} + + + + Destructive changes detected. + Alterações destrutivas detectadas. + + + + The template is invalid and cannot be instantiated. + O modelo é inválido e não pode ser instanciado. + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + Falha ao criar modelo: o nome do modelo não foi especificado. A configuração do modelo não define um nome padrão que possa ser usado quando o nome não for especificado. Especifique o nome do modelo ao criar uma instância ou configurar um nome padrão na configuração do modelo. + + + + Failed to load host data in {0} at {1}. + Falha ao carregar os dados do host em {0} às {1}. + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + Falha ao recuperar pacote com identificador '{0}'. + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + Falha ao recuperar os pacotes de modelos do provedor '{0}'. +Detalhes: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + Falha na verificação {0}. +Detalhes: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + Falha ao armazenar o cache do modelo, detalhes: {0} +O cache do modelo será recriado na próxima execução. + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +Os seguintes modelos usam a mesma identidade '{0}': +{1} +O modelo de '{2}' será usado. Para resolver esse conflito, desinstale os pacotes de modelos em conflito. + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {0} '{1}' de '{2}' + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + O modelo {0} tem os seguintes erros de validação: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + O modelo {0} tem as seguintes mensagens de validação: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + Falha ao carregar o modelo {0}: o modelo não é válido. + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + Falha ao carregar a localização '{0}' do modelo {1}: o arquivo de localização não é válido. A localização será ignorada. + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + O modelo {0} tem os seguintes erros de validação na localização ''{1}'': + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + O modelo {0} tem as seguintes mensagens de validação na localização ''{1}'': + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + O modelo {0} tem os seguintes avisos de validação na localização ''{1}'': + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + O modelo {0} tem os seguintes avisos de validação: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + Vários componentes 'IWorkloadsInfoProvider' fornecidos pelo host ({0}), portanto, 'WorkloadConstraint' não pode ser inicializado corretamente. + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + Nenhum componente 'IWorkloadsInfoProvider' fornecido pelo host. 'WorkloadConstraint' não pode ser inicializado corretamente. + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + O modelo em execução não é suportado - cargas de trabalho(s) opcionais obrigatórias não instaladas. Cargas de trabalho(s) suportadas: {0}. Cargas de trabalho opcionais instaladas atualmente: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + Carga de trabalho + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + O componente 'IWorkloadsInfoProvider' fornecido pelo host forneceu algumas cargas de trabalho duplicadas (duplicatas: {0}). As duplicatas serão ignoradas. + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.ru.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.ru.xlf new file mode 100644 index 000000000000..85ff0a69545b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.ru.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + Условия параметров содержат циклическую зависимость [{0}], из-за которой невозможно выполнить детерминированную оценку. + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + Не удалось вычислить условие {0} для параметра {1} (текст условия: {2}, ошибка вычисления: {3}). Условие может иметь неправильный формат или ссылочные параметры не имеют значений по умолчанию или явных значений. + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + Непредвиденная внутренняя ошибка. Не удалось выполнить топологическую сортировку зависимостей параметров без циклических зависимостей. + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + Условия параметра содержат циклическую зависимость: [{0}]. С текущими значениями параметров возможно детерминированное вычисление параметров, поэтому работа продолжается. Тем не менее, следует проверить шаблон, поскольку при создании экземпляра с другими параметрами может возникнуть ошибка. + {0} is the dependency chain + + + '{0}' should not contain empty items + «{0}» не должна содержать пустых элементов + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + Аргументы не указаны. Необходимо указать хотя бы один аргумент. + + + + '{0}' does not contain valid items. + «{0}» не содержит допустимые элементы. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + «{0}» не является допустимым JSON. + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + «{0}» должно быть массивом объектов. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + «{0}» не является допустимым массивом JSON. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + «{0}» не является допустимой строкой или массивом JSON. + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + «{0}» не является допустимой версией или диапазоном версий. + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + Проверьте конфигурацию ограничения в template.json. + + + + Environment variables + Переменные среды + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + Попытка передать результат внешнего вычисления условий для параметров, для которых в шаблоне не задано необходимое условие (отсутствует условие для атрибутов IsEnabled или IsRequired), или не удалось передать результаты условий для параметров с условиями в шаблоне. Параметры с ошибками: {0}. + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + Попытка передать результат внешнего вычисления условий для параметров, для которых в шаблоне не задано необходимое условие (отсутствует условие для атрибутов IsEnabled или IsRequired), или не удалось передать результаты условий для параметров с условиями в шаблоне. Параметры с ошибками: {0}. + + + + The folder {0} doesn't exist. + Папка {0} не существует. + + + + Check the constraint configuration in template.json. + Проверьте конфигурацию ограничения в template.json. + + + + latest version + последняя версия + small letters, string is used in the sentence + + + version {0} + версия {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + {0} можно установить с помощью нескольких установщиков. Укажите имя используемого установщика. + + + + {0} is already installed. + Установка {0} уже выполнена. + + + + {0} cannot be installed. + Невозможно установить {0}. + + + + {0} is already installed, it will be replaced with {1}. + Уже установлено: {0}. Будет заменено на {1}. + + + + {0} was successfully uninstalled. + Выполнено удаление {0}. + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + Не удалось прочесть список установленных пакетов шаблонов в {0}. Возможно, поврежден файл. Проверьте этот файл вручную и исправьте ошибки в структуре JSON или удалите этот файл, чтобы очистить список установленных пакетов, затем переустановите шаблоны. Подробные сведения об ошибке: {1}. + + + + '{0}' does not have mandatory property '{1}'. + «{0}» не имеет обязательного свойства «{1}». + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + Запуск шаблона на {0} (версия: {1}) не поддерживается, поддерживаемые узлы: {2}. + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + Узел обработчика шаблонов + + + + Host defined parameters + Параметры, определяемые узлом + + + + Found package is vulnerable source: {0} + Найденный пакет представляет собой уязвимый источник: {0} + + + + Failed to load the NuGet source {0}. + Не удалось загрузить источник NuGet {0}. + + + + Failed to load NuGet sources configured for the folder {0}. + Не удалось загрузить настроенные источники NuGet для папки {0}. + + + + Failed to read package information from NuGet source {0}. + Не удалось прочитать сведения о пакете из источника NuGet {0}. + + + + File {0} already exists. + Файл {0} уже существует. + + + + No NuGet sources are defined or enabled. + Источники NuGet не определены или не включены. + + + + The checked package {0} is vulnerable. + Выбранный пакет {0} является уязвимым. + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + Не удалось удалить {0} после сбоя скачивания. Удалите файл вручную, если он существует. + + + + Failed to download {0} from NuGet feed {1}. + Не удалось выполнить скачивание {0} из канала NuGet {1}. + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + Не удалось загрузить источник NuGet {0}: недопустимый источник. Он будет пропущен при дальнейшей обработке. + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + Источники NuGet {0} небезопасны и не будут использоваться при поиске. Если вы хотите включить эти источники в поиск, используйте параметр --force. + + + + {0} is not found in NuGet feeds {1}. + {0} не найдено в веб-каналах NuGet {1}. + + + + Failed to copy package {0} to {1}. + Не удалось скопировать пакет {0} в {1}. + + + + Failed to read content of package {0}. + Не удалось прочитать содержимое пакета {0}. + + + + File {0} already exists. + Файл {0} уже существует. + + + + Failed to download {0} from {1}. + Не удалось загрузить {0} из {1}. + + + + Failed to install the package {0}. +Details: {1}. + Не удалось установить пакет {0}. +Сведения: {1}. + + + + The install request {0} cannot be processed by installer {1}. + Запрос на {0} не может быть обработан установщиком {1}. + + + + The NuGet package {0} is invalid. + Недопустимый пакет NuGet {0}. + + + + The configured NuGet sources are invalid: {0}. + Настроенные источники NuGet недопустимы: {0}. + + + + No NuGet sources are configured. + Нет настроенных источников NuGet. + + + + The operation was cancelled. + Операция отменена. + + + + {0} was not found in NuGet feeds {1}. + {0} не найден в веб-каналах NuGet {1}. + + + + The package {0} is not supported by installer {1}. + Пакет {0} не поддерживается установщиком {1}. + + + + Failed to uninstall the package {0}. +Details: {1}. + Не удалось удалить пакет {0}. +Сведения: {1}. + + + + Failed to check the update for the package {0}. +Details: {1}. + Не удалось проверить обновление для пакета {0}. +Сведения: {1}. + + + + The requested package {0} has vulnerabilities. + Запрошенный пакет {0} содержит уязвимости. + + + + The checked package {0} has vulnerabilities. + Выбранный пакет {0} содержит уязвимости. + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + «{0}» не является допустимым именем операционной системы. Допустимые значения: {1}. + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + Запуск шаблона на {0} не поддерживается, поддерживаемые операционные системы: {1}. + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + Операционная система + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + Расположение пакета шаблона {0} не поддерживается или не существует. + + + + '{0}' is not a valid semver version. + «{0}» не является допустимой версией SemVer. + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + Несколько компонентов ISdkInfoProvider, предоставляемых узлом ({0}), поэтому не удается правильно инициализировать SdkVersionConstraint. + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + Компонент ISdkInfoProvider не предоставляется узлом. Не удается правильно инициализировать SdkVersionConstraint. + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + Выполняемый шаблон в текущей версии пакета SDK для .NET ({0}) не поддерживается. Поддерживаемые версии: {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + Версия пакета SDK для .NET + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + Ограничение \"{0}\" не удалось оценить для аргументов \"{1}\", подробности: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + Ограничение \"{0}\" не удалось инициализировать: {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + Ограничение \"{0}\" неизвестно. + {0} is constraint type + + + Could not load template. + Загрузить шаблон не удалось. + + + + Failed to create template. +Details: {0} + Не удалось создать шаблон. +Сведения: {0} + + + + Destructive changes detected. + Обнаружены необратимые изменения. + + + + The template is invalid and cannot be instantiated. + Шаблон недопустим, и его экземпляр не может быть создан. + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + Не удалось создать шаблон: имя шаблона не указано. В конфигурации шаблона не настроено имя по умолчанию, которое можно использовать, если имя не указано. Укажите имя шаблона при создании экземпляра или настройте имя по умолчанию в конфигурации шаблона. + + + + Failed to load host data in {0} at {1}. + Не удалось загрузить данные узла в {0} в {1}. + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + Не удалось получить пакет с идентификатором "{0}". + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + Не удалось получить пакеты шаблонов у поставщика \"{0}\". +Сведения: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + Не удалось сканировать {0}. +Подробности: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + Не удалось сохранить кэш шаблонов. Сведения: {0} +Кэш шаблонов будет повторно создан при следующем запуске. + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +Следующие шаблоны используют одинаковые идентификаторы '{0}': +{1} +Будет использован шаблон '{2}'. Чтобы устранить этот конфликт, удалите конфликтующие пакеты шаблонов. + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {0} '{1}' из '{2}' + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + Для шаблона {0} получены следующие ошибки проверки: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + Для шаблона {0} получены следующие сообщения проверки: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + Не удалось загрузить шаблон {0}: недопустимый шаблон. + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + Не удалось загрузить локализацию "{0}" для шаблона {1}: недопустимый файл локализации. Локализация будет пропущена. + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + Для шаблона {0} получены следующие ошибки проверки в локализации "{1}": + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + Для шаблона {0} получены следующие сообщения проверки в локализации "{1}": + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + Для шаблона {0} получены следующие предупреждения проверки в локализации "{1}": + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + Для шаблона {0} получены следующие предупреждения проверки: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + Несколько компонентов IWorkloadsInfoProvider, предоставляемых узлом ({0}), поэтому не удается правильно инициализировать WorkloadConstraint. + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + Компонент IWorkloadsInfoProvider не предоставляется узлом. Не удается правильно инициализировать WorkloadConstraint. + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + Выполняемый шаблон не поддерживается — необходимые дополнительные рабочие нагрузки не установлены. Поддерживаемые рабочие нагрузки: {0}. В настоящее время установлены дополнительные рабочие нагрузки: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + Рабочая нагрузка + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + Компонент «IWorkloadsInfoProvider», предоставленный узлом, предоставил несколько дублированных рабочих нагрузок (дубликаты: {0}). Дубликаты будут пропущены. + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.tr.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.tr.xlf new file mode 100644 index 000000000000..45973ae443d5 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.tr.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + Parametre koşulları, belirlenimci değerlendirmeyi engelleyen döngüsel bağımlılık içeriyor: [{0}] + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + {1} parametresinde {0} koşulu değerlendirilemedi (koşul metni: {2}, değerlendirme hatası: {3}). Koşul hatalı biçimlendirilmiş olabilir ya da başvurulan parametreler varsayılan veya açık değerleri içermiyor. + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + Beklenmeyen iç hata. Döngüsel bağımlılıkları olmadığı anlaşılan parametre bağımlılıklarının topolojik sıralaması gerçekleştirilemiyor. + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + Parametre koşulları döngüsel bağımlılık içeriyor: [{0}]. Parametrelerin geçerli değerleriyle parametrelerin belirlenimci değerlendirmesini gerçekleştirmek mümkün olduğu için devam ediliyor. Ancak farklı parametrelerle örnek oluşturma, hataya neden olabileceğinden şablon gözden geçiriliyor. + {0} is the dependency chain + + + '{0}' should not contain empty items + '{0}' boş öğe içermemelidir + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + Bağımsız değişkenler belirtilmedi. En az bir bağımsız değişken belirtilmelidir. + + + + '{0}' does not contain valid items. + '{0}' geçerli öğeler içermiyor. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + '{0}' geçerli bir JSON değil. + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + '{0}' bir nesne dizisi olmalıdır. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + '{0}' geçerli bir JSON dizisi değil. + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + '{0}' geçerli bir JSON dizesi veya dizisi değil. + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + '{0}' geçerli bir sürüm veya sürüm aralığı değil. + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + template.json dosyasındaki kısıtlama yapılandırmasını kontrol edin. + + + + Environment variables + Ortam değişkenleri + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + Şablonda (IsEnabled veya IsRequired öznitelikleri koşulla doldurulmamış) uygun koşul kümesine sahip olmayan parametre(ler) için parametre koşullarının dış değerlendirme sonucu veya şablonda koşulları olan parametrelerin koşul sonuçlarını geçiremedik. Sorunlu parametreler: {0}. + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + Şablonda uygun koşul kümesine sahip olmayan (IsEnabled veya IsRequired öznitelikleri koşulla doldurulmamış) parametre(ler) için parametrelerin koşullarının dış değerlendirme sonucunu geçirme girişimi veya şablonda koşulları olan parametrelerin koşul sonuçlarını geçirme başarısız. Sorunlu parametre(ler): {0}. + + + + The folder {0} doesn't exist. + {0} klasörü yok. + + + + Check the constraint configuration in template.json. + template.json dosyasındaki kısıtlama yapılandırmasını kontrol edin. + + + + latest version + en son sürüm + small letters, string is used in the sentence + + + version {0} + sürüm {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + {0}, birkaç yükleyiciyle yüklenebilir. Kullanılacak yükleyici adını belirtin. + + + + {0} is already installed. + {0} zaten yüklendi. + + + + {0} cannot be installed. + {0} yüklenemiyor. + + + + {0} is already installed, it will be replaced with {1}. + {0} zaten yüklü, {1} ile değiştirilecek. + + + + {0} was successfully uninstalled. + {0} başarıyla kaldırıldı. + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + {0} konumundaki yüklü şablon paketleri listesi okunamadı. Bunun nedeni dosyanın bozuk olması olabilir. Lütfen bu dosyayı el ile gözden geçirin ve JSON yapısındaki hataları düzeltin veya yüklü paketlerin listesini temizlemek ve yeniden yüklemek için dosyayı kaldırın. Hatanın ayrıntıları: {1}. + + + + '{0}' does not have mandatory property '{1}'. + '{0}', zorunlu '{1}' özelliğine sahip değil. + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + Şablonu {0} konağı üzerinde çalıştırma (sürüm: {1}) desteklenmiyor, desteklenen konak/konaklar: {2}. + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + Şablon altyapısı ana bilgisayarı + + + + Host defined parameters + Ana bilgisayar tanımlı parametreler + + + + Found package is vulnerable source: {0} + Bulunan paket savunmasız kaynak: {0} + + + + Failed to load the NuGet source {0}. + {0} NuGet kaynağı yüklenemedi. + + + + Failed to load NuGet sources configured for the folder {0}. + {0} klasörü için yapılandırılan NuGet kaynakları yüklenemedi. + + + + Failed to read package information from NuGet source {0}. + Paket bilgileri {0} NuGet kaynağından okunamadı. + + + + File {0} already exists. + {0} dosyası zaten var. + + + + No NuGet sources are defined or enabled. + Tanımlanmış veya etkinleştirilmiş NuGet kaynağı yok. + + + + The checked package {0} is vulnerable. + Denetlenen {0} paketi savunmasız. + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + Başarısız indirme işleminden sonra {0} kaldırılamadı. Varsa, dosyayı el ile kaldırın. + + + + Failed to download {0} from NuGet feed {1}. + {0}, {1} NuGet akışından indirilemedi. + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + {0} NuGet kaynağı yüklenemedi: kaynak geçerli değil. Daha fazla işlemede atlanacak. + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + {0} NuGet kaynakları güvenli değil ve aranmaz. Bu kaynakları aramaya dahil etmek istiyorsanız --force komutunu kullanın. + + + + {0} is not found in NuGet feeds {1}. + {0}, {1} NuGet akışlarında bulunamadı. + + + + Failed to copy package {0} to {1}. + {0} paketi {1} konumuna kopyalanamadı. + + + + Failed to read content of package {0}. + {0} paketinin içeriği okunamadı. + + + + File {0} already exists. + {0} dosyası zaten var. + + + + Failed to download {0} from {1}. + {0}, {1} kaynağından indirilemedi. + + + + Failed to install the package {0}. +Details: {1}. + {0} paketi yüklenemedi. +Ayrıntılar: {1}. + + + + The install request {0} cannot be processed by installer {1}. + {0} yükleme isteği {1} yükleyicisi tarafından işlenemiyor. + + + + The NuGet package {0} is invalid. + {0} NuGet paketi geçersiz. + + + + The configured NuGet sources are invalid: {0}. + Yapılandırılan NuGet kaynakları geçersiz: {0}. + + + + No NuGet sources are configured. + Hiçbir NuGet kaynağı yapılandırılmadı. + + + + The operation was cancelled. + İşlem iptal edildi. + + + + {0} was not found in NuGet feeds {1}. + {0}, {1} NuGet akışlarında bulunamadı. + + + + The package {0} is not supported by installer {1}. + {0} paketi, {1} yükleyicisi tarafından desteklenmiyor. + + + + Failed to uninstall the package {0}. +Details: {1}. + {0} paketi yüklenemedi. +Ayrıntılar: {1}. + + + + Failed to check the update for the package {0}. +Details: {1}. + {0} paketi için güncelleştirme denetimi başarısız oldu. +Ayrıntılar: {1}. + + + + The requested package {0} has vulnerabilities. + İstenen {0} paketinde güvenlik açıkları var. + + + + The checked package {0} has vulnerabilities. + Denetlenen {0} paketinde güvenlik açıkları var. + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + '{0}' geçerli bir işletim sistemi adı değil. İzin verilen değerler: {1}. + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + Şablonu {0} işletim sistemi üzerinde çalıştırma desteklenmiyor, desteklenen işletim sistemi/sistemleri: {1}. + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + İşletim Sistemi + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + Şablon paketi konumu {0} desteklenmiyor veya yok. + + + + '{0}' is not a valid semver version. + '{0}' geçerli bir semver sürümü değil. + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + Ana bilgisayar ({0}) tarafından sağlanan birden çok 'ISdkInfoProvider' bileşeni bulunuyor. Dolayısıyla 'SdkVersionConstraint' düzgün başlatılamıyor. + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + Ana bilgisayar tarafından sağlanan 'ISdkInfoProvider' bileşeni yok. 'SdkVersionConstraint' düzgün başlatılamıyor. + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + Geçerli .NET SDK sürümünde ({0}) çalışan şablon desteklenmiyor. Desteklenen sürüm(ler): {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + .NET SDK sürümü + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + Kısıtlama '{0}', bağımsız değişkenler '{1}' için değerlendirilemedi, ayrıntılar: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + Kısıtlama '{0}', başlatılamadı: {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + Kısıtlama '{0}' bilinmiyor. + {0} is constraint type + + + Could not load template. + Şablon yüklenemedi. + + + + Failed to create template. +Details: {0} + Şablon oluşturulamadı. +Ayrıntılar: {0}. + + + + Destructive changes detected. + Zararlı değişiklikler algılandı. + + + + The template is invalid and cannot be instantiated. + Şablon geçersiz ve örneği oluşturulamıyor. + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + Şablon oluşturulamadı: Şablon adı belirtilmedi. Şablon yapılandırması, ad belirtilmediğinde kullanılabilecek bir varsayılan ad yapılandırmıyor. Nesne örneği oluştururken şablonun adını belirtin veya şablon yapılandırmasında bir varsayılan ad yapılandırın. + + + + Failed to load host data in {0} at {1}. + {1} konumundaki {0} içinde yer alan konak verileri yüklenemedi. + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + '{0}' tanımlayıcısına sahip paket alınamadı. + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + Şablon paketleri '{0}' sağlayıcısından alınamadı. +Ayrıntılar: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + {0} taranamadı. +Ayrıntılar: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + Şablon önbelleği depolanamadı, ayrıntılar: {0} +Şablon önbelleği sonraki çalıştırmada yeniden oluşturulacak. + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +Aşağıdaki şablonlar aynı '{0}' kimliğini kullanıyor: +{1} +'{2}' paketindeki şablon kullanılacak. Bu çakışmayı çözmek için çakışan şablon paketlerini kaldırın. + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {0} '{2}' paketindeki '{1}' şablonu + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + {0} şablonu aşağıdaki doğrulama hatalarına sahip: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + {0} şablonu aşağıdaki doğrulama iletilerine sahip: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + {0} şablonu yüklenemedi: Şablon geçerli değil. + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + {1} şablonunun '{0}' yerelleştirmesi yüklenemedi: yerelleştirme dosyası geçerli değil. Yerelleştirme atlanacak. + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + {0} şablonu {1} yerelleştirmesinde aşağıdaki doğrulama hatalarına sahip: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + {0} şablonu {1} yerelleştirmesinde aşağıdaki doğrulama iletilerine sahip: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + {0} şablonu {1} yerelleştirmesinde aşağıdaki doğrulama uyarılarına sahip: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + {0} şablonu aşağıdaki doğrulama uyarılarına sahip: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + Ana bilgisayar ({0}) tarafından sağlanan birden çok 'IWorkloadsInfoProvider' bileşeni bulunuyor. Dolayısıyla 'WorkloadConstraint' düzgün başlatılamıyor. + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + Ana bilgisayar tarafından sağlanan 'IWorkloadsInfoProvider' bileşeni yok. 'WorkloadConstraint' düzgün başlatılamıyor. + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + Şablon çalıştırma desteklenmiyor. Gerekli isteğe bağlı iş yükleri yüklü değil. Desteklenen iş yükleri: {0}. Şu anda yüklü isteğe bağlı iş yükleri: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + İş Yükü + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + Ana bilgisayar tarafından sağlanan 'IWorkloadsInfoProvider' bileşeni, bazı yinelenen iş yükleri sağladı (yinelenenler: {0}). Yinelenenler atlanacak. + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.zh-Hans.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.zh-Hans.xlf new file mode 100644 index 000000000000..080fdad08910 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.zh-Hans.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + 参数条件包含阻止确定性计算的循环依赖项: [{0}]。 + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + 无法评估参数 {1} 上的条件 {0} (条件文本: {2},评估错误: {3}) - 条件可能格式不正确,或引用的参数没有默认或显式值。 + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + 意外的内部错误 - 无法执行似乎没有循环依赖项的参数依赖项的拓扑排序。 + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + 参数条件包含循环依赖项: [{0}]。对于参数的当前值,可以确定性地计算参数,因此可以继续操作。但是,应该审阅该模板,因为具有不同参数的实例化可能会导致错误。 + {0} is the dependency chain + + + '{0}' should not contain empty items + “{0}”不应包含空项 + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + 未指定参数。应至少指定一个参数。 + + + + '{0}' does not contain valid items. + \"{0}\" 不包含有效项。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + \"{0}\" 不是有效的 JSON。 + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + \"{0}\" 应为对象数组。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + \"{0}\" 不是有效的 JSON 数组。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + \"{0}\" 不是有效的 JSON 字符串或数组。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + \"{0}\" 不是有效的版本范围。 + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + 检查 template.json 中的约束配置。 + + + + Environment variables + 环境变量 + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + 尝试为未在模板中设置适当条件(IsEnabled 或 IsRequired 属性未填充条件)的参数传递参数条件的外部计算结果,或者无法传递模板中具有条件的参数的条件结果。有问题的参数: {0}。 + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + 尝试为未在模板中设置适当条件(IsEnabled 或 IsRequired 属性未填充条件)的参数传递参数条件的外部计算结果,或者无法传递模板中具有条件的参数的条件结果。有问题的参数: {0}。 + + + + The folder {0} doesn't exist. + 文件夹 {0} 不存在。 + + + + Check the constraint configuration in template.json. + 检查 template.json 中的约束配置。 + + + + latest version + 最新版本 + small letters, string is used in the sentence + + + version {0} + 版本 {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + 可由多个安装程序安装 {0}。请指定要使用的安装程序名称。 + + + + {0} is already installed. + {0} 已安装。 + + + + {0} cannot be installed. + 无法安装 {0}。 + + + + {0} is already installed, it will be replaced with {1}. + 已安装 {0},它将替换为 {1}。 + + + + {0} was successfully uninstalled. + 已成功卸载 {0}。 + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + 无法在 {0} 中读取已安装的模板包列表。原因可能是文件已损坏。请手动查看此文件并修复 JSON 结构中的错误,或删除该文件以清除已安装的列表包列表并重新安装它们。错误的详细信息: {1}。 + + + + '{0}' does not have mandatory property '{1}'. + \"{0}\" 没有必需属性 \"{1}\" 。 + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + 不支持在 {0} (版本: {1})上运行模板,支持的主机为: {2}。 + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + 模板引擎主机 + + + + Host defined parameters + 主机定义的参数 + + + + Found package is vulnerable source: {0} + 发现包是漏洞源: {0} + + + + Failed to load the NuGet source {0}. + 无法加载 NuGet 源 {0}。 + + + + Failed to load NuGet sources configured for the folder {0}. + 无法加载为文件夹 {0} 配置的 NuGet 源 + + + + Failed to read package information from NuGet source {0}. + 无法从 NuGet 源 {0} 读取包信息 + + + + File {0} already exists. + 文件 {0} 已存在。 + + + + No NuGet sources are defined or enabled. + 未定义或启用任何 NuGet 源。 + + + + The checked package {0} is vulnerable. + 选中的包 {0} 易受攻击。 + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + 下载失败后未能删除 {0}。如果该文件存在,请手动将其删除。 + + + + Failed to download {0} from NuGet feed {1}. + 无法从 NuGet 源 {1} 中下载文件 {0}。 + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + 无法加载 NuGet 源 {0}: 源无效。进一步处理中将跳过它。 + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + NuGet 源 {0} 不安全,不会进行搜索。如果要包含这些源进行搜索,请使用 --force。 + + + + {0} is not found in NuGet feeds {1}. + 在 NuGet 源 {1} 中找不到 {0}。 + + + + Failed to copy package {0} to {1}. + 无法将包 {0} 复制到 {1}。 + + + + Failed to read content of package {0}. + 未能读取包 {0} 的内容。 + + + + File {0} already exists. + 文件 {0} 已存在。 + + + + Failed to download {0} from {1}. + 未能从 {1} 下载 {0}。 + + + + Failed to install the package {0}. +Details: {1}. + 未能安装包 {0}。 +详细信息: {1}。 + + + + The install request {0} cannot be processed by installer {1}. + 安装程序 {1} 无法处理安装请求 {0}。 + + + + The NuGet package {0} is invalid. + NuGet 包 {0} 无效。 + + + + The configured NuGet sources are invalid: {0}. + 配置的 NuGet 源无效: {0}。 + + + + No NuGet sources are configured. + 未配置 NuGet 源。 + + + + The operation was cancelled. + 操作被取消。 + + + + {0} was not found in NuGet feeds {1}. + 在 NuGet 源 {1} 中未找到 {0}。 + + + + The package {0} is not supported by installer {1}. + 安装程序 {1} 不支持包 {0}。 + + + + Failed to uninstall the package {0}. +Details: {1}. + 未能卸载包 {0}。 +详细信息: {1}。 + + + + Failed to check the update for the package {0}. +Details: {1}. + 无法检查包 {0} 的更新。 +详细信息: {1}。 + + + + The requested package {0} has vulnerabilities. + 请求的包 {0} 具有漏洞。 + + + + The checked package {0} has vulnerabilities. + 选中的包 {0} 具有漏洞。 + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + \"{0}\" 不是有效的操作系统名称。允许的值为: {1}。 + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + 不支持在 {0} 上运行模板,支持的 OS 为: {1}。 + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + 操作系统 + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + 模板包位置 {0} 不受支持或不存在。 + + + + '{0}' is not a valid semver version. + “{0}”不是有效的 semver 版本。 + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + 主机({0})已提供多个 “ISdkInfoProvider” 组件,因此无法正确初始化 “SdkVersionConstraint”。 + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + 主机未提供 “ISdkInfoProvider” 组件。无法正确初始化 “SdkVersionConstraint”。 + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + 不支持在当前 .NET SDK 版本({0})上运行模板。支持的版本: {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + .NET SDK 版本 + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + 无法对约束 \"{0}\" 的参数 \"{1}\" 进行计算,详细信息: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + 约束 \"{0}\" 初始化失败: {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + 约束 \"{0}\" 未知。 + {0} is constraint type + + + Could not load template. + 无法加载模板。 + + + + Failed to create template. +Details: {0} + 未能创建模板。 +详细信息: {0}。 + + + + Destructive changes detected. + 检测到破坏性更改。 + + + + The template is invalid and cannot be instantiated. + 该模板无效,无法实例化。 + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + 无法创建模板: 未指定模板名称。模板配置不会配置在未指定名称时可以使用的默认名称。在模板配置中实例化或配置默认名称时,请指定模板的名称。 + + + + Failed to load host data in {0} at {1}. + 无法在 {1} 的 {0} 中加载主机数据。 + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + 无法检索标识符为 "{0}" 的包。 + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + 未能从提供程序“{0}”检索模板包。 +详细信息: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + 未能扫描 {0}。 + 详细信息: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + 未能存储模板缓存,详细信息: {0} +将在下次运行时重新创建模板缓存。 + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +以下模板使用相同的标识“{0}”: +{1} +将使用“{2}”中的模板。要解决此冲突,请卸载冲突的模板包。 + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {0}来自“{2}”的“{1}” + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + 该模板 {0} 存在以下验证错误: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + 该模板 {0} 具有以下验证消息: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + 未能加载模板 {0}: 模板无效。 + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + 无法加载“{0}”本地化模板 {1}: 本地化文件无效。将跳过本地化。 + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + 模板 {0} 在“{1}”本地化中存在以下验证错误: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + 模板 {0} 在“{1}”本地化中具有以下验证消息: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + 模板 {0} 在“{1}”本地化中具有以下验证警告: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + 该模板 {0} 具有以下验证警告: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + 主机({0})已提供多个 “IWorkloadsInfoProvider” 组件,因此无法正确初始化 “WorkloadConstraint”。 + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + 主机未提供 “IWorkloadsInfoProvider” 组件。无法正确初始化 “WorkloadConstraint”。 + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + 不支持运行模板 - 未安装所需的可选工作负载。支持的工作负载: {0}。当前安装的可选工作负载: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + 工作负载 + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + 主机提供的 “IWorkloadsInfoProvider” 组件提供了一些重复的工作负载(重复项: {0})。将跳过重复项。 + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.zh-Hant.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.zh-Hant.xlf new file mode 100644 index 000000000000..e0783c7b9b3e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Edge/xlf/LocalizableStrings.zh-Hant.xlf @@ -0,0 +1,515 @@ + + + + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + 參數條件包含迴圈相依性: [{0}],導致無法進行確定性評估。 + {0} is the dependency chain + + + Failed to evaluate condition {0} on parameter {1} (condition text: {2}, evaluation error: {3}) - condition might be malformed or referenced parameters do not have default nor explicit values. + 無法評估參數 {1} 的條件 {0} (條件文字: {2},評估錯誤: {3}) - 條件的格式可能不正確或參照的參數沒有預設值或明確值。 + {0} is the condition type +{1} is parameter name +{2} is condition text +{3} is evaluation error message + + + Unexpected internal error - unable to perform topological sort of parameter dependencies that do not appear to have a cyclic dependencies. + 未預期的內部錯誤 - 無法執行似乎沒有迴圈相依性的參數相依性拓撲排序。 + + + + Parameter conditions contain cyclic dependency: [{0}]. With current values of parameters it's possible to deterministically evaluate parameters - so proceeding further. However template should be reviewed as instantiation with different parameters can lead to error. + 參數條件包含迴圈相依性: [{0}]。使用目前的參數值,可以確定性地評估參數,所以繼續進行。不過,應該檢閱範本,因為使用不同參數具現化可能會導致錯誤。 + {0} is the dependency chain + + + '{0}' should not contain empty items + '{0}' 不應包含空白項目 + {0} is JSON configuration for constraint + + + Argument(s) were not specified. At least one argument should be specified. + 未指定引數。至少應該指定一個引數。 + + + + '{0}' does not contain valid items. + '{0}' 不包含有效的項目。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON. + '{0}' 是無效的 JSON。 + {0} is JSON configuration for constraint + + + '{0}' should be an array of objects. + '{0}' 應為物件陣列。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON array. + '{0}' 不是有效的 JSON 陣列。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid JSON string or array. + '{0}' 不是有效的 JSON 字串或陣列。 + {0} is JSON configuration for constraint + + + '{0}' is not a valid version or version range. + '{0}' 不是有效的版本或版本範圍。 + {0} is version range specified in JSON configuration + + + Check the constraint configuration in template.json. + 檢查 template.json 中的限制式設定。 + + + + Environment variables + 環境變數 + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameters: {0}. + 嘗試針對未在範本 (IsEnabled 或 IsRequired 屬性未填入條件) 中設定適當條件的參數傳遞參數條件的外部評估結果,或無法為範本中具有條件的參數傳遞條件結果。違規參數: {0}。 + + + + Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition) or a failure to pass the condition results for parameters with condition(s) in template. Offending parameter(s): {0}. + 嘗試針對未在範本 (IsEnabled 或 IsRequired 屬性未填入條件) 中設定適當條件的參數傳遞參數條件的外部評估結果,或無法為範本中具有條件的參數傳遞條件結果。違規參數: {0}。 + + + + The folder {0} doesn't exist. + 資料夾 {0} 不存在。 + + + + Check the constraint configuration in template.json. + 檢查 template.json 中的限制式設定。 + + + + latest version + 最新版本 + small letters, string is used in the sentence + + + version {0} + 版本 {0} + small letters, string is used in the sentence + + + {0} can be installed by several installers. Specify the installer name to be used. + {0} 可由多個安裝程式安裝。指定要使用的安裝程式名稱。 + + + + {0} is already installed. + {0} 已安裝。 + + + + {0} cannot be installed. + 無法安裝 {0}。 + + + + {0} is already installed, it will be replaced with {1}. + {0} 已安裝,將會以 {1} 取代。 + + + + {0} was successfully uninstalled. + {0} 已成功解除安裝。 + + + + Failed to read installed template packages list at {0}. It might be due to the file is corrupted. Please review this file manually and fix the errors in JSON structure, or remove the file to clear up the list of list installed packages and reinstall them again. Details of the error: {1}. + 未能讀取 {0} 處的已安裝範本封裝清單。這可能是由於檔案已損毀。請手動檢閲此檔案並修正 JSON 結構中的錯誤,或者移除檔案以清除已安裝封裝的清單並重新安裝。錯誤的詳細資料: {1}. + + + + '{0}' does not have mandatory property '{1}'. + '{0}' 沒有強制屬性 '{1}'。 + {0} is JSON configuration, {1} mandatory JSON property name + + + Running template on {0} (version: {1}) is not supported, supported hosts is/are: {2}. + 不支援在 {0} (版本: {1}) 上執行範本,支援的主機為: {2}。 + {0} - host name ("dotnetcli", "ide"..) +{1} - host version (usually matches VS or SDK version) +{2} - comma-separated list of supported hosts and their versions (example: host1, host2(1.0.0), host3([1.0.0-*])) + + + Template engine host + 範本引擎主機 + + + + Host defined parameters + 主機定義的參數 + + + + Found package is vulnerable source: {0} + 找到的套件為易受攻擊的來源: {0} + + + + Failed to load the NuGet source {0}. + 無法載入 NuGet 來源 {0}。 + + + + Failed to load NuGet sources configured for the folder {0}. + 無法載入為資料夾 {0} 所設定的 NuGet 來源。 + + + + Failed to read package information from NuGet source {0}. + 無法從 NuGet 來源 {0} 讀取套件資訊。 + + + + File {0} already exists. + 檔案 {0} 已經存在。 + + + + No NuGet sources are defined or enabled. + 未定義或啟用 NuGet 來源。 + + + + The checked package {0} is vulnerable. + 核取的套件 {0} 易受攻擊。 + + + + Failed to remove {0} after failed download. Remove the file manually if it exists. + 下載失敗後無法移除 {0}。如果檔案存在,請手動加以移除。 + + + + Failed to download {0} from NuGet feed {1}. + 無法從 NuGet 摘要 {1} 下載 {0}。 + + + + Failed to load NuGet source {0}: the source is not valid. It will be skipped in further processing. + 無法載入 NuGet 來源 {0}: 來源無效。進一步處理時會跳過此情況。 + + + + The NuGet sources {0} are insecure and will not be searched. If you want to include those sources for search, use --force. + NuGet 來源 {0} 不安全,將不會搜尋。若要包含搜尋的來源,請使用 --force。 + + + + {0} is not found in NuGet feeds {1}. + 在 NuGet 摘要 {1} 中找不到 {0}。 + + + + Failed to copy package {0} to {1}. + 無法將套件 {0} 複製到 {1}。 + + + + Failed to read content of package {0}. + 無法讀取套件 {0} 的內容。 + + + + File {0} already exists. + 檔案 {0} 已經存在。 + + + + Failed to download {0} from {1}. + 無法從 {1} 下載 {0}。 + + + + Failed to install the package {0}. +Details: {1}. + 無法安裝套件 {0}。 +詳細資料: {1}。 + + + + The install request {0} cannot be processed by installer {1}. + 安裝程式 {1} 無法處理安裝要求 {0}。 + + + + The NuGet package {0} is invalid. + NuGet 套件 {0} 無效。 + + + + The configured NuGet sources are invalid: {0}. + 設定的 NuGet 來源無效: {0}。 + + + + No NuGet sources are configured. + 未設定任何 NuGet 來源。 + + + + The operation was cancelled. + 作業已取消。 + + + + {0} was not found in NuGet feeds {1}. + 在 NuGet 摘要 {1} 中找不到 {0}。 + + + + The package {0} is not supported by installer {1}. + 安裝程式 {1} 不支援安裝套件 {0}。 + + + + Failed to uninstall the package {0}. +Details: {1}. + 無法取消安裝套件 {0}。 +詳細資料: {1}。 + + + + Failed to check the update for the package {0}. +Details: {1}. + 無法檢查套件 {0} 的更新。 +詳細資料: {1} + + + + The requested package {0} has vulnerabilities. + 要求的套件 {0} 有弱點。 + + + + The checked package {0} has vulnerabilities. + 核取的套件 {0} 有弱點。 + + + + '{0}' is not a valid operating system name. Allowed values are: {1}. + '{0}' 不是有效的作業系統名稱。允許的值為: {1}。 + {0} - OS name in configuration +{1} - comma-separated list of allowed operating system names + + + Running template on {0} is not supported, supported OS is/are: {1}. + 不支援在 {0} 上執行範本,支援的作業系統為: {1}。 + {0} - name of the executing operating system, {1} - comma separated list of supported operating systems. + + + Operating System + 作業系統 + this is name of constraint that will appear in the UI + + + Template package location {0} is not supported, or doesn't exist. + 不支援範本套件位置 {0},或是其不存在。 + + + + '{0}' is not a valid semver version. + '{0}' 不是有效的 SemVer 版本。 + {0} is the provided version + + + Multiple 'ISdkInfoProvider' components provided by host ({0}), therefore 'SdkVersionConstraint' cannot be properly initialized. + 主機 ({0}) 提供多個 'ISdkInfoProvider' 元件,因此無法正確初始化 'SdkVersionConstraint'。 + {0} is the list of providers + + + No 'ISdkInfoProvider' component provided by host. 'SdkVersionConstraint' cannot be properly initialized. + 主機未提供 'ISdkInfoProvider' 元件。無法正確初始化 'SdkVersionConstraint'。 + + + + Running template on current .NET SDK version ({0}) is unsupported. Supported version(s): {1} + 不支援在目前的 .NET SDK 版本 ({0}) 上執行範本。支援的版本: {1} + {0} is the current SDK version, {1} is the list of supported SDK versions + + + .NET SDK version + .NET SDK 版本 + + + + The constraint '{0}' failed to be evaluated for the args '{1}', details: {2} + 無法為引數 '{1}' 評估限制式 '{0}',詳細資料: {2} + {0} - constraint type +{1} - arguments (in JSON format) +{2} - reason of failure (has the period). + + + The constraint '{0}' failed to initialize: {1} + 限制式 '{0}' 無法初始化: {1} + {0} is constraint type +{1} - reason of failure (has the period). + + + The constraint '{0}' is unknown. + 限制式 '{0}' 不明。 + {0} is constraint type + + + Could not load template. + 無法載入範本。 + + + + Failed to create template. +Details: {0} + 無法建立範本。 +詳細資料: {0}。 + + + + Destructive changes detected. + 偵測到破壞性變更。 + + + + The template is invalid and cannot be instantiated. + 範本無效,因此無法具現化。 + + + + Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration. + 無法建立範本: 未指定範本名稱。範本設定不會設定未指定名稱時可以使用的預設名稱。在範本設定中,於具現化或設定預設名稱時指定範本的名稱。 + + + + Failed to load host data in {0} at {1}. + 無法在 {1} 的 {0} 處載入主機資料。 + {0} - path to template location, {1} relative path inside template location. + + + Failed to retrieve package with identifier '{0}'. + 無法擷取識別碼為 '{0}' 的封裝。 + + + + Failed to retrieve template packages from provider '{0}'. +Details: {1} + 無法從提供者'{0}'撷取模板套件。 +詳細資料: {1} + {0} - provider name (not localized). Last line should not end with a period - period is already included in {1}. + + + Failed to scan {0}. +Details: {1} + 無法掃描 {0}。 +詳細資料: {1} + last line should not end with a period, period is a part of the format entry + + + Failed to store template cache, details: {0} +Template cache will be recreated on the next run. + 無法儲存範本快取{0} +詳細資料: 範本快取下次執行時將重新建立。 + {0} should not end with period - period is a part of the format entry. + + + +The following templates use the same identity '{0}': +{1} +The template from '{2}' will be used. To resolve this conflict, uninstall the conflicting template packages. + +下列範本使用相同的身分識別 '{0}': +{1} +將使用來自 '{2}' 的範本。若要解決此衝突,請解除安裝發生衝突的範本套件。 + +The following templates use the same identity 'IDENTITY': + * 'TEMPLATE' from 'PACKAGE_ID' +The template from 'PACKAGE_ID' will be used. To resolve this conflict, uninstall the conflicting template packages. + + + + {0} '{1}' from '{2}' + {0} '{1}' 來自 '{2}' + * 'TEMPLATE' from 'PACKAGE_ID' + + + The template {0} has the following validation errors: + 範本 {0} 具有以下驗證錯誤: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + The template {0} has the following validation messages: + 範本 {0} 具有以下驗證訊息: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Failed to load the template {0}: the template is not valid. + 無法載入範本 {0}: 範本無效。 + {0} template info in format 'template name' (template.identity) + + + Failed to load the '{0}' localization the template {1}: the localization file is not valid. The localization will be skipped. + 無法載入範本 {1} 的 '{0}' 當地語系化: 當地語系化檔案無效。將略過該當地語系化。 + {1} template info in format 'template name' (template.identity), {0} is localization locale in "en-US" format + + + The template {0} has the following validation errors in '{1}' localization: + 範本 {0} 在 '{1}' 當地語系化中存在以下驗證錯誤: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation messages in '{1}' localization: + 範本 {0} 在 '{1}' 當地語系化中存在以下驗證訊息: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings in '{1}' localization: + 範本 {0} 在 '{1}' 當地語系化中存在以下驗證警: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity), {1} is localization locale in "en-US" format + + + The template {0} has the following validation warnings: + 範本 {0} 具有以下驗證警告: + This is a header for the list of issues found for the template, and followed by the list of issues (from new line). {0} template info in format 'template name' (template.identity) + + + Multiple 'IWorkloadsInfoProvider' components provided by host ({0}), therefore 'WorkloadConstraint' cannot be properly initialized. + 主機 ({0}) 提供多個 'IWorkloadsInfoProvider' 元件,因此無法正確初始化 'WorkloadConstraint'。 + {0} is list of ids of providers + + + No 'IWorkloadsInfoProvider' component provided by host. 'WorkloadConstraint' cannot be properly initialized. + 主機未提供 'IWorkloadsInfoProvider' 元件。無法正確初始化 'WorkloadConstraint'。 + + + + Running template is not supported - required optional workload(s) not installed. Supported workload(s): {0}. Currently installed optional workloads: {1} + 不支援執行中的範本 - 未安裝必要的選用工作負載。支援的工作負載: {0}。目前安裝的選用工作負載: {1} + {0} is list of supported workloads. {1} is list of installed optional workloads + + + Workload + 工作負載 + Terminus Technicus - most likely not to be translated (https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-workload-install) + + + 'IWorkloadsInfoProvider' component provided by host provided some duplicated workloads (duplicates: {0}). Duplicates will be skipped. + 主機提供的 'IWorkloadsInfoProvider' 元件提供了一些重複的工作負載 (重複: {0})。將略過重複項目。 + {0} is the list of duplicates + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.IDE/Bootstrapper.cs b/src/TemplateEngine/Microsoft.TemplateEngine.IDE/Bootstrapper.cs new file mode 100644 index 000000000000..dac2e89bff08 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.IDE/Bootstrapper.cs @@ -0,0 +1,467 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +#if NET7_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; +using Microsoft.TemplateEngine.Edge; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.Edge.Template; +using ITemplateCreationResult = Microsoft.TemplateEngine.Edge.Template.ITemplateCreationResult; +using ITemplateMatchInfo = Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo; +using MatchInfo = Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo; +using TemplateCreator = Microsoft.TemplateEngine.Edge.Template.TemplateCreator; +using WellKnownSearchFilters = Microsoft.TemplateEngine.Utils.WellKnownSearchFilters; + +namespace Microsoft.TemplateEngine.IDE +{ + public class Bootstrapper : IDisposable + { + private readonly TemplateCreator _templateCreator; + private readonly TemplatePackageManager _templatePackagesManager; + private readonly EngineEnvironmentSettings _engineEnvironmentSettings; + + /// + /// Creates the instance. + /// + /// caller . + /// if true, settings will be stored in memory and will be disposed with instance. + /// if true, the default components (providers, installers, generator) will be loaded. Same as calling after instance is created. + /// the file path to store host specific settings. Use null for default location. + /// Note: this parameter changes only directory of host and host version specific settings. Global settings path remains unchanged. + /// optional environment to be used (defaults to ). + public Bootstrapper( + ITemplateEngineHost host, + bool virtualizeConfiguration, + bool loadDefaultComponents = true, + string? hostSettingsLocation = null, + IEnvironment? environment = null) + { + _ = host ?? throw new ArgumentNullException(nameof(host)); + environment ??= new DefaultEnvironment(); + + if (string.IsNullOrWhiteSpace(hostSettingsLocation)) + { + _engineEnvironmentSettings = new EngineEnvironmentSettings(host, virtualizeSettings: virtualizeConfiguration, environment: environment); + } + else + { + string hostSettingsDir = Path.Combine(hostSettingsLocation, host.HostIdentifier); + string hostVersionSettingsDir = Path.Combine(hostSettingsLocation, host.HostIdentifier, host.Version); + IPathInfo pathInfo = new DefaultPathInfo(environment, host, hostSettingsDir: hostSettingsDir, hostVersionSettingsDir: hostVersionSettingsDir); + _engineEnvironmentSettings = new EngineEnvironmentSettings( + host, + virtualizeSettings: virtualizeConfiguration, + environment: environment, + pathInfo: pathInfo); + } + + _templateCreator = new TemplateCreator(_engineEnvironmentSettings); + _templatePackagesManager = new TemplatePackageManager(_engineEnvironmentSettings); + if (loadDefaultComponents) + { + LoadDefaultComponents(); + } + } + + /// + /// Loads default components: template package providers and installers defined in Microsoft.TemplateEngine.Edge and default template generator defined in Microsoft.TemplateEngine.Orchestrator.RunnableProjects. + /// + public void LoadDefaultComponents() + { + foreach ((Type Type, IIdentifiedComponent Instance) component in Orchestrator.RunnableProjects.Components.AllComponents) + { + AddComponent(component.Type, component.Instance); + } + foreach ((Type Type, IIdentifiedComponent Instance) component in Components.AllComponents) + { + AddComponent(component.Type, component.Instance); + } + } + + /// + /// Adds component to manager, which can be looked up later via or . + /// Added components are not persisted and need to be called every time new instance of is created. + /// + /// Interface type that added component implements. + /// Instance of type that implements . + public void AddComponent(Type interfaceType, IIdentifiedComponent component) + { + _engineEnvironmentSettings.Components.AddComponent(interfaceType, component); + } + + /// + /// Gets list of all available templates. + /// + /// + /// The list of all available templates. + public Task> GetTemplatesAsync(CancellationToken cancellationToken) + { + return _templatePackagesManager.GetTemplatesAsync(cancellationToken); + } + + /// + /// Gets list of available templates, if is provided returns only matching templates. + /// + /// List of filters to apply. See for predefined filters. + /// + /// true: templates should match all filters; false: templates should match any filter. + /// + /// + /// Filtered list of available templates with details on the applied filters matches. + public Task> GetTemplatesAsync(IEnumerable> filters, bool exactMatchesOnly = true, CancellationToken cancellationToken = default) + { + Func criteria; + if (filters == null || !filters.Any()) + { + // returns all templates + criteria = (t) => true; + filters ??= []; + } + else + { + criteria = exactMatchesOnly ? WellKnownSearchFilters.MatchesAllCriteria : WellKnownSearchFilters.MatchesAtLeastOneCriteria; + } + + return _templatePackagesManager.GetTemplatesAsync(criteria, filters, cancellationToken); + } + + /// + /// Instantiates the template. + /// + /// The template to instantiate. + /// The name to use. + /// The output directory for template instantiation. + /// The template parameters. + /// The baseline configuration to use. + /// A cancellation token to cancel the asynchronous operation. + /// containing information on created template or error occurred. +#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads + public Task CreateAsync( + ITemplateInfo info, + string? name, + string outputPath, + IReadOnlyDictionary parameters, + string? baselineName = null, + CancellationToken cancellationToken = default) +#pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads + { + return _templateCreator.InstantiateAsync( + info, + name, + fallbackName: null, + outputPath: outputPath, + inputParameters: parameters, + forceCreation: false, + baselineName: baselineName, + dryRun: false, + cancellationToken: cancellationToken); + } + + /// + /// Instantiates the template. + /// + /// The template to instantiate. + /// The name to use. + /// The output directory for template instantiation. + /// The template parameters. + /// The baseline configuration to use. + /// A cancellation token to cancel the asynchronous operation. + /// containing information on created template or error occurred. +#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public Task CreateAsync( + ITemplateInfo info, + string? name, + string outputPath, + InputDataSet? inputParameters, + string? baselineName = null, + CancellationToken cancellationToken = default) +#pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + { + return _templateCreator.InstantiateAsync( + info, + name, + fallbackName: null, + outputPath: outputPath, + inputParameters: inputParameters, + forceCreation: false, + baselineName: baselineName, + dryRun: false, + cancellationToken: cancellationToken); + } + + /// + /// Dry runs the template with given parameters. + /// + /// The template to instantiate. + /// The name to use. + /// The output directory for template instantiation. + /// The template parameters. + /// The baseline configuration to use. + /// A cancellation token to cancel the asynchronous operation. + /// containing information on template that would be created or error occurred. + public Task GetCreationEffectsAsync( + ITemplateInfo info, + string? name, + string outputPath, + IReadOnlyDictionary parameters, + string? baselineName = null, + CancellationToken cancellationToken = default) + { + return _templateCreator.InstantiateAsync( + info, + name, + fallbackName: null, + outputPath: outputPath, + inputParameters: parameters, + forceCreation: false, + baselineName: baselineName, + dryRun: true, + cancellationToken: cancellationToken); + } + + #region Template Package Management + + /// + /// Gets the list of available template packages. + /// + /// + /// the list of the template packages. + public Task> GetTemplatePackagesAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return _templatePackagesManager.GetTemplatePackagesAsync(false, cancellationToken); + } + + /// + /// Gets the list of available managed template packages. + /// + /// + /// the list of managed template packages. + public Task> GetManagedTemplatePackagesAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return _templatePackagesManager.GetManagedTemplatePackagesAsync(false, cancellationToken); + } + + /// + /// Installs the template packages + /// The following template packages are supported by default: + /// - the NuGet package from NuGet feed + /// - the NuGet package available at the path + /// - the folder containing the template. + /// + /// the list of to install. + /// to use. + /// + /// the list of containing installation result for each . + public Task> InstallTemplatePackagesAsync(IEnumerable installRequests, InstallationScope scope = InstallationScope.Global, CancellationToken cancellationToken = default) + { + _ = installRequests ?? throw new ArgumentNullException(nameof(installRequests)); + cancellationToken.ThrowIfCancellationRequested(); + + if (!installRequests.Any()) + { + return Task.FromResult((IReadOnlyList)new List()); + } + + IManagedTemplatePackageProvider managedPackageProvider; + switch (scope) + { + case InstallationScope.Global: + default: + { + managedPackageProvider = _templatePackagesManager.GetBuiltInManagedProvider(InstallationScope.Global); + break; + } + } + + return managedPackageProvider.InstallAsync(installRequests, cancellationToken); + } + + /// + /// Gets the latest template package version for . + /// + /// the template packages to check the version for. + /// + /// the list of containing the result for each . + public async Task> GetLatestVersionsAsync(IEnumerable managedPackages, CancellationToken cancellationToken = default) + { + _ = managedPackages ?? throw new ArgumentNullException(nameof(managedPackages)); + cancellationToken.ThrowIfCancellationRequested(); + + if (!managedPackages.Any()) + { + return new List(); + } + + IEnumerable> requestsGroupedByProvider = managedPackages.GroupBy(package => package.ManagedProvider, package => package); + IReadOnlyList[] results = await Task.WhenAll(requestsGroupedByProvider.Select(packages => packages.Key.GetLatestVersionsAsync(packages, cancellationToken))).ConfigureAwait(false); + + return results.SelectMany(result => result).ToList(); + } + + /// + /// Updates the template packages to version specified in . + /// + /// the list of to perform. + /// + /// the list of containing the result for each . + public async Task> UpdateTemplatePackagesAsync(IEnumerable updateRequests, CancellationToken cancellationToken = default) + { + _ = updateRequests ?? throw new ArgumentNullException(nameof(updateRequests)); + cancellationToken.ThrowIfCancellationRequested(); + + if (!updateRequests.Any()) + { + return new List(); + } + + IEnumerable> requestsGroupedByProvider = updateRequests.GroupBy(request => request.TemplatePackage.ManagedProvider, request => request); + IReadOnlyList[] updateResults = await Task.WhenAll(requestsGroupedByProvider.Select(requests => requests.Key.UpdateAsync(requests, cancellationToken))).ConfigureAwait(false); + + return updateResults.SelectMany(result => result).ToList(); + } + + /// + /// Uninstalls the template packages. + /// + /// the list of to uninstall. + /// + /// the list of containing the result for each . + public async Task> UninstallTemplatePackagesAsync(IEnumerable managedPackages, CancellationToken cancellationToken = default) + { + _ = managedPackages ?? throw new ArgumentNullException(nameof(managedPackages)); + cancellationToken.ThrowIfCancellationRequested(); + + if (!managedPackages.Any()) + { + return new List(); + } + + IEnumerable> requestsGroupedByProvider = managedPackages.GroupBy(package => package.ManagedProvider, package => package); + IReadOnlyList[] uninstallResults = await Task.WhenAll(requestsGroupedByProvider.Select(packages => packages.Key.UninstallAsync(packages, cancellationToken))).ConfigureAwait(false); + + return uninstallResults.SelectMany(result => result).ToList(); + } + + #endregion Template Package Management + + public void Dispose() + { + _templatePackagesManager.Dispose(); + _engineEnvironmentSettings?.Dispose(); + } + + #region Obsolete + + [Obsolete("Use " + nameof(GetTemplatesAsync) + "instead")] + public async Task> ListTemplates(bool exactMatchesOnly, params Func[] filters) + { + return TemplateListFilter.FilterTemplates(await _templatePackagesManager.GetTemplatesAsync(default).ConfigureAwait(false), exactMatchesOnly, filters); + } + + [Obsolete("Use ITemplateEngineHost.BuiltInComponents or AddComponent to add components.")] + public void Register(Type type) + { + _engineEnvironmentSettings.Components.Register(type); + } + + [Obsolete("Use ITemplateEngineHost.BuiltInComponents or AddComponent to add components.")] +#if NET7_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "Obsolete method; Assembly.GetTypes() is inherently reflection-based.")] +#endif + public void Register(Assembly assembly) + { + _engineEnvironmentSettings.Components.RegisterMany(assembly.GetTypes()); + } + + [Obsolete("use Task> InstallTemplatePackagesAsync(IEnumerable installRequests, InstallationScope scope = InstallationScope.Global, CancellationToken cancellationToken = default) instead")] + public void Install(string path) + { + Install(new[] { path }); + } + + [Obsolete("use Task> InstallTemplatePackagesAsync(IEnumerable installRequests, InstallationScope scope = InstallationScope.Global, CancellationToken cancellationToken = default) instead")] + public void Install(params string[] paths) + { + Install((IEnumerable)paths); + } + + [Obsolete("use Task> InstallTemplatePackagesAsync(IEnumerable installRequests, InstallationScope scope = InstallationScope.Global, CancellationToken cancellationToken = default) instead")] + public void Install(IEnumerable paths) + { + _ = paths ?? throw new ArgumentNullException(nameof(paths)); + + if (!paths.Any()) + { + return; + } + + var installRequests = paths.Select(path => new InstallRequest(path)).ToList(); + Task> t = InstallTemplatePackagesAsync(installRequests); + t.Wait(); + } + + [Obsolete("use Task> UninstallTemplatePackagesAsync(IEnumerable managedPackages, CancellationToken cancellationToken = default) instead")] + public IEnumerable Uninstall(string path) + { + return Uninstall(new[] { path }); + } + + [Obsolete("use Task> UninstallTemplatePackagesAsync(IEnumerable managedPackages, CancellationToken cancellationToken = default) instead")] + public IEnumerable Uninstall(params string[] paths) + { + return Uninstall((IEnumerable)paths); + } + + [Obsolete("use Task> UninstallTemplatePackagesAsync(IEnumerable managedPackages, CancellationToken cancellationToken = default) instead")] + public IEnumerable Uninstall(IEnumerable paths) + { + _ = paths ?? throw new ArgumentNullException(nameof(paths)); + + if (!paths.Any()) + { + return []; + } + + var task = GetManagedTemplatePackagesAsync(); + task.Wait(); + var templatePackages = task.Result; + + var packagesToUninstall = new List(); + foreach (string path in paths) + { + packagesToUninstall.AddRange(templatePackages.Where(package => package.Identifier.Equals(path, StringComparison.OrdinalIgnoreCase))); + } + + Task> uninstallTask = UninstallTemplatePackagesAsync(packagesToUninstall); + uninstallTask.Wait(); + return uninstallTask.Result + .Where(result => result.Success) + .Select(result => result.TemplatePackage!.Identifier); + } + + [Obsolete("Use Task CreateAsync(ITemplateInfo info, string? name, string outputPath, IReadOnlyDictionary parameters, string? baselineName = null, CancellationToken cancellationToken = default) instead.")] + public async Task CreateAsync(ITemplateInfo info, string name, string outputPath, IReadOnlyDictionary parameters, bool skipUpdateCheck, string baselineName) + { + ITemplateCreationResult instantiateResult = await _templateCreator.InstantiateAsync(info, name, name, outputPath, parameters, false, baselineName).ConfigureAwait(false); + return instantiateResult.CreationResult; + } + + [Obsolete("Use Task GetCreationEffectsAsync(ITemplateInfo info, string? name, string outputPath, IReadOnlyDictionary parameters, string? baselineName = null, CancellationToken cancellationToken = default) instead.")] + public async Task GetCreationEffectsAsync(ITemplateInfo info, string name, string outputPath, IReadOnlyDictionary parameters, string baselineName) + { + ITemplateCreationResult instantiateResult = await _templateCreator.InstantiateAsync(info, name, name, outputPath, parameters, false, baselineName, true).ConfigureAwait(false); + return instantiateResult.CreationEffects; + } + + #endregion Obsolete + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.IDE/Microsoft.TemplateEngine.IDE.csproj b/src/TemplateEngine/Microsoft.TemplateEngine.IDE/Microsoft.TemplateEngine.IDE.csproj new file mode 100644 index 000000000000..7c17ac1e4746 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.IDE/Microsoft.TemplateEngine.IDE.csproj @@ -0,0 +1,23 @@ + + + + $(NetMinimum);$(NetCurrent);netstandard2.0;$(NetFrameworkMinimum) + Helper package for adding Template Engine to IDEs + true + true + true + + true + + + + + + + + + + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.IDE/PublicAPI.Shipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.IDE/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..453be566bdd7 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.IDE/PublicAPI.Shipped.txt @@ -0,0 +1,26 @@ +#nullable enable +Microsoft.TemplateEngine.IDE.Bootstrapper +Microsoft.TemplateEngine.IDE.Bootstrapper.AddComponent(System.Type! interfaceType, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! component) -> void +Microsoft.TemplateEngine.IDE.Bootstrapper.CreateAsync(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! info, string! name, string! outputPath, System.Collections.Generic.IReadOnlyDictionary! parameters, bool skipUpdateCheck, string! baselineName) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.IDE.Bootstrapper.CreateAsync(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! info, string? name, string! outputPath, System.Collections.Generic.IReadOnlyDictionary! parameters, string? baselineName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.IDE.Bootstrapper.Dispose() -> void +Microsoft.TemplateEngine.IDE.Bootstrapper.GetCreationEffectsAsync(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! info, string! name, string! outputPath, System.Collections.Generic.IReadOnlyDictionary! parameters, string! baselineName) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.IDE.Bootstrapper.GetCreationEffectsAsync(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! info, string? name, string! outputPath, System.Collections.Generic.IReadOnlyDictionary! parameters, string? baselineName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.IDE.Bootstrapper.GetLatestVersionsAsync(System.Collections.Generic.IEnumerable! managedPackages, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.IDE.Bootstrapper.Install(params string![]! paths) -> void +Microsoft.TemplateEngine.IDE.Bootstrapper.Install(string! path) -> void +Microsoft.TemplateEngine.IDE.Bootstrapper.Install(System.Collections.Generic.IEnumerable! paths) -> void +Microsoft.TemplateEngine.IDE.Bootstrapper.InstallTemplatePackagesAsync(System.Collections.Generic.IEnumerable! installRequests, Microsoft.TemplateEngine.Edge.Settings.InstallationScope scope = Microsoft.TemplateEngine.Edge.Settings.InstallationScope.Global, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.IDE.Bootstrapper.ListTemplates(bool exactMatchesOnly, params System.Func![]! filters) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.IDE.Bootstrapper.LoadDefaultComponents() -> void +Microsoft.TemplateEngine.IDE.Bootstrapper.Register(System.Reflection.Assembly! assembly) -> void +Microsoft.TemplateEngine.IDE.Bootstrapper.Register(System.Type! type) -> void +Microsoft.TemplateEngine.IDE.Bootstrapper.Uninstall(params string![]! paths) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.IDE.Bootstrapper.Uninstall(string! path) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.IDE.Bootstrapper.Uninstall(System.Collections.Generic.IEnumerable! paths) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.IDE.Bootstrapper.UninstallTemplatePackagesAsync(System.Collections.Generic.IEnumerable! managedPackages, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.IDE.Bootstrapper.UpdateTemplatePackagesAsync(System.Collections.Generic.IEnumerable! updateRequests, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.IDE.Bootstrapper.GetManagedTemplatePackagesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.IDE.Bootstrapper.GetTemplatePackagesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.IDE.Bootstrapper.GetTemplatesAsync(System.Collections.Generic.IEnumerable!>! filters, bool exactMatchesOnly = true, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.IDE.Bootstrapper.GetTemplatesAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.IDE/PublicAPI.Unshipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.IDE/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..34b8eeef253d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.IDE/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.TemplateEngine.IDE.Bootstrapper.Bootstrapper(Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost! host, bool virtualizeConfiguration, bool loadDefaultComponents = true, string? hostSettingsLocation = null, Microsoft.TemplateEngine.Abstractions.IEnvironment? environment = null) -> void +Microsoft.TemplateEngine.IDE.Bootstrapper.CreateAsync(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! info, string? name, string! outputPath, Microsoft.TemplateEngine.Edge.Template.InputDataSet? inputParameters, string? baselineName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IDeferredMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IDeferredMacro.cs new file mode 100644 index 000000000000..7e8db6076e42 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IDeferredMacro.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions +{ + /// + /// An interface for macros created that can create the config from other config (deferred config). + /// + [Obsolete("Use IGeneratedSymbolConfig instead.")] + public interface IDeferredMacro : IMacro + { + /// + /// Creates from . + /// + /// + /// Deprecated as can process only own configuration. + /// + [Obsolete("Use IMacro{T}.Evaluate or IGeneratedSymbolConfig.Evaluate instead for generated symbol instead.")] + IMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IMacroConfig rawConfig); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IDeterministicModeMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IDeterministicModeMacro.cs new file mode 100644 index 000000000000..6c73f88aab1b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IDeterministicModeMacro.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions +{ + /// + /// A macro that supports deterministic mode (for test purposes). + /// + internal interface IDeterministicModeMacro : IMacro + { + /// + /// Evaluates the macro based on . The result is modification of variable collection . + /// The evaluation is performed deterministically, i.e. different external factors cannot impact the result and the recurrent evaluation is guaranteed to provide same result. + /// + void EvaluateConfigDeterministically(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, IMacroConfig config); + } + + /// + /// A macro that supports deterministic mode (for test purposes). + /// + internal interface IDeterministicModeMacro : IMacro where T : IMacroConfig + { + /// + /// Evaluates the macro based on . The result is modification of variable collection . + /// The evaluation is performed deterministically, i.e. different external factors cannot impact the result and the recurrent evaluation is guaranteed to provide same result. + /// + void EvaluateDeterministically(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, T config); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IGeneratedSymbolConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IGeneratedSymbolConfig.cs new file mode 100644 index 000000000000..5ceaffacec44 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IGeneratedSymbolConfig.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions +{ + /// + /// Represents the configuration of . + /// + public interface IGeneratedSymbolConfig : IMacroConfig + { + /// + /// Gets data type of the variable to be created. + /// + string DataType { get; } + + /// + /// Gets the collection of additional macro parameters, where key is a parameter name, and value is a JSON value serialized to string. + /// + IReadOnlyDictionary Parameters { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IGeneratedSymbolMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IGeneratedSymbolMacro.cs new file mode 100644 index 000000000000..2e0cbb05a752 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IGeneratedSymbolMacro.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions +{ + /// + /// An interface for a macro created via generated symbols. + /// + public interface IGeneratedSymbolMacro : IMacro + { + /// + /// Creates a macro configuration based on the provided engine environment settings and generated symbol configuration. + /// This method allows to instantiate generated macro during population. + /// + /// The engine environment settings. + /// The generated symbol configuration. + /// The macro configuration. + IMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig generatedSymbolConfig); + } + + /// + /// An interface for a typed macro created via generated symbols. + /// + /// The type of macro configuration. + public interface IGeneratedSymbolMacro : IMacro where T : IMacroConfig + { + /// + /// Creates a macro configuration based on the provided engine environment settings and generated symbol configuration. + /// Method is needed for supporting invocation. + /// + /// The engine environment settings. + /// The generated symbol configuration. + /// The typed macro configuration . + T CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig generatedSymbolConfig); + } + +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IMacro.cs new file mode 100644 index 000000000000..b8d002ae93ca --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IMacro.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions +{ + /// + /// Represents a macro that can modify variable collection before template instantiation. + /// + public interface IMacro : IIdentifiedComponent + { + /// + /// Gets macro type. The type identifies the macro and should be unique. + /// + string Type { get; } + + /// + /// Evaluates the macro based on . The result is modification of variable collection . + /// + void EvaluateConfig(IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, IMacroConfig config); + } + + /// + /// Represents a macro that can modify variable collection before template instantiation. + /// + public interface IMacro : IMacro where T : IMacroConfig + { + /// + /// Evaluates the macro based on . The result is modification of variable collection . + /// + void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, T config); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IMacroConfig.cs new file mode 100644 index 000000000000..da33d7d49ff4 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IMacroConfig.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions +{ + /// + /// Base interface for configurations. + /// + public interface IMacroConfig + { + /// + /// Gets the variable name for this . + /// + string VariableName { get; } + + /// + /// Gets type. + /// + string Type { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IMacroDependency.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IMacroDependency.cs new file mode 100644 index 000000000000..698c34be8927 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Abstractions/IMacroDependency.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions +{ + /// + /// Represents a configuration for the macro that configures dependencies on other symbols. + /// + public interface IMacroConfigDependency + { + /// + /// Gets the set of symbol names required by the macro. + /// ResolveSymbolDependencies method should be called prior to accessing the property. + /// Before it the property won't be populated. + /// + /// when is not called for the property population. + HashSet Dependencies { get; } + + /// + /// Resolves the macro dependencies out of the provided list of symbols. + /// As the result of method execution, should be populated. + /// + /// The list of symbols that exist in configuration. + /// The method should identify which of those symbols are the dependencies for given macro. + void ResolveSymbolDependencies(IReadOnlyList symbols); + } + +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/BindSymbolEvaluator.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/BindSymbolEvaluator.cs new file mode 100644 index 000000000000..adfaafef836c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/BindSymbolEvaluator.cs @@ -0,0 +1,252 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Components; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal class BindSymbolEvaluator + { + private readonly IEngineEnvironmentSettings _settings; + private readonly IReadOnlyList _bindSymbolSources; + private readonly ILogger _logger; + + internal BindSymbolEvaluator(IEngineEnvironmentSettings settings) + { + _settings = settings; + _bindSymbolSources = settings.Components.OfType().ToList(); + _logger = settings.Host.LoggerFactory.CreateLogger(); + } + + /// + /// Evaluates bind symbols from external sources. + /// Sets the evaluated symbols to . + /// + /// The symbols to evaluate. + /// The variable collection to set results to. + /// The cancellation token that allows cancelling the operation. + public async Task EvaluateBindSymbolsAsync(IEnumerable symbols, IVariableCollection variableCollection, CancellationToken cancellationToken) + { + if (!_bindSymbolSources.Any()) + { + _logger.LogDebug("No sources for bind symbols are available"); + return; + } + _logger.LogDebug( + "Configured bind sources are: {0}.", + string.Join(", ", _bindSymbolSources.Select(s => $"{s.DisplayName}({s.GetType().Name})"))); + + //set default values for symbols that have them defined + foreach (BindSymbol bindSymbol in symbols) + { + if (!variableCollection.ContainsKey(bindSymbol.Name) && bindSymbol.DefaultValue != null) + { + bool result = ParameterConverter.TryConvertLiteralToDatatype(bindSymbol.DefaultValue, bindSymbol.DataType, out object? value); + if (result && value != null) + { + variableCollection[bindSymbol.Name] = value; + } + else if (!result && !string.IsNullOrWhiteSpace(bindSymbol.DataType)) + { + _logger.LogWarning(LocalizableStrings.BindSymbolEvaluator_Warning_DefaultValueConversionFailure, bindSymbol.Name, bindSymbol.DefaultValue, bindSymbol.DataType); + } + } + } + + IEnumerable bindSymbols = symbols.Where(bs => !string.IsNullOrWhiteSpace(bs.Binding)); + if (!bindSymbols.Any()) + { + _logger.LogDebug("No bind symbols has '{0}' defined.", nameof(BindSymbol.Binding).ToLowerInvariant()); + return; + } + + IReadOnlyList<(BindSymbol Symbol, Task Task)> tasksToRun = bindSymbols + .Select(s => (s, GetBoundValueAsync(s.Binding, cancellationToken))) + .ToList(); + + try + { + await Task.WhenAll(tasksToRun.Select(t => t.Task)).ConfigureAwait(false); + } + catch (Exception) + { + //do nothing + //errors are handled below + } + cancellationToken.ThrowIfCancellationRequested(); + ProcessEvaluationResults(variableCollection, tasksToRun); + } + + private void ProcessEvaluationResults(IVariableCollection variableCollection, IReadOnlyList<(BindSymbol Symbol, Task Task)> completedTasks) + { + foreach ((BindSymbol currentSymbol, Task currentTask) in completedTasks) + { + if (!currentTask.IsCompleted) + { + throw new InvalidOperationException("The method should be used only after the tasks are completed."); + } + if (currentTask.IsFaulted || currentTask.IsCanceled) + { + _logger.LogWarning(LocalizableStrings.BindSymbolEvaluator_Warning_EvaluationError, currentSymbol.Name); + _logger.LogDebug(currentTask.Exception, "The evaluation task has failed: {0}", currentTask.Exception.Message); + continue; + } + + if (currentTask.Result is null) + { + if (currentSymbol.DefaultValue != null) + { + _logger.LogDebug( + "Failed to evaluate bind symbol '{0}', the returned value is null. The default value '{1}' is used instead.", + currentSymbol.Name, + currentSymbol.DefaultValue); + } + else + { + _logger.LogWarning(LocalizableStrings.BindSymbolEvaluator_Warning_EvaluationError, currentSymbol.Name); + } + continue; + } + + string obtainedValue = currentTask.Result!; + bool result = ParameterConverter.TryConvertLiteralToDatatype(obtainedValue, currentSymbol.DataType, out object? convertedValue); + if (result && convertedValue != null) + { + variableCollection[currentSymbol.Name] = convertedValue; + _logger.LogDebug("Variable '{0}' was set to '{1}'.", currentSymbol.Name, convertedValue); + continue; + } + if (!result) + { + _logger.LogWarning( + LocalizableStrings.BindSymbolEvaluator_Warning_ConversionFailure, + currentSymbol.Name, + obtainedValue, + currentSymbol.DataType ?? ""); + continue; + } + _logger.LogDebug( + "Variable '{0}' was not set: the value '{1}' after conversion to datatype '{2}' is null.", + currentSymbol.Name, + obtainedValue, + currentSymbol.DataType ?? ""); + } + } + + private async Task GetBoundValueAsync(string configuredBinding, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(configuredBinding)) + { + throw new ArgumentException($"'{nameof(configuredBinding)}' cannot be null or whitespace.", nameof(configuredBinding)); + } + _logger.LogDebug("Evaluating binding '{0}'.", configuredBinding); + + string? prefix = null; + string binding = configuredBinding; + if (configuredBinding.Contains(':')) + { + prefix = configuredBinding.Substring(0, configuredBinding.IndexOf(':')).Trim(); + binding = configuredBinding.Substring(configuredBinding.IndexOf(':') + 1).Trim(); + } + if (string.IsNullOrWhiteSpace(binding)) + { + binding = configuredBinding; + } + + IEnumerable? sourcesToSearch = null; + if (string.IsNullOrWhiteSpace(prefix)) + { + _logger.LogDebug("Binding '{0}' does not define prefix. All the sources that do not require prefix will be queried for this binding.", configuredBinding); + sourcesToSearch = _bindSymbolSources.Where(source => !source.RequiresPrefixMatch); + } + else + { + sourcesToSearch = _bindSymbolSources.Where(s => s.SourcePrefix?.Equals(prefix, StringComparison.OrdinalIgnoreCase) ?? false); + _logger.LogDebug( + "The following sources match prefix '{0}': {1}.", + prefix, + string.Join(", ", sourcesToSearch.Select(s => s.DisplayName))); + if (!sourcesToSearch.Any()) + { + _logger.LogDebug("No sources matches prefix '{0}' does not define prefix. All the sources that do not require prefix will be queried for the binding '{1}'.", prefix, configuredBinding); + sourcesToSearch = _bindSymbolSources.Where(source => !source.RequiresPrefixMatch); + binding = configuredBinding; + } + } + + var successfulTasks = await RunEvaluationTasks(sourcesToSearch, binding, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + if (!successfulTasks.Any() && binding != configuredBinding) + { + _logger.LogDebug( + "No values were retrieved for '{0}' for the sources matching the prefix. All the sources will be queried for the binding '{1}' now.", + binding, + configuredBinding); + binding = configuredBinding; + //if nothing is found, try all sources with unparsed values from configuration + sourcesToSearch = _bindSymbolSources.Where(source => !source.RequiresPrefixMatch); + successfulTasks = await RunEvaluationTasks(sourcesToSearch, configuredBinding, cancellationToken).ConfigureAwait(false); + } + cancellationToken.ThrowIfCancellationRequested(); + + if (!successfulTasks.Any()) + { + _logger.LogDebug("No values were retrieved for '{0}'.", binding); + return null; + } + else if (successfulTasks.Count() == 1) + { + _logger.LogDebug("'{0}' was retrieved for '{1}'.", successfulTasks.Single().Value, binding); + return successfulTasks.Single().Value; + } + else + { + //in case of multiple results, use highest priority source + _logger.LogDebug( + "The following values were retrieved for binding '{0}': {1}.", + binding, + string.Join(", ", successfulTasks.Select(t => $"{t.Source.DisplayName} (priority: {t.Source.Priority}): '{t.Value}'"))); + var highestPriority = successfulTasks.Max(t => t.Source.Priority); + var highestPriorityTasks = successfulTasks.Where(t => t.Source.Priority == highestPriority); + if (highestPriorityTasks.Count() > 1) + { + string sourcesList = string.Join(", ", highestPriorityTasks.Select(t => $"'{t.Source.DisplayName}'")); + string prefixesList = string.Join(", ", highestPriorityTasks.Select(t => $"'{t.Source.SourcePrefix}:'")); + _logger.LogWarning(LocalizableStrings.BindSymbolEvaluator_Warning_ValueAvailableFromMultipleSources, configuredBinding, sourcesList, prefixesList); + return null; + } + else + { + _logger.LogDebug("'{0}' was selected for '{1}' as highest priority value.", highestPriorityTasks.Single().Value, binding); + return highestPriorityTasks.Single().Value; + } + } + } + + private async Task> RunEvaluationTasks(IEnumerable sourcesToSearch, string binding, CancellationToken cancellationToken) + { + var tasksToRun = sourcesToSearch.Select(s => new { Source = s, Task = s.GetBoundValueAsync(_settings, binding, cancellationToken) }).ToList(); + try + { + await Task.WhenAll(tasksToRun.Select(t => t.Task)).ConfigureAwait(false); + } + catch (Exception) + { + //do nothing, errors are handled below. + } + cancellationToken.ThrowIfCancellationRequested(); + foreach (var task in tasksToRun.Where(t => t.Task.IsFaulted)) + { + _logger.LogDebug("Failed to retrieve '{0}' from the source {1}: {2}", binding, nameof(task.Source), task.Task.Exception.Message); + } + + return tasksToRun + .Where(t => t.Task.IsCompleted && t.Task.Result != null) + .Select(t => (t.Source, t.Task.Result!)); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Components.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Components.cs new file mode 100644 index 000000000000..2d64bf114816 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Components.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Validation; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + public static class Components + { + private static readonly CaseChangeMacro CaseChange = new(); + private static readonly GeneratePortNumberMacro GeneratePortNumberMacro = new(); + private static readonly CoalesceMacro CoalesceMacro = new(); + private static readonly ConstantMacro ConstantMacro = new(); + private static readonly GuidMacro GuidMacro = new(); + private static readonly SwitchMacro SwitchMacro = new(); + private static readonly RegexMatchMacro RegexMatchMacro = new(); + private static readonly RegexMacro RegexMacro = new(); + private static readonly RandomMacro RandomMacro = new(); + private static readonly NowMacro NowMacro = new(); + private static readonly JoinMacro JoinMacro = new(); + + public static IReadOnlyList<(Type Type, IIdentifiedComponent Instance)> AllComponents { get; } = + new (Type Type, IIdentifiedComponent Instance)[] + { + (typeof(IGenerator), new RunnableProjectGenerator()), + + (typeof(IOperationConfig), new BalancedNestingConfig()), + (typeof(IOperationConfig), new ConditionalConfig()), + (typeof(IOperationConfig), new FlagsConfig()), + (typeof(IOperationConfig), new IncludeConfig()), + (typeof(IOperationConfig), new RegionConfig()), + (typeof(IOperationConfig), new ReplacementConfig()), + + (typeof(IMacro), CaseChange), + (typeof(IGeneratedSymbolMacro), CaseChange), + (typeof(IMacro), CoalesceMacro), + (typeof(IGeneratedSymbolMacro), CoalesceMacro), + (typeof(IMacro), ConstantMacro), + (typeof(IGeneratedSymbolMacro), ConstantMacro), + (typeof(IMacro), new EvaluateMacro()), + (typeof(IMacro), GeneratePortNumberMacro), + (typeof(IGeneratedSymbolMacro), GeneratePortNumberMacro), + (typeof(IMacro), GuidMacro), + (typeof(IGeneratedSymbolMacro), GuidMacro), + (typeof(IMacro), JoinMacro), + (typeof(IGeneratedSymbolMacro), JoinMacro), + (typeof(IMacro), NowMacro), + (typeof(IGeneratedSymbolMacro), NowMacro), + (typeof(IMacro), new ProcessValueFormMacro()), + (typeof(IMacro), RandomMacro), + (typeof(IGeneratedSymbolMacro), RandomMacro), + (typeof(IMacro), RegexMacro), + (typeof(IGeneratedSymbolMacro), RegexMacro), + (typeof(IMacro), RegexMatchMacro), + (typeof(IGeneratedSymbolMacro), RegexMatchMacro), + (typeof(IMacro), SwitchMacro), + (typeof(IGeneratedSymbolMacro), SwitchMacro), + + (typeof(ITemplateValidatorFactory), new MandatoryValidationFactory()), + (typeof(ITemplateValidatorFactory), new MandatoryLocalizationValidationFactory()), + }; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BaseReplaceSymbol.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BaseReplaceSymbol.cs new file mode 100644 index 000000000000..3556d35f8507 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BaseReplaceSymbol.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. + +using System.Text.Json.Nodes; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + public abstract class BaseReplaceSymbol : BaseSymbol + { + private protected BaseReplaceSymbol(string name, string? replaces) : base(name) + { + ReplacementContexts = []; + Replaces = replaces; + } + + private protected BaseReplaceSymbol(BaseReplaceSymbol clone) : base(clone) + { + FileRename = clone.FileRename; + Replaces = clone.Replaces; + ReplacementContexts = clone.ReplacementContexts; + } + + private protected BaseReplaceSymbol(JsonObject jObject, string name) : base(name) + { + FileRename = jObject.ToString(nameof(FileRename)); + Replaces = jObject.ToString(nameof(Replaces)); + ReplacementContexts = ReplacementContext.FromJObject(jObject); + } + + /// + /// Gets the text that should be replaced by the value of this symbol. + /// Corresponds to "replaces" JSON property. + /// + public string? Replaces { get; } + + /// + /// Gets the replacement contexts that determine when this symbol is allowed to do replacement operations. + /// + public IReadOnlyList ReplacementContexts { get; } + + /// + /// Gets the part of the file name that should be replaced with the value of this symbol. + /// Corresponds to "fileRename" JSON property. + /// + public string? FileRename { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BaseSymbol.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BaseSymbol.cs new file mode 100644 index 000000000000..1c5659eefd9f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BaseSymbol.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + public abstract class BaseSymbol + { + private protected BaseSymbol(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or whitespace.", nameof(name)); + } + Name = name; + } + + private protected BaseSymbol(BaseSymbol clone) + { + Name = clone.Name; + } + + /// + /// Gets the name of the symbol. + /// Corresponds to key that defines the symbol in "symbols" JSON object. + /// + public string Name { get; } + + /// + /// Gets the type of the symbol. + /// Corresponds to "type" JSON property. + /// + public abstract string Type { get; } + + /// + /// Indicates that the symbol is implicit and was created by generator itself. + /// Those symbols are not part of JSON. + /// + internal bool IsImplicit { get; init; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BaseValueSymbol.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BaseValueSymbol.cs new file mode 100644 index 000000000000..a3a2b0cc7b50 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BaseValueSymbol.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + public abstract class BaseValueSymbol : BaseReplaceSymbol + { + /// + /// Initializes this instance with given JSON data. + /// + /// + /// + /// + /// + private protected BaseValueSymbol(string name, JsonObject jObject, string? defaultOverride, bool symbolConditionsSupported = false) : base(jObject, name) + { + DefaultValue = defaultOverride ?? jObject.ToString(nameof(DefaultValue)); + IsRequired = ParseIsRequiredField(jObject, !symbolConditionsSupported); + DataType = jObject.ToString(nameof(DataType)); + if (!jObject.TryGetValue(nameof(Forms), out JsonNode? formsToken) || formsToken is not JsonObject formsObject) + { + // no value forms explicitly defined, use the default ("identity") + Forms = SymbolValueFormsModel.Default; + } + else + { + // the config defines forms for the symbol. Use them. + Forms = SymbolValueFormsModel.FromJObject(formsObject); + } + } + + private protected BaseValueSymbol(BaseValueSymbol clone, SymbolValueFormsModel formsFallback) : base(clone) + { + DefaultValue = clone.DefaultValue; + Forms = clone.Forms.GlobalForms.Count != 0 ? clone.Forms : formsFallback; + IsRequired = clone.IsRequired; + DataType = clone.DataType; + } + + private protected BaseValueSymbol(string name, string? replaces) : base(name, replaces) + { + Forms = SymbolValueFormsModel.Default; + } + + /// + /// Gets default value of the symbol. + /// Corresponds to "defaultValue" JSON property. + /// + public string? DefaultValue { get; internal init; } + + /// + /// Gets the forms defined for the symbol + /// Corresponds to "forms" JSON property. + /// + public SymbolValueFormsModel Forms { get; internal init; } + + /// + /// Specifies if the symbol is required. + /// Corresponds to "isRequired" JSON property. + /// + public bool IsRequired { get; internal init; } + + /// + /// Gets the data type of the symbol. + /// Corresponds to "datatype" JSON property. + /// + public string? DataType { get; internal init; } + + private protected bool TryGetIsRequiredField(JsonNode token, out bool result) + { + result = false; + var kind = token.GetValueKind(); + return (kind is JsonValueKind.True or JsonValueKind.False || kind == JsonValueKind.String) + && + bool.TryParse(token.ToString(), out result); + } + + private bool ParseIsRequiredField(JsonNode token, bool throwOnError) + { + if (!token.TryGetValue(nameof(IsRequired), out JsonNode? isRequiredToken)) + { + return false; + } + + if ( + !TryGetIsRequiredField(isRequiredToken!, out bool value) + && + throwOnError) + { + throw new ArgumentException(string.Format(LocalizableStrings.Symbol_Error_IsRequiredNotABool, isRequiredToken)); + } + + return value; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BindSymbol.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BindSymbol.cs new file mode 100644 index 000000000000..49f8ba92270f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/BindSymbol.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Defines the symbol of type "bind". + /// + public sealed class BindSymbol : BaseReplaceSymbol + { + internal const string TypeName = "bind"; + + internal BindSymbol(string name, string binding) : base(name, null) + { + if (string.IsNullOrWhiteSpace(binding)) + { + throw new ArgumentException($"'{nameof(binding)}' cannot be null or whitespace.", nameof(binding)); + } + + Binding = binding; + } + + internal BindSymbol(string name, JsonObject jObject) : base(jObject, name) + { + string? binding = jObject.ToString(nameof(Binding)); + if (string.IsNullOrWhiteSpace(binding)) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.SymbolModel_Error_MandatoryPropertyMissing, name, BindSymbol.TypeName, nameof(Binding).ToLowerInvariant()), name); + } + + Binding = binding!; + DefaultValue = jObject.ToString(nameof(DefaultValue)); + DataType = jObject.ToString(nameof(DataType)); + } + + /// + /// Gets the name of the host property or the environment variable which will provide the value of this symbol. + /// + public string Binding { get; } + + /// + public override string Type => TypeName; + + /// + /// Gets default value of the symbol. + /// Corresponds to "defaultValue" JSON property. + /// + public string? DefaultValue { get; internal init; } + + /// + /// Gets the data type of the symbol. + /// Corresponds to "datatype" JSON property. + /// + public string? DataType { get; internal init; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ComputedSymbol.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ComputedSymbol.cs new file mode 100644 index 000000000000..59b326f0f6f7 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ComputedSymbol.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Defines the symbol of type "computed". + /// + public sealed class ComputedSymbol : BaseSymbol + { + internal const string TypeName = "computed"; + + internal ComputedSymbol(string name, string value) : base(name) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException($"'{nameof(value)}' cannot be null or whitespace.", nameof(value)); + } + + Value = value; + } + + internal ComputedSymbol(string name, JsonObject jObject) : base(name) + { + string? value = jObject.ToString(nameof(Value)); + if (string.IsNullOrWhiteSpace(value)) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.SymbolModel_Error_MandatoryPropertyMissing, name, ComputedSymbol.TypeName, nameof(Value).ToLowerInvariant()), name); + } + + Value = value!; + + Evaluator = jObject.ToString(nameof(Evaluator)); + } + + /// + /// Defines the value of the computed symbol. + /// Corresponds to "value" JSON property. + /// + public string Value { get; } + + /// + public override string Type => TypeName; + + /// + /// Defines the evaluator to be used when computing the symbol value. + /// Corresponds to "evaluator" JSON property. + /// + public string? Evaluator { get; internal init; } + + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ConditionedConfigurationElement.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ConditionedConfigurationElement.cs new file mode 100644 index 000000000000..b5604b9fe083 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ConditionedConfigurationElement.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Expressions.Cpp2; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + public abstract class ConditionedConfigurationElement + { + /// + /// Stores the result of the condition evaluation. If the condition + /// was not evaluated before, the value is null. + /// + private bool? _conditionResult; + + /// + /// Gets the condition string to be evaluated so that configuration element is applied. + /// Usually corresponds to "condition" JSON property. + /// + public string? Condition { get; internal init; } + + /// + /// Stores the result of condition evaluation. should be done before accessing this property. + /// + /// when the property is accessed prior to method is called. + public bool ConditionResult + { + get + { + if (!_conditionResult.HasValue) + { + throw new InvalidOperationException("ConditionResult access attempted prior to evaluation."); + } + + return _conditionResult.Value; + } + } + + /// + /// Evaluates the condition . + /// + /// the logger to be used to log the messages during evaluation. + /// the variable collection that will be used to evaluate the condition. + /// true if condition evaluates to true or not specified, otherwise false. + public bool EvaluateCondition(ILogger logger, IVariableCollection variables) + { + bool conditionResult = string.IsNullOrEmpty(Condition) || + Cpp2StyleEvaluatorDefinition.EvaluateFromString(logger, Condition!, variables); + _conditionResult = conditionResult; + return conditionResult; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/CustomFileGlobModel.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/CustomFileGlobModel.cs new file mode 100644 index 000000000000..002cf6012841 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/CustomFileGlobModel.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Defines operation for specific files. + /// + public sealed class CustomFileGlobModel : ConditionedConfigurationElement + { + internal CustomFileGlobModel(string glob, IReadOnlyList operations) + { + Glob = glob; + Operations = operations; + } + + /// + /// Gets the glob which defines the files that operations should be applied to. + /// + public string Glob { get; } + + /// + /// Gets the collection of operations to apply. + /// + public IReadOnlyList Operations { get; } + + /// + /// Gets the prefix that is used in flags. + /// + public string? FlagPrefix { get; internal init; } + + /// + /// Gets the variable configuration format. + /// + internal IVariableConfig VariableFormat { get; } = VariableConfig.Default; + + internal static CustomFileGlobModel FromJObject(JsonObject globData, string globName) + { + // setup the custom operations + List customOpsForGlob = new List(); + if (globData.TryGetValue(nameof(Operations), out JsonNode? operationData)) + { + foreach (JsonNode? operationConfig in (JsonArray)operationData!) + { + if (operationConfig is JsonObject obj) + { + customOpsForGlob.Add(CustomOperationModel.FromJObject(obj)); + } + } + } + + CustomFileGlobModel globModel = new CustomFileGlobModel(globName, customOpsForGlob) + { + FlagPrefix = globData.ToString(nameof(FlagPrefix)), + Condition = globData.ToString(nameof(Condition)) + }; + + return globModel; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/CustomOperationModel.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/CustomOperationModel.cs new file mode 100644 index 000000000000..6fe8f87c4b98 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/CustomOperationModel.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Defines the operation configuration. + /// + public sealed class CustomOperationModel : ConditionedConfigurationElement + { + internal CustomOperationModel() { } + + /// + /// Gets the operation type. + /// + public string? Type { get; internal init; } + + /// + /// Gets the operation raw configuration in JSON format. + /// + public string? Configuration { get; internal init; } + + internal static CustomOperationModel FromJObject(JsonObject jObject) + { + CustomOperationModel model = new CustomOperationModel + { + Type = jObject.ToString(nameof(Type)), + Condition = jObject.ToString(nameof(Condition)), + Configuration = jObject.Get(nameof(Configuration))?.ToJsonString(), + }; + + return model; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/DerivedSymbol.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/DerivedSymbol.cs new file mode 100644 index 000000000000..31c3bcd4683a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/DerivedSymbol.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Defines the symbol of type "derived". + /// + public sealed class DerivedSymbol : BaseValueSymbol + { + internal const string TypeName = "derived"; + + internal DerivedSymbol(string name, string valueTransform, string valueSource, string? replaces = null) : base(name, replaces) + { + if (string.IsNullOrWhiteSpace(valueTransform)) + { + throw new ArgumentException($"'{nameof(valueTransform)}' cannot be null or whitespace.", nameof(valueTransform)); + } + + if (string.IsNullOrWhiteSpace(valueSource)) + { + throw new ArgumentException($"'{nameof(valueSource)}' cannot be null or whitespace.", nameof(valueSource)); + } + + ValueTransform = valueTransform; + ValueSource = valueSource; + } + + internal DerivedSymbol(string name, JsonObject jObject, string? defaultOverride) + : base(name, jObject, defaultOverride) + { + string? valueTransform = jObject.ToString(nameof(ValueTransform)); + if (string.IsNullOrWhiteSpace(valueTransform)) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.SymbolModel_Error_MandatoryPropertyMissing, name, DerivedSymbol.TypeName, nameof(ValueTransform).ToLowerInvariant()), name); + } + + ValueTransform = valueTransform!; + + string? valueSource = jObject.ToString(nameof(ValueSource)); + if (string.IsNullOrWhiteSpace(valueSource)) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.SymbolModel_Error_MandatoryPropertyMissing, name, DerivedSymbol.TypeName, nameof(ValueSource).ToLowerInvariant()), name); + } + + ValueSource = valueSource!; + } + + /// + public override string Type => TypeName; + + /// + /// Defines the transformation to be applied to (value form). + /// + public string ValueTransform { get; } + + /// + /// Defines the variable to be transformed. + /// + public string ValueSource { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ExtendedFileSource.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ExtendedFileSource.cs new file mode 100644 index 000000000000..7c80e482031e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ExtendedFileSource.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Defines the sources. Corresponds to the element of "sources" JSON array. + /// + public sealed class ExtendedFileSource : ConditionedConfigurationElement + { + internal static readonly Dictionary RenameDefaults = new Dictionary(StringComparer.Ordinal); + + internal ExtendedFileSource() { } + + /// + /// Defines the files to be just copied when instantiating template. Operations will not be applied to them. + /// Can be glob. If single value is given, it can be also a filename where from to read this configuration. + /// + public IReadOnlyList CopyOnly { get; internal init; } = []; + + /// + /// Defines the files to be include when instantiating template. Operations will be applied to them. + /// Can be glob. If single value is given, it can be also a filename where from to read this configuration. + /// + public IReadOnlyList Include { get; internal init; } = []; + + /// + /// Defines the files to be excluded when instantiating template. These files will not be processed during template instantiation. + /// Can be glob. If single value is given, it can be also a filename where from to read this configuration. + /// + public IReadOnlyList Exclude { get; internal init; } = []; + + /// + /// Defines the files to be renamed when instantiating the template. The key is a source file name, the value is the final file name. + /// + public IReadOnlyDictionary Rename { get; internal init; } = RenameDefaults; + + /// + /// Defines the path to the source files to apply the settings to. The path is relative to the directory containing the .template.config folder. + /// Default value: "./". + /// + public string Source { get; internal init; } = "./"; + + /// + /// Defines the path the content should be written to. The path is relative to the directory the user has specified to instantiate the template to. + /// Default value: "./". + /// + public string Target { get; internal init; } = "./"; + + /// + /// A list of additional source information which gets added to the top-level source information, based on evaluation the corresponding "source.modifiers.condition". + /// + public IReadOnlyList Modifiers { get; internal init; } = []; + + internal static ExtendedFileSource FromJObject(JsonObject jObject) + { + List modifiers = new List(); + ExtendedFileSource src = new ExtendedFileSource() + { + CopyOnly = jObject.ToStringReadOnlyList(nameof(CopyOnly)), + Exclude = jObject.ToStringReadOnlyList(nameof(Exclude)), + Include = jObject.ToStringReadOnlyList(nameof(Include)), + Condition = jObject.ToString(nameof(Condition)), + Rename = jObject.Get(nameof(Rename))?.ToStringDictionary().ToDictionary(x => x.Key, x => x.Value) ?? RenameDefaults, + Modifiers = modifiers, + Source = jObject.ToString(nameof(Source)) ?? "./", + Target = jObject.ToString(nameof(Target)) ?? "./" + }; + + foreach (JsonObject entry in jObject.Items(nameof(src.Modifiers))) + { + SourceModifier modifier = new SourceModifier + { + Condition = entry.ToString(nameof(modifier.Condition)), + CopyOnly = entry.ToStringReadOnlyList(nameof(CopyOnly)), + Exclude = entry.ToStringReadOnlyList(nameof(Exclude)), + Include = entry.ToStringReadOnlyList(nameof(Include)), + Rename = entry.Get(nameof(Rename))?.ToStringDictionary().ToDictionary(x => x.Key, x => x.Value) ?? RenameDefaults, + }; + modifiers.Add(modifier); + } + + return src; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/GeneratedSymbol.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/GeneratedSymbol.cs new file mode 100644 index 000000000000..d6ab5bd2e596 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/GeneratedSymbol.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Defines the symbol of type "generated". + /// + public sealed class GeneratedSymbol : BaseReplaceSymbol, IGeneratedSymbolConfig + { + internal const string TypeName = "generated"; + + internal GeneratedSymbol(string name, string generator) : base(name, null) + { + if (string.IsNullOrWhiteSpace(generator)) + { + throw new ArgumentException($"'{nameof(generator)}' cannot be null or whitespace.", nameof(generator)); + } + Generator = generator; + } + + internal GeneratedSymbol(string name, string generator, IReadOnlyDictionary parameters, string? dataType = null) : this(name, generator) + { + DataType = dataType; + Parameters = parameters; + } + + internal GeneratedSymbol(string name, JsonObject jObject) : base(jObject, name) + { + string? generator = jObject.ToString(nameof(Generator)); + if (string.IsNullOrWhiteSpace(generator)) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.SymbolModel_Error_MandatoryPropertyMissing, name, TypeName, nameof(Generator).ToLowerInvariant()), name); + } + + Generator = generator!; + DataType = jObject.ToString(nameof(DataType)); + Parameters = jObject.ToJsonNodeStringDictionary(StringComparer.OrdinalIgnoreCase, nameof(Parameters)); + } + + /// + public override string Type => TypeName; + + /// + /// Defines the data type of generated value. + /// + public string? DataType { get; internal init; } + + /// + /// Refers to the Type property value of a concrete IMacro. + /// + public string Generator { get; } + + /// + /// Defines the parameters to be used for generating the value. + /// - the key is a parameter name + /// - the value is a JSON value. + /// + public IReadOnlyDictionary Parameters { get; internal init; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + string IGeneratedSymbolConfig.DataType => DataType ?? "string"; + + string IMacroConfig.VariableName => Name; + + string IMacroConfig.Type => Generator; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ManualInstructionModel.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ManualInstructionModel.cs new file mode 100644 index 000000000000..b0465c4a32d5 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ManualInstructionModel.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Represents an instruction that should be manually performed by the user + /// as part of a post action. + /// + public sealed class ManualInstructionModel : ConditionedConfigurationElement + { + internal ManualInstructionModel(string? id, string text) + { + Id = id; + Text = text; + } + + internal ManualInstructionModel(string? id, string text, string? condition) + { + Id = id; + Text = text; + Condition = condition; + } + + /// + /// Gets the string identifying this instruction within the post action. + /// This property can be null if the author has not provided one. In that case this manual instruction + /// cannot be referenced in a context where an id is required, such as in templatestrings.json files. + /// + public string? Id { get; } + + /// + /// Gets the text explaining the steps the user should take. + /// + public string Text { get; private set; } + + /// + /// Localizes this manual instruction by replacing the value of the property the given text. + /// + /// Localized . + internal void Localize(string localizedText) + { + Text = localizedText; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ParameterSymbol.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ParameterSymbol.cs new file mode 100644 index 000000000000..153ebdd936ce --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ParameterSymbol.cs @@ -0,0 +1,267 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Defines the symbol of type "parameter". + /// + public sealed class ParameterSymbol : BaseValueSymbol + { + internal const string TypeName = "parameter"; + + private IReadOnlyDictionary? _choices; + private string? _description; + private string? _displayName; + + /// + /// Creates an instance of using + /// the provided JSON Data. + /// + /// + /// JSON to initialize the symbol with. + /// + internal ParameterSymbol(string name, JsonObject jObject, string? defaultOverride) + : base(name, jObject, defaultOverride, true) + { + DefaultIfOptionWithoutValue = jObject.ToString(nameof(DefaultIfOptionWithoutValue)); + DisplayName = jObject.ToString(nameof(DisplayName)) ?? string.Empty; + Description = jObject.ToString(nameof(Description)) ?? string.Empty; + + var choicesAndDescriptions = new Dictionary(); + + if (DataType == "choice") + { + TagName = jObject.ToString(nameof(TagName)); + + foreach (JsonObject choiceObject in jObject.Items(nameof(Choices))) + { + string? choiceName = choiceObject.ToString("choice"); + + if (string.IsNullOrWhiteSpace(choiceName)) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.SymbolModel_Error_MandatoryPropertyMissing, name, ParameterSymbol.TypeName, "choice"), name); + } + + var choice = new ParameterChoice( + choiceObject.ToString("displayName") ?? string.Empty, + choiceObject.ToString("description") ?? string.Empty); + + choicesAndDescriptions.Add(choiceName!, choice); + } + } + else if (DataType == "bool" && string.IsNullOrEmpty(DefaultIfOptionWithoutValue)) + { + // bool flags are considered true if they're provided without a value. + DefaultIfOptionWithoutValue = "true"; + } + + if (DefaultValue == null && !IsRequired) + { + DefaultValue = ParameterConverter.GetDefault(DataType); + } + + Choices = choicesAndDescriptions; + AllowMultipleValues = jObject.ToBool(nameof(AllowMultipleValues)); + EnableQuotelessLiterals = jObject.ToBool(nameof(EnableQuotelessLiterals)); + + this.Precedence = GetPrecedence(IsRequired, jObject); + } + + /// + /// Creates a clone of the given . + /// + /// The symbol to copy the values from. + /// The value to be used for in the case + /// that the does not specify a value for . + internal ParameterSymbol(ParameterSymbol cloneFrom, SymbolValueFormsModel formsFallback) : base(cloneFrom, formsFallback) + { + Description = cloneFrom.Description; + IsTag = cloneFrom.IsTag; + TagName = cloneFrom.TagName; + Choices = cloneFrom.Choices; + AllowMultipleValues = cloneFrom.AllowMultipleValues; + EnableQuotelessLiterals = cloneFrom.EnableQuotelessLiterals; + Precedence = cloneFrom.Precedence; + } + + /// + /// Creates a default instance of . + /// + internal ParameterSymbol(string name, string? replaces = null) : base(name, replaces) + { + Precedence = TemplateParameterPrecedence.Default; + } + + public override string Type => TypeName; + + /// + /// Gets the friendly name of the symbol to be displayed to the user. + /// + public string? DisplayName + { + get => _displayName; + internal init => _displayName = value; + } + + /// + /// Gets the description of the parameter. + /// + public string? Description + { + get => _description; + internal init => _description = value; + } + + /// + /// If this is set, the option can be provided without a value. It will be given this value. + /// + public string? DefaultIfOptionWithoutValue { get; internal init; } + + /// + /// If this is set, it's allowed to specify multiple values of that parameter. + /// + public bool AllowMultipleValues { get; internal init; } + + /// + /// If this is set, it's allowed to specify choice literals without quotation within conditions. + /// + public bool EnableQuotelessLiterals { get; internal init; } + + public TemplateParameterPrecedence Precedence { get; internal init; } + + public string? IsEnabledCondition { get; internal init; } + + public string? IsRequiredCondition { get; internal init; } + + public IReadOnlyDictionary? Choices + { + get => _choices; + + internal init => _choices = value?.CloneIfDifferentComparer(StringComparer.OrdinalIgnoreCase); + } + + // only relevant for choice datatype + internal bool IsTag { get; init; } + + // only relevant for choice datatype + internal string? TagName { get; init; } + + internal static ParameterSymbol FromDeprecatedConfigTag(string name, string value) + { + ParameterSymbol symbol = new ParameterSymbol(name) + { + DefaultValue = value, + DataType = "choice", + Precedence = GetPrecedence(false, true, true, null, null), + Choices = new Dictionary() + { + { value, new ParameterChoice(string.Empty, string.Empty) } + }, + Forms = SymbolValueFormsModel.Default + }; + + return symbol; + } + + internal void Localize(IParameterSymbolLocalizationModel locModel) + { + _displayName = locModel.DisplayName ?? _displayName; + _description = locModel.Description ?? _description; + if (Choices == null) + { + return; + } + + foreach (KeyValuePair choice in Choices) + { + if (locModel.Choices.TryGetValue(choice.Key, out ParameterChoiceLocalizationModel? locChoiceModel)) + { + choice.Value.Localize(locChoiceModel); + } + } + + } + + private static TemplateParameterPrecedence GetPrecedence(bool isRequired, JsonObject jObject) + { + string? isRequiredCondition = ParseIsRequiredConditionField(jObject); + + // Initialize IsEnabled - as a condition or a constant + string? isEnabledCondition = null; + bool isEnabled = true; + if (jObject != null && jObject.TryGetValue("IsEnabled", out JsonNode? isEnabledToken)) + { + if (isEnabledToken!.TryParseBool(out bool enabledConst)) + { + isEnabled = enabledConst; + } + else if (isEnabledToken!.GetValueKind() == JsonValueKind.String) + { + isEnabledCondition = isEnabledToken.ToString(); + } + } + + return GetPrecedence(isRequired, isEnabled, false, isRequiredCondition, isEnabledCondition); + } + + private static TemplateParameterPrecedence GetPrecedence(bool isRequired, bool isEnabled, bool isTag, string? isRequiredCondition, string? isEnabledCondition) + { + // If enable condition is set - parameter is conditionally disabled (regardless if require condition is set or not) + // Conditionally required is if and only if the only require condition is set + + if (!isEnabled) + { + return new TemplateParameterPrecedence(PrecedenceDefinition.Disabled); + } + + if (!string.IsNullOrEmpty(isEnabledCondition)) + { + return new TemplateParameterPrecedence(PrecedenceDefinition.ConditionalyDisabled, isRequiredCondition, isEnabledCondition, isRequired); + } + + if (isTag) + { + return new TemplateParameterPrecedence(PrecedenceDefinition.Implicit); + } + + if (!string.IsNullOrEmpty(isRequiredCondition)) + { + return new TemplateParameterPrecedence(PrecedenceDefinition.ConditionalyRequired, isRequiredCondition, null); + } + + if (isRequired) + { + return new TemplateParameterPrecedence(PrecedenceDefinition.Required, null, null, true); + } + + return TemplateParameterPrecedence.Default; + } + + private static string? ParseIsRequiredConditionField(JsonNode token) + { + if (!token.TryGetValue(nameof(IsRequired), out JsonNode? isRequiredToken)) + { + return null; + } + + // Attribute parsable as a bool - so we do not want to present it as a condition + if (isRequiredToken!.TryParseBool(out _)) + { + return null; + } + + if (isRequiredToken!.GetValueKind() != JsonValueKind.String) + { + throw new ArgumentException(string.Format(LocalizableStrings.Symbol_Error_IsRequiredNotABoolOrString, isRequiredToken)); + } + + return isRequiredToken.ToString(); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/PostActionModel.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/PostActionModel.cs new file mode 100644 index 000000000000..a5842f41f9da --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/PostActionModel.cs @@ -0,0 +1,213 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Localization; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Validation; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + public sealed class PostActionModel : ConditionedConfigurationElement + { + /// + /// Default id to be used when post action contains only one manual instruction + /// and the author has not explicitly specified an id. + /// + internal const string DefaultIdForSingleManualInstruction = "default"; + + private string? _description; + + internal PostActionModel() + : this(new Dictionary(), new List()) { } + + internal PostActionModel(IReadOnlyDictionary args, IReadOnlyList manualInstructions) + { + Args = args; + ManualInstructionInfo = manualInstructions; + } + + /// + /// Gets a string that uniquely identifies this post action within a template. + /// + public string? Id { get; internal init; } + + /// + /// Gets the description of the post action. + /// + public string? Description + { + get => _description; + + internal init => _description = value; + } + + /// + /// Gets the identifier of the action that will be performed. + /// Note that this is not an identifier for the post action itself. + /// + public Guid ActionId { get; internal init; } + + /// + /// Gets a value indicating whether the template instantiation should continue + /// in case of an error with this post action. + /// + public bool ContinueOnError { get; internal init; } + + /// + /// Gets the arguments for this post action. + /// + public IReadOnlyDictionary Args { get; internal init; } = new Dictionary(); + + /// + /// Gets the list of arguments to which file renames should be applied. + /// + public IReadOnlyList ApplyFileRenamesToArgs { get; internal init; } = []; + + /// + /// Gets a value indicating whether the file renames should be applied to manual instructions. + /// + public bool ApplyFileRenamesToManualInstructions { get; internal init; } + + /// + /// Gets the list of instructions that should be manually performed by the user. + /// "instruction" contains the text that explains the steps to be taken by the user. + /// An instruction is only considered if the "condition" evaluates to true. + /// + public IReadOnlyList ManualInstructionInfo { get; internal init; } = new List(); + + internal static IReadOnlyList LoadListFromJArray(JsonArray? jArray, List validationEntries) + { + List localizedPostActions = new(); + if (jArray == null) + { + return localizedPostActions; + } + + HashSet postActionIds = new(); + for (int postActionIndex = 0; postActionIndex < jArray.Count; postActionIndex++) + { + JsonNode? action = jArray[postActionIndex]; + string? postActionId = action.ToString(nameof(Id)); + string? description = action.ToString(nameof(Description)); + Guid actionId = action!.ToGuid(nameof(ActionId)); + bool continueOnError = action.ToBool(nameof(ContinueOnError)); + string? postActionCondition = action.ToString(nameof(Condition)); + bool applyFileRenamesToManualInstructions = action.ToBool(nameof(ApplyFileRenamesToManualInstructions)); + IReadOnlyList applyFileRenamesToArgs = action.ArrayAsStrings(nameof(ApplyFileRenamesToArgs)) ?? []; + + if (postActionId != null && !postActionIds.Add(postActionId)) + { + // There is already a post action with the same id. Localization won't work properly. Let user know. + validationEntries.Add( + new ValidationEntry( + IValidationEntry.SeverityLevel.Warning, + "CONFIG0201", + string.Format(LocalizableStrings.Authoring_CONFIG0201_PostActionIdIsNotUnique, postActionId, postActionIndex))); + postActionId = null; + } + + if (actionId == default) + { + validationEntries.Add( + new ValidationEntry( + IValidationEntry.SeverityLevel.Error, + "CONFIG0202", + string.Format(LocalizableStrings.Authoring_CONFIG0202_PostActionMustHaveActionId, postActionIndex))); + continue; + } + + Dictionary args = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var argInfo in action.PropertiesOf("Args")) + { + args.Add(argInfo.Key, argInfo.Value?.ToString() ?? string.Empty); + } + + List verifiedApplyFileRenamesToArgs = new(); + + foreach (string arg in applyFileRenamesToArgs) + { + if (!args.ContainsKey(arg)) + { + validationEntries.Add( + new ValidationEntry( + IValidationEntry.SeverityLevel.Warning, + "CONFIG0204", + string.Format(LocalizableStrings.Authoring_CONFIG0204_UnknownArgumentForReplace, arg, nameof(ApplyFileRenamesToArgs).ToCamelCase(), nameof(Args).ToCamelCase()))); + } + else + { + verifiedApplyFileRenamesToArgs.Add(arg); + } + } + + IReadOnlyList manualInstructions = LoadManualInstructionsFromJArray(action.Get("ManualInstructions"), validationEntries); + + PostActionModel model = new(args, manualInstructions) + { + Id = postActionId, + Description = description, + ActionId = actionId, + ContinueOnError = continueOnError, + Condition = postActionCondition, + ApplyFileRenamesToArgs = verifiedApplyFileRenamesToArgs, + ApplyFileRenamesToManualInstructions = applyFileRenamesToManualInstructions, + }; + + localizedPostActions.Add(model); + } + + return localizedPostActions; + } + + internal void Localize(PostActionLocalizationModel locModel) + { + _description = locModel.Description ?? Description; + + foreach (var manualInstruction in ManualInstructionInfo) + { + string localizedInstruction = string.Empty; + bool exactIdMatch = manualInstruction.Id != null && locModel.Instructions.TryGetValue(manualInstruction.Id, out localizedInstruction); + bool defaultIdMatch = manualInstruction.Id == null && ManualInstructionInfo.Count == 1 && locModel.Instructions.TryGetValue(DefaultIdForSingleManualInstruction, out localizedInstruction); + + if (exactIdMatch || defaultIdMatch) + { + manualInstruction.Localize(localizedInstruction); + } + } + } + + private static IReadOnlyList LoadManualInstructionsFromJArray(JsonArray? jArray, List validationEntries) + { + var results = new List(); + if (jArray == null) + { + return results; + } + + HashSet manualInstructionIds = new HashSet(); + for (int i = 0; i < jArray.Count; i++) + { + JsonNode? jToken = jArray[i]; + string? id = jToken.ToString("id"); + string text = jToken.ToString("text") ?? string.Empty; + string? condition = jToken.ToString("condition"); + + if (id != null && !manualInstructionIds.Add(id)) + { + // There is already an instruction with the same id. We won't be able to localize this. Let user know. + validationEntries.Add( + new ValidationEntry( + IValidationEntry.SeverityLevel.Warning, + "CONFIG0203", + string.Format(LocalizableStrings.Authoring_CONFIG0203_ManualInstructionIdIsNotUnique, id, i))); + id = null; + } + + results.Add(new ManualInstructionModel(id, text, condition)); + } + + return results; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/PrimaryOutputModel.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/PrimaryOutputModel.cs new file mode 100644 index 000000000000..bd2129338e75 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/PrimaryOutputModel.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Defines the primary output. Corresponds to the element of "primaryOutputs" JSON array. + /// Primary outputs define the list of template files for further processing. + /// + public sealed class PrimaryOutputModel : ConditionedConfigurationElement + { + internal PrimaryOutputModel(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new System.ArgumentException($"'{nameof(path)}' cannot be null or whitespace.", nameof(path)); + } + + Path = path; + } + + /// + /// Defines the relative path to the file after the template is instantiated. + /// + public string Path { get; } + + internal static IReadOnlyList ListFromJArray(JsonArray? jsonData) + { + List modelList = new List(); + + if (jsonData == null) + { + return modelList; + } + + foreach (JsonNode? pathInfo in jsonData) + { + string? path = pathInfo.ToString(nameof(Path)); + if (string.IsNullOrWhiteSpace(path)) + { + continue; + } + + PrimaryOutputModel pathModel = new PrimaryOutputModel(path!.NormalizePath()) + { + Condition = pathInfo.ToString("condition") + }; + + modelList.Add(pathModel); + } + + return modelList; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ReplacementContext.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ReplacementContext.cs new file mode 100644 index 000000000000..e6d430fe6c72 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/ReplacementContext.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Defines the replacement context for the symbol. + /// + public sealed class ReplacementContext + { + internal ReplacementContext(string? before, string? after) + { + OnlyIfBefore = before; + OnlyIfAfter = after; + } + + /// + /// Gets the context that should be present before the symbol, in order to replacement to be applied. + /// Corresponds to "onlyIf.before" JSON property. + /// + public string? OnlyIfBefore { get; } + + /// + /// Gets the context that should be present after the symbol, in order to replacement to be applied. + /// Corresponds to "onlyIf.after" JSON property. + /// + public string? OnlyIfAfter { get; } + + internal static IReadOnlyList FromJObject(JsonObject jObject) + { + JsonArray? onlyIf = jObject.Get("onlyIf"); + + if (onlyIf != null) + { + List contexts = new List(); + foreach (JsonNode? entry in onlyIf) + { + if (entry is not JsonObject) + { + continue; + } + + string? before = entry.ToString("before"); + string? after = entry.ToString("after"); + contexts.Add(new ReplacementContext(before, after)); + } + + return contexts; + } + else + { + return []; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/SourceModifier.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/SourceModifier.cs new file mode 100644 index 000000000000..c17ec8db80b4 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/SourceModifier.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// Defines the source modifier, these settings are applied to in addition to the top-level source information, in case is met. + /// + public sealed class SourceModifier : ConditionedConfigurationElement + { + internal SourceModifier() { } + + /// + /// Defines the files to be just copied when instantiating template. Operations will not be applied to them. + /// Can be glob. If single value is given, it can be also a filename where from to read this configuration. + /// + public IReadOnlyList CopyOnly { get; internal init; } = []; + + /// + /// Defines the files to be include when instantiating template. Operations will be applied to them. + /// Can be glob. If single value is given, it can be also a filename where from to read this configuration. + /// + public IReadOnlyList Include { get; internal init; } = []; + + /// + /// Defines the files to be excluded when instantiating template. These files will not be processed during template instantiation. + /// Can be glob. If single value is given, it can be also a filename where from to read this configuration. + /// + public IReadOnlyList Exclude { get; internal init; } = []; + + /// + /// Defines the files to be renamed when instantiating the template. The key is a source file name, the value is the final file name. + /// + public IReadOnlyDictionary Rename { get; internal init; } = ExtendedFileSource.RenameDefaults; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/SymbolModelConverter.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/SymbolModelConverter.cs new file mode 100644 index 000000000000..45f905dd2225 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/SymbolModelConverter.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + internal sealed class SymbolModelConverter + { + // Note: Only ParameterSymbol has a Description property, this it's the only one that gets localization + // TODO: change how localization gets merged in, don't do it here. + internal static BaseSymbol? GetModelForObject(string name, JsonObject jObject, ILogger? logger, string? defaultOverride) + { + try + { + return jObject.ToString(nameof(BaseSymbol.Type)) switch + { + ParameterSymbol.TypeName => new ParameterSymbol(name, jObject, defaultOverride), + DerivedSymbol.TypeName => new DerivedSymbol(name, jObject, defaultOverride), + ComputedSymbol.TypeName => new ComputedSymbol(name, jObject), + BindSymbol.TypeName => new BindSymbol(name, jObject), + GeneratedSymbol.TypeName => new GeneratedSymbol(name, jObject), + _ => null, + }; + } + catch (TemplateAuthoringException ex) + { + logger?.LogWarning(ex.Message); + return null; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/SymbolValueFormsModel.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/SymbolValueFormsModel.cs new file mode 100644 index 000000000000..a26fa440d642 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/SymbolValueFormsModel.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ValueForms; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + public class SymbolValueFormsModel + { + internal SymbolValueFormsModel(IReadOnlyList globalForms) + { + GlobalForms = globalForms; + } + + public IReadOnlyList GlobalForms { get; } + + internal static SymbolValueFormsModel Empty { get; } = new SymbolValueFormsModel([]); + + // by default, symbols get the "identity" value form, for a direct replacement + internal static SymbolValueFormsModel Default { get; } = new SymbolValueFormsModel(new[] { IdentityValueFormFactory.FormIdentifier }); + + internal static SymbolValueFormsModel NameForms { get; } = new SymbolValueFormsModel(new[] + { + IdentityValueFormFactory.FormIdentifier, + DefaultSafeNameValueFormFactory.FormIdentifier, + DefaultLowerSafeNameValueFormFactory.FormIdentifier, + DefaultSafeNamespaceValueFormFactory.FormIdentifier, + DefaultLowerSafeNamespaceValueFormFactory.FormIdentifier + }); + + internal static SymbolValueFormsModel FromJObject(JsonObject configJson) + { + JsonNode? globalConfig = JExtensions.GetPropertyCaseInsensitive(configJson, "global"); + List globalForms; + bool addIdentity; + + if (globalConfig?.GetValueKind() == JsonValueKind.Array) + { + // config is just an array of form names. + globalForms = globalConfig.ArrayAsStrings().ToList(); + addIdentity = true; // default value + } + else if (globalConfig?.GetValueKind() == JsonValueKind.Object) + { + // config is an object. + globalForms = globalConfig.ArrayAsStrings("forms").ToList(); + addIdentity = globalConfig.ToBool("addIdentity", true); + } + else + { + throw new Exception("Malformed global value forms."); + } + + if (addIdentity && !globalForms.Contains(IdentityValueFormFactory.FormIdentifier, StringComparer.OrdinalIgnoreCase)) + { + globalForms.Insert(0, IdentityValueFormFactory.FormIdentifier); + } + + return new SymbolValueFormsModel(globalForms); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/TemplateConfigModel.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/TemplateConfigModel.cs new file mode 100644 index 000000000000..e85bd75a2870 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ConfigModel/TemplateConfigModel.cs @@ -0,0 +1,591 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Localization; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ValueForms; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel +{ + /// + /// The class represents model of template.json. + /// + public sealed class TemplateConfigModel + { + private const string NameSymbolName = "name"; + private readonly Dictionary _forms = SetupValueFormMapForTemplate(); + + // validation entries: + // CONFIG00xx - primary metadata + // CONFIG01xx - symbols + // CONFIG02xx - post actions + private readonly List _validationEntries = new(); + private IReadOnlyDictionary _tags = new Dictionary(); + private Dictionary _symbols = new(); + private IReadOnlyList _postActions = new List(); + private string? _author; + private string? _name; + private string? _description; + private string? _sourceName; + + internal TemplateConfigModel(string identity) + { + if (string.IsNullOrWhiteSpace(identity)) + { + throw new ArgumentException($"'{nameof(identity)}' cannot be null or whitespace.", nameof(identity)); + } + + Identity = identity; + Symbols = []; + } + + private TemplateConfigModel(JsonObject source, ILogger? logger, string? baselineName = null) + { + ILogger? logger1 = logger; + + string? identity = source.ToString(nameof(Identity)); + if (string.IsNullOrWhiteSpace(identity)) + { + throw new TemplateAuthoringException($"'identity' is missing or is an empty string.", "identity"); + } + + Identity = identity!; + Name = source.ToString(nameof(Name)); + + Author = source.ToString(nameof(Author)); + Classifications = source.ArrayAsStrings(nameof(Classifications)); + DefaultName = source.ToString(nameof(DefaultName)); + PreferDefaultName = source.ToBool(nameof(PreferDefaultName)); + Description = source.ToString(nameof(Description)) ?? string.Empty; + GroupIdentity = source.ToString(nameof(GroupIdentity)); + Precedence = source.ToInt32(nameof(Precedence)); + Guids = source.ArrayAsGuids(nameof(Guids)); + + SourceName = source.ToString(nameof(SourceName)); + PlaceholderFilename = source.ToString(nameof(PlaceholderFilename))!; + GeneratorVersions = source.ToString(nameof(GeneratorVersions)); + ThirdPartyNotices = source.ToString(nameof(ThirdPartyNotices)); + PreferNameDirectory = source.ToBool(nameof(PreferNameDirectory)); + + ShortNameList = source.ToStringReadOnlyList("ShortName"); + Forms = SetupValueFormMapForTemplate(source); + + var sources = new List(); + Sources = sources; + foreach (JsonObject item in source.Items(nameof(Sources))) + { + ExtendedFileSource src = ExtendedFileSource.FromJObject(item); + sources.Add(src); + } + + IBaselineInfo? baseline = null; + BaselineInfo = BaselineInfoFromJObject(source.PropertiesOf("baselines")); + + if (!string.IsNullOrEmpty(baselineName)) + { + BaselineInfo.TryGetValue(baselineName!, out baseline); + } + + Dictionary symbols = new(StringComparer.Ordinal); + // create a name symbol. If one is explicitly defined in the template, it'll override this. + NameSymbol = SetupDefaultNameSymbol(SourceName); + symbols[NameSymbol.Name] = NameSymbol; + + // tags are being deprecated from template configuration, but we still read them for backwards compatibility. + // They're turned into symbols here, which eventually become tags. + _tags = source.ToStringDictionary(StringComparer.OrdinalIgnoreCase, "tags"); + foreach (KeyValuePair tagInfo in _tags) + { + if (!symbols.ContainsKey(tagInfo.Key)) + { + symbols[tagInfo.Key] = ParameterSymbol.FromDeprecatedConfigTag(tagInfo.Key, tagInfo.Value); + } + } + foreach (var prop in source.PropertiesOf(nameof(Symbols))) + { + if (prop.Value is not JsonObject obj) + { + continue; + } + if (string.IsNullOrWhiteSpace(prop.Key)) + { + continue; + } + + string? defaultOverride = null; + if (baseline?.DefaultOverrides != null) + { + baseline.DefaultOverrides.TryGetValue(prop.Key, out defaultOverride); + } + + BaseSymbol? modelForSymbol = SymbolModelConverter.GetModelForObject(prop.Key, obj, logger, defaultOverride); + + if (modelForSymbol != null) + { + // The symbols dictionary comparer is Ordinal, making symbol names case-sensitive. + if (string.Equals(prop.Key, NameSymbolName, StringComparison.Ordinal) + && symbols.TryGetValue(prop.Key, out BaseSymbol existingSymbol) + && existingSymbol is ParameterSymbol existingParameterSymbol + && modelForSymbol is ParameterSymbol modelForParameterSymbol) + { + // "name" symbol is explicitly defined above. If it's also defined in the template.json, it gets special handling here. + symbols[prop.Key] = new ParameterSymbol(modelForParameterSymbol, existingParameterSymbol.Forms); + } + else + { + // last in wins (in the odd case where a template.json defined a symbol multiple times) + symbols[prop.Key] = modelForSymbol; + } + } + } + foreach (BindSymbol symbol in ImplicitBindSymbols) + { + if (!symbols.ContainsKey(symbol.Name)) + { + symbols[symbol.Name] = symbol; + } + } + _symbols = symbols; + _postActions = PostActionModel.LoadListFromJArray(source.Get("PostActions"), _validationEntries); + PrimaryOutputs = PrimaryOutputModel.ListFromJArray(source.Get(nameof(PrimaryOutputs))); + + // Custom operations at the global level + JsonNode? globalCustomConfigData = JExtensions.GetPropertyCaseInsensitive(source, nameof(GlobalCustomOperations)); + if (globalCustomConfigData != null) + { + GlobalCustomOperations = CustomFileGlobModel.FromJObject((JsonObject)globalCustomConfigData, string.Empty); + } + + // Custom operations for specials + IReadOnlyDictionary allSpecialOpsConfig = source.ToJsonNodeDictionary(StringComparer.OrdinalIgnoreCase, nameof(SpecialCustomOperations)); + List specialCustomSetup = new(); + + foreach (KeyValuePair globConfigKeyValue in allSpecialOpsConfig) + { + string globName = globConfigKeyValue.Key; + JsonNode globData = globConfigKeyValue.Value; + + CustomFileGlobModel globModel = CustomFileGlobModel.FromJObject((JsonObject)globData, globName); + specialCustomSetup.Add(globModel); + } + + SpecialCustomOperations = specialCustomSetup; + + List constraints = new(); + foreach (var prop in source.PropertiesOf(nameof(Constraints))) + { + if (prop.Value is not JsonObject obj) + { + logger1?.LogWarning(LocalizableStrings.SimpleConfigModel_Error_Constraints_InvalidSyntax, nameof(Constraints).ToLowerInvariant()); + continue; + } + + string? type = obj.ToString(nameof(TemplateConstraintInfo.Type)); + if (string.IsNullOrWhiteSpace(type)) + { + logger1?.LogWarning(LocalizableStrings.SimpleConfigModel_Error_Constraints_MissingType, obj.ToJsonString(), nameof(TemplateConstraintInfo.Type).ToLowerInvariant()); + continue; + } + obj.TryGetValue(nameof(TemplateConstraintInfo.Args), out JsonNode? args); + constraints.Add(new TemplateConstraintInfo(type!, args?.ToJsonString())); + } + Constraints = constraints; + } + + /// + /// Gets the template author ("author" JSON property). + /// + public string? Author + { + get => _author; + + internal init => _author = value; + } + + /// + /// Gets the default name for the template ("defaultName" JSON property). + /// + public string? DefaultName { get; internal init; } + + /// + /// Gets the description of the template ("description" JSON property). + /// + public string? Description + { + get => _description; + + internal init => _description = value; + } + + /// + /// Gets the group identity of the template ("groupIdentity" JSON property). + /// This allows multiple templates to be displayed as one, with the the decision for which one to use based on and other parameters added by user for the instantiation. + /// + public string? GroupIdentity { get; internal init; } + + /// + /// Gets the precedence of the template in a group ("precedence" JSON property). + /// + public int Precedence { get; internal init; } + + /// + /// Gets the template name ("name" JSON property). + /// + public string? Name + { + get => _name; + + internal init => _name = value; + } + + /// + /// Gets the link to 3rd party notices ("thirdPartyNotices" JSON property). + /// + public string? ThirdPartyNotices { get; internal init; } + + /// + /// Indicates whether to create a directory for the template if name is specified but an output directory is not set (instead of creating the content directly in the current directory) ("preferNameDirectory" JSON property). + /// + public bool PreferNameDirectory { get; internal init; } + + /// + /// Indicates whether to use the template's default name or parent folder's name for template name ("preferDefaultName: JSON property). + /// + public bool PreferDefaultName { get; internal init; } + + /// + /// Gets the collection of template tags ("tags" JSON property). + /// + public IReadOnlyDictionary Tags + { + get => _tags; + + internal init => _tags = value; + } + + /// + /// Gets the list of template short names ("shortName" JSON property). + /// + public IReadOnlyList ShortNameList { get; internal init; } = []; + + /// + /// Gets the list of post actions defined for the template ("postActions" JSON property). + /// + public IReadOnlyList PostActionModels + { + get => _postActions; + + internal init => _postActions = value; + } + + /// + /// Gets the list of template primary outputs ("primaryOutputs" JSON property). + /// + public IReadOnlyList PrimaryOutputs { get; internal init; } = []; + + /// + /// Gets version expression which defines which generator versions is supported by the template ("generatorVersions" JSON property). + /// + public string? GeneratorVersions { get; internal init; } + + /// + /// Gets the list of baselines defined for the template ("baselines" JSON property). + /// + public IReadOnlyDictionary BaselineInfo { get; internal init; } = new Dictionary(); + + /// + /// Gets the template identity ("identity" JSON property) - a unique name for this template. + /// + public string Identity { get; } + + /// + /// Gets the list of classifications of the template ("classifications" JSON property). + /// + public IReadOnlyList Classifications { get; internal init; } = []; + + /// + /// Gets the list of guids defined in the template ("guids" JSON property). + /// + public IReadOnlyList Guids { get; internal init; } = []; + + /// + /// Gets the source name defined in the template ("sourceName" JSON property). + /// + public string? SourceName + { + get => _sourceName; + + internal init + { + _sourceName = value; + NameSymbol = SetupDefaultNameSymbol(value); + _symbols[NameSymbolName] = NameSymbol; + } + } + + /// + /// Gets the list of sources defined in the template ("sources" JSON property). + /// + public IReadOnlyList Sources { get; internal init; } = []; + + /// + /// Gets the list of constraints defined in the template ("constraints" JSON property). + /// + public IReadOnlyList Constraints { get; internal init; } = []; + + /// + /// Gets the list of symbols defined in the template ("symbols" JSON property). + /// + public IEnumerable Symbols + + { + get => _symbols.Values; + + internal init + { + _symbols = value.ToDictionary(s => s.Name, s => s); + _symbols[NameSymbolName] = NameSymbol; + foreach (BindSymbol symbol in ImplicitBindSymbols) + { + if (!_symbols.ContainsKey(symbol.Name)) + { + _symbols[symbol.Name] = symbol; + } + } + } + } + + /// + /// Gets the list of forms defined in the template ("forms" JSON property). + /// + public IReadOnlyDictionary Forms + { + get => _forms; + internal init + { + foreach (KeyValuePair kvp in value) + { + _forms[kvp.Key] = kvp.Value; + } + } + } + + /// + /// Gets the placeholder filename defined in the template ("placeholderFilename" JSON property). + /// + public string? PlaceholderFilename { get; internal init; } + + /// + /// Gets the list of global custom operations defined for the template ("globalCustomOperations" JSON property). + /// + public CustomFileGlobModel? GlobalCustomOperations { get; internal init; } + + /// + /// Gets the list of custom operations defined for the template for specific files ("specialCustomOperations" JSON property). + /// + public IReadOnlyList SpecialCustomOperations { get; internal init; } = []; + + internal BaseSymbol NameSymbol { get; private init; } = SetupDefaultNameSymbol(null); + + /// + /// Gets the list of validation errors for template. + /// + internal IReadOnlyList ValidationErrors => _validationEntries; + + private static IReadOnlyList ImplicitBindSymbols { get; } = SetupImplicitBindSymbols(); + + /// + /// Creates from stream . + /// + /// The stream containing template configuration in JSON format. + /// The logger to use for reporting errors/messages. + /// The file path of template configuration (optional, used for logging). + public static TemplateConfigModel FromStream(Stream content, ILogger? logger = null, string? filename = null) + { + using TextReader tr = new StreamReader(content, System.Text.Encoding.UTF8, true); + return FromTextReader(tr, logger); + } + + /// + /// Creates from string . + /// + /// The string containing template configuration in JSON format. + /// The logger to use for reporting errors/messages. + /// The file path of template configuration (optional, used for logging). + public static TemplateConfigModel FromString(string content, ILogger? logger = null, string? filename = null) + { + if (string.IsNullOrWhiteSpace(content)) + { + throw new ArgumentException($"'{nameof(content)}' cannot be null or whitespace.", nameof(content)); + } + + using TextReader tr = new StringReader(content); + return FromTextReader(tr, logger); + } + + internal static TemplateConfigModel FromTextReader(TextReader content, ILogger? logger = null) + { + string json = content.ReadToEnd(); + JsonObject source = JExtensions.ParseJsonObject(json); + return new TemplateConfigModel(source, logger, null); + } + + internal static TemplateConfigModel FromJObject(JsonObject source, ILogger? logger = null, string? baselineName = null) + { + return new TemplateConfigModel(source, logger, baselineName); + } + + internal void RemoveSymbol(string name) + { + _symbols.Remove(name); + if (name.Equals(NameSymbolName, StringComparison.Ordinal)) + { + _symbols[name] = NameSymbol; + } + } + + /// + /// Localizes this with given localization model. + /// + /// Localization model containing the localized strings. + /// This method works on a best-effort basis. If the given model is invalid or incompatible, + /// erroneous data will be skipped. No errors will be logged. Use + /// to validate localization models before calling this method. + internal void Localize(LocalizationModel locModel) + { + _author = locModel.Author ?? Author; + _name = locModel.Name ?? Name; + _description = locModel.Description ?? Description; + + foreach (ParameterSymbol symbol in Symbols.OfType()) + { + if (locModel.ParameterSymbols.TryGetValue(symbol.Name, out IParameterSymbolLocalizationModel symbolLocModel)) + { + symbol.Localize(symbolLocModel); + } + } + + foreach (PostActionModel postAction in _postActions) + { + if (postAction.Id != null && locModel.PostActions.TryGetValue(postAction.Id, out PostActionLocalizationModel postActionLocModel)) + { + postAction.Localize(postActionLocModel); + } + } + } + + /// + /// Extracts parameters from the model. + /// + internal IEnumerable ExtractParameters() + { + Dictionary parameters = new(); + foreach (ParameterSymbol parameterSymbol in Symbols.OfType()) + { + bool isName = parameterSymbol == NameSymbol; + Parameter parameter = new(parameterSymbol.Name, type: parameterSymbol.Type, parameterSymbol.DataType!) + { + DefaultValue = parameterSymbol.DefaultValue ?? (!parameterSymbol.IsRequired ? parameterSymbol.Replaces : null), + IsName = isName, + Name = parameterSymbol.Name, + Precedence = parameterSymbol.Precedence, + Description = parameterSymbol.Description, + Choices = parameterSymbol.Choices, + DefaultIfOptionWithoutValue = parameterSymbol.DefaultIfOptionWithoutValue, + DisplayName = parameterSymbol.DisplayName, + EnableQuotelessLiterals = parameterSymbol.EnableQuotelessLiterals, + AllowMultipleValues = parameterSymbol.AllowMultipleValues, + }; + parameters[parameterSymbol.Name] = parameter; + } + + return parameters.Values; + } + + private static BaseSymbol SetupDefaultNameSymbol(string? sourceName) + { + string? replaces = string.IsNullOrWhiteSpace(sourceName) ? null : sourceName; + return new ParameterSymbol(NameSymbolName, replaces) + { + Description = "The default name symbol", + DataType = "string", + Forms = SymbolValueFormsModel.NameForms, + Precedence = new TemplateParameterPrecedence(PrecedenceDefinition.Implicit), + IsImplicit = true, + }; + } + + private static Dictionary SetupValueFormMapForTemplate(JsonObject? source = null) + { + Dictionary formMap = new(StringComparer.Ordinal); + + // setup all the built-in default forms. + // name of the form is form identifier + // this is only possible for the forms that don't need configuration + foreach (KeyValuePair builtInForm in ValueFormRegistry.FormLookup) + { + formMap[builtInForm.Key] = builtInForm.Value.Create(); + } + + if (source == null) + { + return formMap; + } + + // setup the forms defined by the template configuration. + // if any have the same name as a default, the default is overridden. + IReadOnlyDictionary templateDefinedForms = source.ToJsonNodeDictionary(StringComparer.OrdinalIgnoreCase, nameof(Forms)); + + foreach (KeyValuePair form in templateDefinedForms) + { + if (form.Value is JsonObject o) + { + formMap[form.Key] = ValueFormRegistry.GetForm(form.Key, o); + } + } + return formMap; + } + + private static IReadOnlyDictionary BaselineInfoFromJObject(IEnumerable> baselineProperties) + { + Dictionary allBaselines = new(); + + foreach (var property in baselineProperties) + { + if (property.Value is not JsonObject obj) + { + continue; + } + + IReadOnlyDictionary? defaultOverrides = obj.Get(nameof(Utils.BaselineInfo.DefaultOverrides))?.ToStringDictionary() ?? new Dictionary(); + BaselineInfo baseline = new(defaultOverrides, obj.ToString(nameof(baseline.Description))); + allBaselines[property.Key] = baseline; + } + + return allBaselines; + } + + private static IReadOnlyList SetupImplicitBindSymbols() + { + bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + if (!isWindows) + { + return []; + } + //on Windows we implicitly bind OS to avoid likely breaking change. + //this environment variable is commonly used in conditions when using run script post action. + return new[] + { + new BindSymbol("OS", "env:OS") + { + IsImplicit = true, + } + }; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/CreationEffects.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/CreationEffects.cs new file mode 100644 index 000000000000..7e53fea6afc2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/CreationEffects.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal class CreationEffects : ICreationEffects + { + internal CreationEffects(IReadOnlyList fileChanges, ICreationResult creationResult) + { + FileChanges = fileChanges ?? throw new System.ArgumentNullException(nameof(fileChanges)); + CreationResult = creationResult ?? throw new System.ArgumentNullException(nameof(creationResult)); + } + + public IReadOnlyList FileChanges { get; } + + public ICreationResult CreationResult { get; } + } + + internal class CreationEffects2 : ICreationEffects, ICreationEffects2 + { + internal CreationEffects2(IReadOnlyList fileChanges, ICreationResult creationResult) + { + FileChanges = fileChanges ?? throw new System.ArgumentNullException(nameof(fileChanges)); + CreationResult = creationResult ?? throw new System.ArgumentNullException(nameof(creationResult)); + } + + public IReadOnlyList FileChanges { get; } + + IReadOnlyList ICreationEffects.FileChanges => FileChanges; + + public ICreationResult CreationResult { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/CreationResult.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/CreationResult.cs new file mode 100644 index 000000000000..22a98a8ec637 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/CreationResult.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal class CreationResult : ICreationResult + { + public CreationResult(IReadOnlyList postActions, IReadOnlyList primaryOutputs) + { + PostActions = postActions ?? throw new System.ArgumentNullException(nameof(postActions)); + PrimaryOutputs = primaryOutputs ?? throw new System.ArgumentNullException(nameof(primaryOutputs)); + } + + public IReadOnlyList PostActions { get; } + + public IReadOnlyList PrimaryOutputs { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/CryptoRandom.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/CryptoRandom.cs new file mode 100644 index 000000000000..4bb248f6a8c5 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/CryptoRandom.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Cryptography; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal class CryptoRandom : IDisposable + { + private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); + private readonly byte[] _uint32Buffer = new byte[4]; + + public CryptoRandom() { } + +#pragma warning disable IDE0060 // Remove unused parameter + public CryptoRandom(int ignoredSeed) { } +#pragma warning restore IDE0060 // Remove unused parameter + + public static int NextInt(int minValue, int maxValue) + { + using CryptoRandom random = new CryptoRandom(); + return random.Next(minValue, maxValue); + } + + public int Next() + { + _rng.GetBytes(_uint32Buffer); + return BitConverter.ToInt32(_uint32Buffer, 0) & 0x7FFFFFFF; + } + + public int Next(int maxValue) + { + if (maxValue < 0) + { + throw new ArgumentOutOfRangeException(nameof(maxValue)); + } + + return Next(0, maxValue); + } + + public int Next(int minValue, int maxValue) + { + if (minValue > maxValue) + { + throw new ArgumentOutOfRangeException(nameof(minValue)); + } + + if (minValue == maxValue) + { + return minValue; + } + + long diff = maxValue - minValue; + // We are avoiding remainder bias by discarding the remainder. + // background: https://ericlippert.com/2013/12/16/how-much-bias-is-introduced-by-the-remainder-technique/ + while (true) + { + _rng.GetBytes(_uint32Buffer); + uint rand = BitConverter.ToUInt32(_uint32Buffer, 0); + long max = 1 + (long)uint.MaxValue; + long remainder = max % diff; + if (rand < max - remainder) + { + return (int)(minValue + (rand % diff)); + } + } + } + + public double NextDouble() + { + _rng.GetBytes(_uint32Buffer); + uint rand = BitConverter.ToUInt32(_uint32Buffer, 0); + return rand / (1.0 + uint.MaxValue); + } + + public void NextBytes(byte[] buffer) + { + buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); + _rng.GetBytes(buffer); + } + + public void Dispose() + { + _rng.Dispose(); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/DirectoryBasedTemplate.Interfaces.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/DirectoryBasedTemplate.Interfaces.cs new file mode 100644 index 000000000000..d885ca7c99c9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/DirectoryBasedTemplate.Interfaces.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Localization; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Validation; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal abstract partial class DirectoryBasedTemplate : ITemplateMetadata, ITemplateLocator, ITemplateValidationInfo, IValidationInfo + { + string ITemplateMetadata.Identity => ConfigurationModel.Identity; + + Guid ITemplateLocator.GeneratorId => Generator.Id; + + string? ITemplateMetadata.Author => ConfigurationModel.Author; + + string? ITemplateMetadata.Description => ConfigurationModel.Description; + + IReadOnlyList ITemplateMetadata.Classifications => ConfigurationModel.Classifications; + + string? ITemplateMetadata.DefaultName => ConfigurationModel.DefaultName; + + string? ITemplateMetadata.GroupIdentity => ConfigurationModel.GroupIdentity; + + int ITemplateMetadata.Precedence => ConfigurationModel.Precedence; + + string ITemplateMetadata.Name => ConfigurationModel.Name ?? string.Empty; + + IReadOnlyList ITemplateMetadata.ShortNameList => ConfigurationModel.ShortNameList ?? new List(); + + public IParameterDefinitionSet ParameterDefinitions => Parameters; + + string ITemplateLocator.MountPointUri => ConfigFile?.MountPoint.MountPointUri ?? throw new InvalidOperationException($"{nameof(ConfigFile)} should be set in order to continue"); + + string ITemplateLocator.ConfigPlace => ConfigFile?.FullPath ?? throw new InvalidOperationException($"{nameof(ConfigFile)} should be set in order to continue"); + + string? ITemplateMetadata.ThirdPartyNotices => ConfigurationModel.ThirdPartyNotices; + + IReadOnlyDictionary ITemplateMetadata.BaselineInfo => ConfigurationModel.BaselineInfo; + + IReadOnlyDictionary ITemplateMetadata.TagsCollection => ConfigurationModel.Tags; + + IReadOnlyList ITemplateMetadata.PostActions => ConfigurationModel.PostActionModels.Select(pam => pam.ActionId).ToArray(); + + IReadOnlyList ITemplateMetadata.Constraints => ConfigurationModel.Constraints; + + bool ITemplateMetadata.PreferDefaultName => ConfigurationModel.PreferDefaultName; + + public bool IsValid => !ValidationErrors.Any(e => e.Severity == IValidationEntry.SeverityLevel.Error); + + public IReadOnlyList ValidationErrors => _validationErrors; + + TemplateConfigModel ITemplateValidationInfo.ConfigModel => ConfigurationModel; + + IDirectory ITemplateValidationInfo.TemplateSourceRoot => TemplateSourceRoot; + + IFile? ITemplateValidationInfo.ConfigFile => ConfigFile; + + IReadOnlyDictionary ITemplateValidationInfo.Localizations => Localizations; + + IReadOnlyDictionary ITemplateValidationInfo.HostFiles => HostFiles; + + internal abstract IReadOnlyDictionary Localizations { get; } + + internal abstract IReadOnlyDictionary HostFiles { get; } + + public void AddValidationError(IValidationEntry validationEntry) => _validationErrors.Add(validationEntry); + + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/DirectoryBasedTemplate.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/DirectoryBasedTemplate.cs new file mode 100644 index 000000000000..e547621644fb --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/DirectoryBasedTemplate.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Validation; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + /// + /// The class represents the template loaded from directory. + /// The configuration can be loaded from file (production scenario) or loaded from (test scenario). + /// In both cases the template should be available from . + /// + internal abstract partial class DirectoryBasedTemplate + { + internal const string HostTemplateFileConfigBaseName = ".host.json"; + internal const string LocalizationFilePrefix = "templatestrings."; + internal const string LocalizationFileExtension = ".json"; + protected const string AdditionalConfigFilesIndicator = "AdditionalConfigFiles"; + + private readonly List _validationErrors = new(); + + /// + /// Creates the instance of the class based on configuration from . + /// + /// when template configuration is invalid. + /// when template identity is null. + /// when the template is not supported by current generator version. + protected DirectoryBasedTemplate(IEngineEnvironmentSettings settings, IGenerator generator, IFile templateFile, string? baselineName = null) + { + EngineEnvironmentSettings = settings; + //TODO: create specific logger if needed + Logger = settings.Host.Logger; + Generator = generator; + + ConfigFile = templateFile; + + if (ConfigFile.Parent?.Parent is null) + { + throw new TemplateAuthoringException(LocalizableStrings.Authoring_TemplateRootOutsideInstallSource); + } + ConfigDirectory = templateFile.Parent; + TemplateSourceRoot = ConfigFile.Parent.Parent; + + ConfigurationModel = TemplateConfigModel.FromJObject( + MergeAdditionalConfiguration(templateFile.ReadJObjectFromIFile(), templateFile), + Logger, + baselineName); + CheckGeneratorVersionRequiredByTemplate(); + } + + /// + /// Test constructor. Do not use in production. + /// This constructor does not set the location of configuration file. + /// + protected DirectoryBasedTemplate(IEngineEnvironmentSettings settings, IGenerator generator, TemplateConfigModel configModel, IDirectory templateSource) + { + EngineEnvironmentSettings = settings; + //TODO: create specific logger if needed + Logger = settings.Host.Logger; + Generator = generator; + + TemplateSourceRoot = templateSource; + + ConfigurationModel = configModel; + + CheckGeneratorVersionRequiredByTemplate(); + } + + public ILogger Logger { get; } + + /// + /// Gets the configuration model. + /// For test purposes, preloaded model can be used. + /// + public TemplateConfigModel ConfigurationModel { get; } + + /// + /// Gets the directory with source files for the template. + /// + public IDirectory TemplateSourceRoot { get; } + + public IEngineEnvironmentSettings EngineEnvironmentSettings { get; } + + /// + /// Gets configuration . when the template is created from the model built from code. + /// + public virtual IFile? ConfigFile { get; } + + /// + /// Gets the configuration directory. when the template is created from the model built from code. + /// + public virtual IDirectory? ConfigDirectory { get; } + + protected IGenerator Generator { get; } + + /// + /// Gets the template parameters. + /// + protected IParameterDefinitionSet Parameters => new ParameterDefinitionSet(ConfigurationModel.ExtractParameters()); + + internal Task ValidateAsync(ValidationScope scope, CancellationToken cancellationToken) + { + try + { + return ValidationManager.Instance.ValidateTemplateAsync(EngineEnvironmentSettings, this, scope, cancellationToken); + } + catch (Exception ex) when (ex is not TaskCanceledException) + { + //TODO: better error handling + Logger.LogError("Failed to validate template: {ex}", ex.Message); + } + return Task.CompletedTask; + } + + /// + /// Parses host file name to get host identifier. + /// + /// + /// + protected string ParseHostFileName(string filename) => filename.Replace(HostTemplateFileConfigBaseName, string.Empty); + + /// + /// Parses localization file name to get locale. + /// + /// localization file. + /// + protected CultureInfo? ParseLocFileName(IFile locFile) + { + string filename = locFile.Name; + string localeStr = filename.Substring(LocalizationFilePrefix.Length, filename.Length - LocalizationFilePrefix.Length - LocalizationFileExtension.Length); + CultureInfo? locale = null; + + try + { + // PERF: Avoid calling CultureInfo.GetCultures and searching the results as it heavily allocates on each invocation. + locale = CultureInfo.GetCultureInfo(localeStr); + } + catch (CultureNotFoundException) + { + Logger.LogWarning(LocalizableStrings.LocalizationModelDeserializer_Error_UnknownLocale, localeStr); + } + return locale; + } + + /// + /// Checks the for additional configuration files. + /// If found, merges them all together. + /// Returns the merged JObject (or the original if there was nothing to merge). + /// Additional files must be in the same folder as the template file. + /// + /// when additional files configuration is invalid. + private static JsonObject MergeAdditionalConfiguration(JsonObject primarySource, IFileSystemInfo primarySourceConfig) + { + IReadOnlyList otherFiles = primarySource.ArrayAsStrings(AdditionalConfigFilesIndicator); + + if (!otherFiles.Any()) + { + return primarySource; + } + + JsonObject combinedSource = primarySource.DeepCloneObject(); + + foreach (string partialConfigFileName in otherFiles) + { + if (!partialConfigFileName.EndsWith("." + RunnableProjectGenerator.TemplateConfigFileName)) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.SimpleConfigModel_AuthoringException_MergeConfiguration_InvalidFileName, partialConfigFileName, RunnableProjectGenerator.TemplateConfigFileName), partialConfigFileName); + } + + IFile? partialConfigFile = (primarySourceConfig.Parent?.EnumerateFiles(partialConfigFileName, SearchOption.TopDirectoryOnly).FirstOrDefault(x => string.Equals(x.Name, partialConfigFileName))) + ?? throw new TemplateAuthoringException( + string.Format( + LocalizableStrings.SimpleConfigModel_AuthoringException_MergeConfiguration_FileNotFound, + partialConfigFileName), + partialConfigFileName); + JsonObject partialConfigJson = partialConfigFile.ReadJObjectFromIFile(); + combinedSource.Merge(partialConfigJson); + } + + return combinedSource; + } + + /// + /// Checks if the template is supported by current generator version. + /// + /// when the template is not supported by current generator version. + /// when check for the version leads to unexpected result. + private void CheckGeneratorVersionRequiredByTemplate() + { + if (string.IsNullOrWhiteSpace(ConfigurationModel.GeneratorVersions)) + { + return; + } + + string allowedGeneratorVersions = ConfigurationModel.GeneratorVersions!; + + if (!VersionStringHelpers.TryParseVersionSpecification(allowedGeneratorVersions, out IVersionSpecification? versionChecker)) + { + throw new NotSupportedException(string.Format(LocalizableStrings.RunnableProjectGenerator_Exception_TemplateVersionNotSupported, allowedGeneratorVersions, RunnableProjectGenerator.GeneratorVersion)); + } + + if (versionChecker is null) + { + throw new InvalidOperationException($"{nameof(versionChecker)} cannot be null when {nameof(VersionStringHelpers.TryParseVersionSpecification)} is 'true'"); + } + + if (!versionChecker.CheckIfVersionIsValid(RunnableProjectGenerator.GeneratorVersion)) + { + throw new NotSupportedException(string.Format(LocalizableStrings.RunnableProjectGenerator_Exception_TemplateVersionNotSupported, allowedGeneratorVersions, RunnableProjectGenerator.GeneratorVersion)); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/EvaluatorSelector.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/EvaluatorSelector.cs new file mode 100644 index 000000000000..d76173c2eccb --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/EvaluatorSelector.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Expressions.Cpp; +using Microsoft.TemplateEngine.Core.Expressions.Cpp2; +using Microsoft.TemplateEngine.Core.Expressions.MSBuild; +using Microsoft.TemplateEngine.Core.Expressions.VisualBasic; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal delegate bool ConditionStringEvaluator(ILogger logger, string text, IVariableCollection variables); + + internal enum EvaluatorType + { + /// + /// C++ V2 style evaluator, see for more details. + /// + CPP2, + + /// + /// C++ style evaluator, see for more details. + /// + CPP, + + /// + /// MSBuild style evaluator, see for more details. + /// + MSBuild, + + /// + /// VB style evaluator, see for more details. + /// + VB + } + + internal static class EvaluatorSelector + { + /// + /// Gets based on selected . + /// + /// The evaluator type to use. See for more info. + /// . + /// when is not supported (unknown). + internal static ConditionStringEvaluator SelectStringEvaluator(EvaluatorType evaluatorType) + { + ConditionStringEvaluator evaluator = evaluatorType switch + { + EvaluatorType.CPP2 => Cpp2StyleEvaluatorDefinition.EvaluateFromString, + EvaluatorType.CPP => CppStyleEvaluatorDefinition.EvaluateFromString, + EvaluatorType.MSBuild => MSBuildStyleEvaluatorDefinition.EvaluateFromString, + EvaluatorType.VB => VisualBasicStyleEvaluatorDefintion.EvaluateFromString, + _ => throw new NotSupportedException($"{evaluatorType} is not supported.") + }; + return evaluator; + } + + /// + /// Parses to . + /// + /// the string value to parse. + /// default to use, when the is null or empty. + /// The parameter is optional. In case it is not passed or is passed, is used. + /// Parsed . + /// when cannot is unknown and cannot be parsed. + internal static EvaluatorType ParseEvaluatorName(string? name, EvaluatorType? @default = null) + { + EvaluatorType defaultType = @default ?? EvaluatorType.CPP; + string evaluatorName = name ?? string.Empty; + EvaluatorType evaluator = evaluatorName switch + { + "" => defaultType, + "C++2" => EvaluatorType.CPP2, + "C++" => EvaluatorType.CPP, + "MSBUILD" => EvaluatorType.MSBuild, + "VB" => EvaluatorType.VB, + _ => throw new TemplateAuthoringException(string.Format(LocalizableStrings.EvaluatorSelector_Exception_UnknownEvaluator, evaluatorName)), + }; + return evaluator; + } + + /// + /// Gets based on selected . + /// + /// The evaluator type to use. See for more info. + /// . + /// when is not supported (unknown). + internal static ConditionEvaluator Select(EvaluatorType evaluatorType) + { + ConditionEvaluator evaluator = evaluatorType switch + { + EvaluatorType.CPP2 => Cpp2StyleEvaluatorDefinition.Evaluate, + EvaluatorType.CPP => CppStyleEvaluatorDefinition.Evaluate, + EvaluatorType.MSBuild => MSBuildStyleEvaluatorDefinition.Evaluate, + EvaluatorType.VB => VisualBasicStyleEvaluatorDefintion.Evaluate, + _ => throw new NotSupportedException($"{evaluatorType} is not supported.") + }; + return evaluator; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileRenameGenerator.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileRenameGenerator.cs new file mode 100644 index 000000000000..430cea4366e8 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileRenameGenerator.cs @@ -0,0 +1,165 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal class FileRenameGenerator + { + private readonly IProcessor _symbolRenameProcessor; + + internal FileRenameGenerator( + IEngineEnvironmentSettings environmentSettings, + string? sourceName, + object? resolvedNameParamValue, + IVariableCollection variables, + IReadOnlyList symbolBasedFileRenames) + { + _symbolRenameProcessor = SetupSymbolBasedRenameProcessor(environmentSettings, sourceName, resolvedNameParamValue, variables, symbolBasedFileRenames); + } + + /// + /// Creates the complete file rename mapping for the template invocation being processed. + /// Renames are based on: + /// - parameters with a FileRename specified + /// - the source and target names. + /// Any input fileRenames will be applied before the parameter symbol renames. + /// + internal static IReadOnlyDictionary AugmentFileRenames( + IEngineEnvironmentSettings environmentSettings, + string? sourceName, + IDirectory templateSourceDir, + string sourceDirectory, + ref string targetDirectory, + object? resolvedNameParamValue, + IVariableCollection variables, + Dictionary fileRenames, + IReadOnlyList? symbolBasedFileRenames = null) + { + Dictionary allRenames = new(StringComparer.Ordinal); + + IProcessor sourceRenameProcessor = SetupSourceBasedRenameProcessor(environmentSettings, fileRenames); + IProcessor symbolRenameProcessor = SetupSymbolBasedRenameProcessor(environmentSettings, sourceName, resolvedNameParamValue, variables, symbolBasedFileRenames); + + //replace sourceName in target directory, if needed + if (sourceName is not null && resolvedNameParamValue is not null) + { + string targetName = resolvedNameParamValue.ToString().Trim(); + targetDirectory = targetDirectory.Replace(sourceName, targetName); + } + + IDirectory? sourceBaseDirectoryInfo = templateSourceDir.DirectoryInfo(sourceDirectory.TrimEnd('/')); + + if (sourceBaseDirectoryInfo is null) + { + return allRenames; + } + + foreach (IFileSystemInfo fileSystemEntry in sourceBaseDirectoryInfo.EnumerateFileSystemInfos("*", SearchOption.AllDirectories)) + { + string sourceTemplateRelativePath = fileSystemEntry.PathRelativeTo(sourceBaseDirectoryInfo); + + // first apply the sources renames, then apply the symbol renames to that result. + string renameFromSourcesValue = ApplyRenameProcessor(sourceRenameProcessor, sourceTemplateRelativePath); + string renameFinalTargetValue = ApplyRenameProcessor(symbolRenameProcessor, renameFromSourcesValue); + + if (!string.Equals(sourceTemplateRelativePath, renameFinalTargetValue, StringComparison.Ordinal)) + { + allRenames[sourceTemplateRelativePath] = renameFinalTargetValue; + } + } + + return allRenames; + } + + /// + /// Applies configured renames to . + /// + internal string ApplyRenameToString(string stringToReplace) => ApplyRenameProcessor(_symbolRenameProcessor, stringToReplace); + + private static string ApplyRenameProcessor(IProcessor processor, string sourceFilename) + { + using MemoryStream source = new(Encoding.UTF8.GetBytes(sourceFilename)); + using MemoryStream target = new(); + + _ = processor.Run(source, target); + return Encoding.UTF8.GetString(target.ToArray()); + } + + /// + /// Creates and returns the processor used to create the file rename mapping for source based file renames. + /// + private static IProcessor SetupSourceBasedRenameProcessor(IEngineEnvironmentSettings environmentSettings, IReadOnlyDictionary substringReplacementMap) + { + List operations = new(); + foreach (KeyValuePair replacement in substringReplacementMap) + { + IOperationProvider replacementOperation = new Replacement(replacement.Key.TokenConfig(), replacement.Value, null, true); + operations.Add(replacementOperation); + } + return SetupProcessor(environmentSettings, operations); + } + + /// + /// Creates and returns the processor used to create the file rename mapping based on the symbols with fileRename defined. + /// Also sets up rename for the target directory. + /// + private static IProcessor SetupSymbolBasedRenameProcessor( + IEngineEnvironmentSettings environmentSettings, + string? sourceName, + object? resolvedNameParamValue, + IVariableCollection variables, + IReadOnlyList? symbolBasedFileRenames) + { + List operations = new(); + if (resolvedNameParamValue != null && sourceName != null) + { + SetupRenameForTargetDirectory(sourceName, resolvedNameParamValue, operations); + } + + if (symbolBasedFileRenames != null) + { + foreach (IReplacementTokens fileRenameToken in symbolBasedFileRenames) + { + if (variables.TryGetValue(fileRenameToken.VariableName, out object? newValueObject)) + { + string newValue = newValueObject?.ToString() ?? string.Empty; + operations.Add(new Replacement(fileRenameToken.OriginalValue, newValue, null, true)); + } + } + } + return SetupProcessor(environmentSettings, operations); + } + + /// + /// Sets up a rename based on the "name" parameter. + /// + /// + /// + /// + private static void SetupRenameForTargetDirectory( + string sourceName, + object resolvedNameParamValue, + List operations) + { + string targetName = ((string)resolvedNameParamValue).Trim(); + operations.Add(new Replacement(sourceName.TokenConfig(), targetName, null, true)); + } + + private static IProcessor SetupProcessor(IEngineEnvironmentSettings environmentSettings, IReadOnlyList operations) + { + IVariableCollection variables = new VariableCollection(); + EngineConfig config = new(environmentSettings.Host.Logger, variables); + IProcessor processor = Processor.Create(config, operations); + return processor; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileSourceHierarchicalPathMatcher.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileSourceHierarchicalPathMatcher.cs new file mode 100644 index 000000000000..0cda5b56a1d6 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileSourceHierarchicalPathMatcher.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + [Flags] + internal enum FileDispositionStates + { + None = 0, + Include = 0x01, + CopyOnly = 0x02, + Exclude = 0x04 + } + + internal static class EnumExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool Has(this FileDispositionStates value, FileDispositionStates check) + { + return (value & check) == check; + } + } + + internal class FileSourceHierarchicalPathMatcher + { + private readonly IReadOnlyList _evaluators; + + // Caching the most recent result. + private string _cachedEvaluatedPath; + + private FileDispositionStates _cachedStatesForFile; + + internal FileSourceHierarchicalPathMatcher(FileSourceMatchInfo matchInfo) + { + List evaluators = new List(); + + for (int i = matchInfo.Modifiers.Count - 1; i >= 0; i--) + { + evaluators.Add(new FileSourceEvaluablePathMatcher(matchInfo.Modifiers[i])); + } + + evaluators.Add(new FileSourceEvaluablePathMatcher(matchInfo.TopLevelEvaluable)); + _evaluators = evaluators; + + _cachedEvaluatedPath = string.Empty; + _cachedStatesForFile = FileDispositionStates.None; + } + + internal FileDispositionStates Evaluate(string path) + { + if (!string.Equals(path, _cachedEvaluatedPath, StringComparison.Ordinal)) + { + _cachedStatesForFile = EvaluatePath(path); + _cachedEvaluatedPath = path; + } + + return _cachedStatesForFile; + } + + private FileDispositionStates EvaluatePath(string path) + { + FileDispositionStates continuationReason = FileDispositionStates.None; + + foreach (FileSourceEvaluablePathMatcher evaluator in _evaluators) + { + FileDispositionStates state = evaluator.Evaluate(path); + + if (state.Has(FileDispositionStates.Exclude)) + { + return FileDispositionStates.Exclude; + } + else if (state.Has(FileDispositionStates.CopyOnly)) + { + if (state.Has(FileDispositionStates.Include)) + { + return FileDispositionStates.CopyOnly | FileDispositionStates.Include; + } + + continuationReason = FileDispositionStates.CopyOnly; + } + else if (state.Has(FileDispositionStates.Include)) + { + if (continuationReason == FileDispositionStates.CopyOnly) + { + return FileDispositionStates.CopyOnly | FileDispositionStates.Include; + } + + return FileDispositionStates.Include; + } + } + + return FileDispositionStates.None; + } + + private class FileSourceEvaluablePathMatcher + { + private readonly IReadOnlyList _include; + + private readonly IReadOnlyList _exclude; + + private readonly IReadOnlyList _copyOnly; + + internal FileSourceEvaluablePathMatcher(FileSourceEvaluable evaluable) + { + _include = SetupMatcherList(evaluable.Include); + _exclude = SetupMatcherList(evaluable.Exclude); + _copyOnly = SetupMatcherList(evaluable.CopyOnly); + } + + internal FileDispositionStates Evaluate(string path) + { + if (_exclude.Any(x => x.IsMatch(path))) + { + return FileDispositionStates.Exclude; + } + + bool include = _include.Any(x => x.IsMatch(path)); + + if (_copyOnly.Any(x => x.IsMatch(path))) + { + if (include) + { + return FileDispositionStates.CopyOnly | FileDispositionStates.Include; + } + else + { + return FileDispositionStates.CopyOnly; + } + } + + if (include) + { + return FileDispositionStates.Include; + } + + return FileDispositionStates.None; + } + + private static IReadOnlyList SetupMatcherList(IReadOnlyList fileSources) + { + int expect = fileSources?.Count ?? 0; + List paths = new List(expect); + if (fileSources != null && expect > 0) + { + foreach (string source in fileSources) + { + paths.Add(new GlobbingPatternMatcher(source, false)); + } + } + + return paths; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileSourceMatchInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileSourceMatchInfo.cs new file mode 100644 index 000000000000..5915bf321fe9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileSourceMatchInfo.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal class FileSourceEvaluable + { + internal FileSourceEvaluable(IReadOnlyList include, IReadOnlyList exclude, IReadOnlyList copyOnly) + { + Include = include; + Exclude = exclude; + CopyOnly = copyOnly; + } + + internal IReadOnlyList Include { get; } + + internal IReadOnlyList Exclude { get; } + + internal IReadOnlyList CopyOnly { get; } + } + + internal class FileSourceMatchInfo + { + internal FileSourceMatchInfo(string source, string target, FileSourceEvaluable topLevelEvaluable, IReadOnlyDictionary renames, IReadOnlyList modifiers) + { + Source = source; + Target = target; + TopLevelEvaluable = topLevelEvaluable; + Renames = renames; + Modifiers = modifiers; + } + + internal string Source { get; } + + internal string Target { get; } + + internal FileSourceEvaluable TopLevelEvaluable { get; } + + internal IReadOnlyDictionary Renames { get; } + + internal IReadOnlyList Modifiers { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileSourceStateMatcher.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileSourceStateMatcher.cs new file mode 100644 index 000000000000..f42bdb049705 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/FileSourceStateMatcher.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal class FileSourceStateMatcher : IPathMatcher + { + private readonly FileDispositionStates _checkState; + + private readonly FileSourceHierarchicalPathMatcher _stateMatcher; + + internal FileSourceStateMatcher(FileDispositionStates checkState, FileSourceHierarchicalPathMatcher stateMatcher) + { + _checkState = checkState; + _stateMatcher = stateMatcher; + } + + public string Pattern => $"Composite matcher for disposition: {_checkState}"; + + public bool IsMatch(string path) + { + return _stateMatcher.Evaluate(path).Has(_checkState); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobalRunConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobalRunConfig.cs new file mode 100644 index 000000000000..3f3e2d390db8 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobalRunConfig.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal class GlobalRunConfig + { + public IReadOnlyList Operations { get; init; } = []; + + public IVariableConfig VariableSetup { get; init; } = VariableConfig.Default; + + public IReadOnlyList Replacements { get; init; } = []; + + public IReadOnlyList CustomOperations { get; init; } = []; + + public IReadOnlyList SymbolNames { get; init; } = []; + + public IReadOnlyList Macros { get; init; } = []; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobalRunSpec.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobalRunSpec.cs new file mode 100644 index 000000000000..92a73dc80fc9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobalRunSpec.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Expressions.Cpp2; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal class GlobalRunSpec : IGlobalRunSpec + { + private readonly IReadOnlyDictionary _operationConfigLookup; + + internal GlobalRunSpec( + IDirectory templateRoot, + IComponentManager componentManager, + IVariableCollection variables, + IRunnableProjectConfig configuration) + { + List operationConfigReaders = new List(componentManager.OfType()); + Dictionary operationConfigLookup = new Dictionary(); + + foreach (IOperationConfig opConfig in operationConfigReaders) + { + operationConfigLookup[opConfig.Key] = opConfig; + } + + _operationConfigLookup = operationConfigLookup; + + RootVariableCollection = variables; + IgnoreFileNames = configuration.IgnoreFileNames; + Operations = ResolveOperations(configuration.GlobalOperationConfig, templateRoot, variables); + List> specials = new List>(); + + if (configuration.SpecialOperationConfig != null) + { + foreach ((string glob, GlobalRunConfig runConfig) in configuration.SpecialOperationConfig) + { + IReadOnlyList specialOps = []; + + if (runConfig != null) + { + specialOps = ResolveOperations(runConfig, templateRoot, variables); + } + + RunSpec spec = new(specialOps, runConfig?.VariableSetup.FallbackFormat); + specials.Add(new KeyValuePair(new GlobbingPatternMatcher(glob), spec)); + } + } + + Special = specials; + } + + public IReadOnlyList Include { get; private set; } = []; + + public IReadOnlyList Exclude { get; private set; } = []; + + public IReadOnlyList CopyOnly { get; private set; } = []; + + public IReadOnlyList Operations { get; } + + public IVariableCollection RootVariableCollection { get; } + + public IReadOnlyList> Special { get; } + + public IReadOnlyList IgnoreFileNames { get; } + + internal IReadOnlyDictionary Rename { get; private set; } = new Dictionary(); + + public bool TryGetTargetRelPath(string sourceRelPath, out string targetRelPath) + { + return Rename.TryGetValue(sourceRelPath, out targetRelPath); + } + + internal void SetupFileSource(FileSourceMatchInfo source) + { + FileSourceHierarchicalPathMatcher matcher = new FileSourceHierarchicalPathMatcher(source); + Include = new List() { new FileSourceStateMatcher(FileDispositionStates.Include, matcher) }; + Exclude = new List() { new FileSourceStateMatcher(FileDispositionStates.Exclude, matcher) }; + CopyOnly = new List() { new FileSourceStateMatcher(FileDispositionStates.CopyOnly, matcher) }; + Rename = source.Renames ?? new Dictionary(StringComparer.Ordinal); + } + + // Returns a list of operations which contains the custom operations and the default operations. + // If there are custom Conditional operations, don't include the default Conditionals. + // + // Note: we may need a more robust filtering mechanism in the future. + private IReadOnlyList ResolveOperations(GlobalRunConfig runConfig, IDirectory templateRoot, IVariableCollection variables) + { + IReadOnlyList customOperations = SetupCustomOperations(runConfig.CustomOperations, templateRoot, variables); + IReadOnlyList defaultOperations = SetupOperations(templateRoot.MountPoint.EnvironmentSettings, variables, runConfig); + + List operations = new List(customOperations); + + if (customOperations.Any(x => x is Conditional)) + { + operations.AddRange(defaultOperations.Where(op => op is not Conditional)); + } + else + { + operations.AddRange(defaultOperations); + } + + return operations; + } + + private IReadOnlyList SetupOperations(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, GlobalRunConfig runConfig) + { + // default operations + List operations = new List(); + operations.AddRange(runConfig.Operations); + + // replacements + if (runConfig.Replacements != null) + { + foreach (IReplacementTokens replaceSetup in runConfig.Replacements) + { + IOperationProvider? replacement = ReplacementConfig.Setup(environmentSettings, replaceSetup, variables); + if (replacement != null) + { + operations.Add(replacement); + } + } + } + + if (runConfig.VariableSetup.Expand) + { + operations.Add(new ExpandVariables(null, true)); + } + + return operations; + } + + private IReadOnlyList SetupCustomOperations(IReadOnlyList customModel, IDirectory templateRoot, IVariableCollection variables) + { + ITemplateEngineHost host = templateRoot.MountPoint.EnvironmentSettings.Host; + List customOperations = new List(); + + foreach (CustomOperationModel opModel in customModel) + { + string? opType = opModel.Type; + string? condition = opModel.Condition; + + if (string.IsNullOrEmpty(condition) + || Cpp2StyleEvaluatorDefinition.EvaluateFromString(host.Logger, condition!, variables)) + { + if (opType == null) + { + continue; + } + if (opModel.Configuration == null) + { + continue; + } + if (_operationConfigLookup.TryGetValue(opType, out IOperationConfig realConfigObject)) + { + customOperations.AddRange( + realConfigObject.ConfigureFromJson(opModel.Configuration, templateRoot)); + } + else + { + host.Logger.LogWarning($"Operation type = [{opType}] from configuration is unknown."); + } + } + } + + return customOperations; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobbingPatternMatcher.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobbingPatternMatcher.cs new file mode 100644 index 000000000000..7aa4b6126508 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/GlobbingPatternMatcher.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal class GlobbingPatternMatcher : IPathMatcher + { + private readonly Glob _pattern; + + internal GlobbingPatternMatcher(string pattern, bool canBeNameOnlyMatch = true) + { + Pattern = pattern; + _pattern = Glob.Parse(pattern, canBeNameOnlyMatch); + } + + public string Pattern { get; } + + public bool IsMatch(string path) + { + return _pattern.IsMatch(path); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IOperationConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IOperationConfig.cs new file mode 100644 index 000000000000..3fbf890af21c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IOperationConfig.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions +{ + internal interface IOperationConfig : IIdentifiedComponent + { + string Key { get; } + + IEnumerable ConfigureFromJson(string rawConfiguration, IDirectory templateRoot); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IParameterBasedVariableCollection.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IParameterBasedVariableCollection.cs new file mode 100644 index 000000000000..efae9105ed1f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IParameterBasedVariableCollection.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Parameters; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + /// + /// Extends the contract with the metadata + /// about data from template parameters. + /// + internal interface IParameterBasedVariableCollection : IVariableCollection + { + /// + /// Bound and merged parameter data and their metadata. + /// + IParameterSetData ParameterSetData { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IRunnableProjectConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IRunnableProjectConfig.cs new file mode 100644 index 000000000000..350dbd674bd9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/IRunnableProjectConfig.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal interface IRunnableProjectConfig + { + /// + /// Gets the list of to be applied to specific files included in glob. + /// + IReadOnlyList<(string Glob, GlobalRunConfig RunConfig)> SpecialOperationConfig { get; } + + /// + /// Gets the to be applied to all template files. + /// + GlobalRunConfig GlobalOperationConfig { get; } + + /// + /// Gets the list of evaluated sources based on configuration. method should be called first before accessing it. + /// + IReadOnlyList EvaluatedSources { get; } + + IReadOnlyList IgnoreFileNames { get; } + + /// + /// Gets the list of enabled post actions. method should be called first before accessing it. + /// + IReadOnlyList PostActions { get; } + + /// + /// Gets the list of enabled primary outputs. method should be called first before accessing it. + /// + IReadOnlyList PrimaryOutputs { get; } + + /// + /// Evaluates conditional elements based on . + /// + /// + void Evaluate(IVariableCollection rootVariableCollection); + + /// + /// Removes parameter from template. + /// + /// + void RemoveParameter(ITemplateParameter parameter); + + Task EvaluateBindSymbolsAsync(IEngineEnvironmentSettings settings, IVariableCollection variableCollection, CancellationToken cancellationToken); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ListGlobbingPatternMatcher.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ListGlobbingPatternMatcher.cs new file mode 100644 index 000000000000..8175b82ada3a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/ListGlobbingPatternMatcher.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal class ListGlobbingPatternMatcher : IPathMatcher + { + private readonly IReadOnlyList _pathMatchers; + + private string? _displayPattern; + + internal ListGlobbingPatternMatcher(IList patternList) + { + List pathMatchers = new List(); + + foreach (string pattern in patternList) + { + pathMatchers.Add(new GlobbingPatternMatcher(pattern)); + } + + _pathMatchers = pathMatchers; + } + + public string Pattern + { + get + { + if (_displayPattern == null) + { + StringBuilder displaySB = new StringBuilder(128); + displaySB.AppendLine("Composite matcher - matches any of these:"); + + foreach (IPathMatcher matcher in _pathMatchers) + { + displaySB.AppendLine($"\t{matcher.Pattern}"); + } + + _displayPattern = displaySB.ToString(); + } + + return _displayPattern; + } + } + + public bool IsMatch(string path) + { + foreach (IPathMatcher matcher in _pathMatchers) + { + if (matcher.IsMatch(path)) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/LocalizableStrings.resx b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/LocalizableStrings.resx new file mode 100644 index 000000000000..c284032b70b9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/LocalizableStrings.resx @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Id of the post action '{0}' at index '{1}' is not unique. Only the first post action that uses this id will be localized. + {0} is any identifier string that user provided in the file. Such as "myPostAction", "firstPostAction", "postAction1" etc. +{1} is a number representing the zero-based index of the post action. Such as "5", or "0". + + + Post action at index '{0}' should have an 'actionId' to declare the action to be executed. + {0} is a whole number representing the zero-based index of the post action. Such as "5" or "0". +Do not localize: actionId + + + 'id' of the manual instruction '{0}' at index {1} is not unique. Only the first manual instruction that uses this id will be localized. + {0} is any identifier string that user provided in the file. Such as: "myInstruction", "firstInstruction", "instruction1" etc. +{1} is a whole number representing the zero-based index of the manual instruction. Such as "5", or "0". + + + The argument '{0}' configured in '{1}' is not listed in '{2}' and will be skipped for processing. + + + Parameter conditions contain cyclic dependency: [{0}] that is preventing deterministic evaluation. + + + Localization file should only contain elements with type 'string'. Remove elements that are not strings. + + + In localization file under the post action with id '{1}', there are localized strings for manual instruction(s) with ids '{0}'. These manual instructions do not exist in the template.json file and should be removed from localization file. + {0} is a list of single-word identifier strings separated by comma. Such as "myManualInstruction, someUserPickedText, instruction3". +{1} is a single-word identifier string. Such as "myPostAction". + + + Choice parameter '{0}' is invalid. It allows multiple values ('AllowMultipleValues=true'), while some of the configured choices contain separator characters ({1}). Invalid choices: {2} + {0} is the offending symbol name, {1} is a set of separator characters, {2} is a csv list of offending choice values + + + Post action(s) with id(s) '{0}' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + {0} is a list of single-word identifier strings separated by comma. Such as "myManualInstruction, someUserPickedText, instruction3". + + + '{0}' could not be parsed as a regular expression + + + One or more postActions have a malformed or missing manualInstructions value. + + + Missing '{0}'. + + + A non-bool DataType was specified for a regexMatch type symbol + + + Source '{0}' in template does not exist. + + + Source location '{0}' is outside the specified install source location. + + + Source must be a directory, but '{0}' is a file. + + + The template configuration {0} is missing common information: + {0} is template configuration location. + + + Failed to load template from {0}: + {0} is template configuration location. + + + Failed to load template from {0}. +Details: {1} + {0} is template configuration location. + + + The template root is outside the specified install source location. + + + Bind symbol '{0}': failed to convert value '{1}' to datatype '{2}'. The symbol processing is skipped. + {0} - the name of symbol in configuration, {1} - obtained value for a symbol, {2} - defined datatype for a symbol + + + Bind symbol '{0}': failed to convert default value '{1}' to datatype '{2}'. The default value is skipped. + {0} - the name of symbol in configuration, {1} - default value in configuraion, {2} - defined datatype for a symbol + + + Failed to evaluate bind symbol '{0}', it will be skipped. + {0} - name of the symbol in configuration that cannot be evaluated + + + Failed to evaluate binding '{0}', its value is available from multiple sources: {1}. Use the prefixed value ({2}) in template configuration instead. + {0} - binding from template.json configuration +{1} - the comma separated list of sources (localized values) which define the binding +{2} - the comma separated list of prefixes that can be used to define the source of the value + + + Template authoring error encountered while processing macro '{0}': Variable '{1}'. Neither source nor fallback variable was found. + {0} - macro type, {1} - macro name + + + Unrecognized evaluator: '{0}'. + {0} - invalid evaluator value + + + The high port bound '{0}' is greater than the maximum allowed, '{1}' will be used instead. + + + The low port bound '{0}' is less than the minimum allowed, '{1}' will be used instead. + + + The low port bound '{0}' is greater the high port bound '{1}', the default range [{2}-{3}] will be used instead. + + + Generated symbol '{0}': array '{1}' should contain JSON objects with property non-empty '{2}' when '{3}' is '{4}'. + {0} - name of generated symbol, {1} - the invalid property name; {2}, {3}, {4} - the property names and values. + + + Failed to read or parse localization file {0}, it will be skipped from further processing. + + + Locale '{0}' does not match any known cultures. + + + The method '{0}' must be called prior '{1}' property reading. + erty name. + + + Generated symbol '{0}': array '{1}' should contain JSON objects. + {0} - name of generated symbol, {1} - the invalid property name. + + + Generated symbol '{0}': '{1}' is not a valid JSON. + {0} - name of generated symbol, {1} - the invalid property name. + + + Generated symbol '{0}': the regex pattern '{1}' is invalid. + {0} - name of generated symbol, {1} - the invalid regex pattern + + + Generated symbol '{0}' of type '{1}' should have '{2}' property defined. + {0} - name of generated symbol, {1} - the generated symbol type, {2} - the missing property. + + + Generated symbol '{0}': array '{1}' should contain JSON objects with property '{2}'. + {0} - name of generated symbol, {1} - the invalid property name; {2} - the missing property. + + + Generated symbol '{0}': '{1}' should be a valid JSON array. + {0} - name of generated symbol, {1} - the invalid property name. + + + Generated symbol '{0}': '{1}' should be a boolean value. + {0} - name of generated symbol, {1} - the invalid property name. + + + Generated symbol '{0}': '{1}' should be an integer value. + {0} - name of generated symbol, {1} - the invalid property name. + + + Generated symbol '{0}': '{1}' should be a string value. + {0} - name of generated symbol, {1} - the invalid property name. + + + Failed to process macro '{0}' of type '{1}'. + {0} - macro variable name, {1} - macro type (not localized). + + + '{0}' was not processed due to the next issues in dependencies: '{1}'. + {0} - macro name, {1} - dependent macros errors + + + Generated symbol '{0}': type '{1}' is unknown, processing is skipped. + {0} - macro (generated symbol) variable name, {1} - macro type (not localized). + + + The symbol '{0}': unable to find a form '{1}', the further processing of the symbol will be skipped. + {0} - the name of the symbol in configuration, {1} - the name of the form in configuration. + + + Variable [{0}] already added with value [{1}]. Cannot add it as implicit variable with value of self. + {0} is the choiceKey, {1} is the existing conflict value + + + Template configuration must be a file, but {0} is not a file. + {0} is template configuration location. + + + Template localization configuration must be a file, but {0} is not a file. + {0} is template localization configuration location. + + + The template configuration {0} is not valid. + {0} is template configuration location. + + + The template is not compatible with generator version: allowed versions: {0}, generator version: {1}. + + + Localization file {0} is not compatible with base configuration {1}, and will be skipped. + {0} and {1} are template configuration locations. + + + Failed to load additional configuration file {0}, the file does not exist. + + + Failed to load additional configuration file {0}. Additional configuration file names must end with '.{1}'. + + + '{0}' should contain objects. + {0} is name of JSON property + + + Constraint definition '{0}' does not contain mandatory property '{1}'. + {0} - JSON with constraint defintion, {1} - missing property. + + + The symbol '{0}' of type '{1}' is incorrect: mandatory property '{2}' is not set. The symbol will be skipped. + + + Found disallowed value for IsRequired property: [{0}], expected boolean value. + {0} - the json property + + + Found disallowed value for IsRequired property: [{0}], expected boolean or string (condition) value + {0} - the json property + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/LocalizationModel.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/LocalizationModel.cs new file mode 100644 index 000000000000..55143767d662 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/LocalizationModel.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Localization +{ + /// + /// Represents the data model that contains the localized strings for a template. + /// + internal class LocalizationModel + { + public LocalizationModel( + string? name, + string? description, + string? author, + IReadOnlyDictionary parameterSymbols, + IReadOnlyDictionary postActions) + { + Name = name; + Description = description; + Author = author; + ParameterSymbols = parameterSymbols ?? throw new ArgumentNullException(nameof(parameterSymbols)); + PostActions = postActions ?? throw new ArgumentNullException(nameof(postActions)); + } + + /// + /// Gets the localized author name. + /// + public string? Author { get; } + + /// + /// Gets the localized template name. + /// + public string? Name { get; } + + /// + /// Gets the localized template description. + /// + public string? Description { get; } + + /// + /// Gets the localization models for the parameter symbols defined in this template. + /// + public IReadOnlyDictionary ParameterSymbols { get; } + + /// + /// Gets the localization models for the post actions defined in this template. + /// The keys represent the id of the post actions. + /// + public IReadOnlyDictionary PostActions { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/ParameterSymbolLocalizationModel.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/ParameterSymbolLocalizationModel.cs new file mode 100644 index 000000000000..f0698ee7b8ac --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/ParameterSymbolLocalizationModel.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Localization +{ + internal class ParameterSymbolLocalizationModel : IParameterSymbolLocalizationModel + { + internal ParameterSymbolLocalizationModel(string name, string? displayName, string? description, IReadOnlyDictionary choices) + { + Name = name; + DisplayName = displayName; + Description = description; + Choices = choices; + } + + public string Name { get; } + + public string? DisplayName { get; } + + public string? Description { get; } + + public IReadOnlyDictionary Choices { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/PostActionLocalizationModel.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/PostActionLocalizationModel.cs new file mode 100644 index 000000000000..36253b560c02 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/PostActionLocalizationModel.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Localization +{ + internal class PostActionLocalizationModel + { + /// + /// Localized description of the post action. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the localized manual instructions of the post action. + /// The keys represent the manual instruction ids. + /// + public IReadOnlyDictionary Instructions { get; set; } = new Dictionary(); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/TemplateLocalizationInfo.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/TemplateLocalizationInfo.cs new file mode 100644 index 000000000000..784bf77a7d57 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Localization/TemplateLocalizationInfo.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Localization +{ + internal class TemplateLocalizationInfo : ILocalizationLocator + { + private readonly List _validationEntries = new(); + + public TemplateLocalizationInfo(CultureInfo locale, LocalizationModel model, IFile file) + { + Locale = locale; + Model = model; + File = file; + } + + string ILocalizationLocator.Locale => Locale.Name; + + string ILocalizationLocator.ConfigPlace => File.FullPath; + + string ILocalizationLocator.Identity => throw new NotImplementedException(); + + string? ILocalizationLocator.Author => Model.Author; + + string? ILocalizationLocator.Name => Model.Name; + + string? ILocalizationLocator.Description => Model.Description; + + IReadOnlyDictionary ILocalizationLocator.ParameterSymbols => Model.ParameterSymbols; + + public bool IsValid => !ValidationErrors.Any(e => e.Severity == IValidationEntry.SeverityLevel.Error); + + public IReadOnlyList ValidationErrors => _validationEntries; + + internal CultureInfo Locale { get; } + + internal LocalizationModel Model { get; } + + internal IFile File { get; } + + internal void AddValidationEntry(IValidationEntry entry) + { + _validationEntries.Add(entry); + } + + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/LocalizationModelDeserializer.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/LocalizationModelDeserializer.cs new file mode 100644 index 000000000000..abaf99372754 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/LocalizationModelDeserializer.cs @@ -0,0 +1,185 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Localization; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal static class LocalizationModelDeserializer + { + /// + /// Character to be used when separating a key into parts. + /// + private const char KeySeparator = '/'; + + /// + /// Deserializes the given localization file into an . + /// + /// File to be deserialized. + /// loaded localization model. + /// is null. + /// The file does not contain valid JSON data, JSON data contains element other than strings. + public static LocalizationModel Deserialize(IFile file) + { + _ = file ?? throw new ArgumentNullException(nameof(file)); + + System.Text.Json.Nodes.JsonObject srcObject = file.ReadJObjectFromIFile(); + + List<(string Key, string Value)> localizedStrings = srcObject + .Select(p => p.Value?.GetValueKind() == JsonValueKind.String ? (p.Key, p.Value.GetValue()) : throw new Exception(LocalizableStrings.Authoring_InvalidJsonElementInLocalizationFile)) + .ToList(); + + var symbols = LoadSymbolModels(localizedStrings); + var postActions = LoadPostActionModels(localizedStrings); + + return new LocalizationModel( + name: localizedStrings.FirstOrDefault(s => s.Key == "name").Value, + description: localizedStrings.FirstOrDefault(s => s.Key == "description").Value, + author: localizedStrings.FirstOrDefault(s => s.Key == "author").Value, + symbols, + postActions); + } + + /// + /// Generates parameter symbol localization models from the given localized strings. + /// + private static IReadOnlyDictionary LoadSymbolModels(List<(string Key, string Value)> localizedStrings) + { + var results = new Dictionary(); + + // Property names are in format: symbols/framework/choices/net5.0/description + // Split them using '/' and store together with the localized string. + IEnumerable<(IEnumerable NameParts, string LocalizedString)> strings = localizedStrings + .Where(s => s.Key.StartsWith("symbols" + KeySeparator)) + .Select(s => (s.Key.Split(KeySeparator).AsEnumerable().Skip(1), s.Value)) + .ToList(); + + // Group by symbol name + foreach (var parameterParts in strings.GroupBy(p => p.NameParts.FirstOrDefault())) + { + if (string.IsNullOrEmpty(parameterParts.Key)) + { + // Symbol with no name. Ignore. + continue; + } + + string symbolName = parameterParts.Key; + string? displayName = parameterParts.SingleOrDefault(p => p.NameParts.Skip(1).FirstOrDefault() == "displayName").LocalizedString; + string? description = parameterParts.SingleOrDefault(p => p.NameParts.Skip(1).FirstOrDefault() == "description").LocalizedString; + + IReadOnlyDictionary? choiceModels = LoadChoiceModels(parameterParts + .Where(p => p.NameParts.Skip(1).FirstOrDefault() == "choices") + .Select(p => (p.NameParts.Skip(2), p.LocalizedString))); + + ParameterSymbolLocalizationModel paramLoc = new ParameterSymbolLocalizationModel( + symbolName, + displayName, + description, + choiceModels); + + results[symbolName] = paramLoc; + } + + return results; + } + + /// + /// Generates post action localization models. The given parts should begin with the choice name + /// as shown below (prior parts of the name such as "symbols" and parameter name shouldn't be included). + /// + /// net5.0/displayName + /// net5.0/description + /// netstandard2.0/description + /// + /// + private static IReadOnlyDictionary LoadChoiceModels(IEnumerable<(IEnumerable NameParts, string LocalizedString)> strings) + { + var results = new Dictionary(); + + foreach (var choiceParts in strings.GroupBy(p => p.NameParts.FirstOrDefault())) + { + if (string.IsNullOrEmpty(choiceParts.Key)) + { + // Choice with no name. Ignore + continue; + } + + string? displayName = choiceParts.SingleOrDefault(p => p.NameParts.Skip(1).FirstOrDefault() == "displayName").LocalizedString; + string? description = choiceParts.SingleOrDefault(p => p.NameParts.Skip(1).FirstOrDefault() == "description").LocalizedString; + + results.Add(choiceParts.Key, new ParameterChoiceLocalizationModel(displayName, description)); + } + + return results; + } + + /// + /// Generates post action localization models from the given localized strings. + /// + private static IReadOnlyDictionary LoadPostActionModels(List<(string Key, string Value)> localizedStrings) + { + var results = new Dictionary(); + + // Property names are in format: postActions/actionId/manualInstructions/instructionId/description + // Split them using '/' and store together with the localized string. + IEnumerable<(IEnumerable NameParts, string LocalizedString)> strings = localizedStrings + .Where(s => s.Key.StartsWith("postActions" + KeySeparator)) + .Select(s => (s.Key.Split(KeySeparator).AsEnumerable().Skip(1), s.Value)); + + foreach (var postActionParts in strings.GroupBy(p => p.NameParts.FirstOrDefault())) + { + if (string.IsNullOrEmpty(postActionParts.Key)) + { + // Post action with no ID. Ignore. + continue; + } + + string postActionId = postActionParts.Key; + string? description = postActionParts.SingleOrDefault(p => p.NameParts.Skip(1).FirstOrDefault() == "description").LocalizedString; + var instructions = LoadManualInstructionModels(postActionParts + .Where(s => s.NameParts.Skip(1).FirstOrDefault()?.StartsWith("manualInstructions") == true) + .Select(s => (s.NameParts.Skip(2), s.LocalizedString))); + + results[postActionId] = new PostActionLocalizationModel() + { + Description = description, + Instructions = instructions, + }; + } + + return results; + } + + /// + /// Generates manual instruction localization models. + /// The given parts should begin with the manual instruction id. + /// + /// instructionToRestore/text + /// instructionToRestart/text + /// + /// + /// The localized manual instructions where each key represents the instruction id. + private static IReadOnlyDictionary LoadManualInstructionModels(IEnumerable<(IEnumerable NameParts, string LocalizedString)> strings) + { + var results = new Dictionary(); + + foreach (var instructionParts in strings.GroupBy(p => p.NameParts.FirstOrDefault())) + { + if (string.IsNullOrEmpty(instructionParts.Key)) + { + // Instruction with no ID. Ignore. + continue; + } + + string id = instructionParts.Key; + string? text = instructionParts.SingleOrDefault(p => p.NameParts.Skip(1).FirstOrDefault() == "text").LocalizedString; + results[id] = text; + } + + return results; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/MacroProcessingException.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/MacroProcessingException.cs new file mode 100644 index 000000000000..7fc574b3a738 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/MacroProcessingException.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal sealed class MacroProcessingException : ContentGenerationException + { + internal MacroProcessingException(IMacroConfig config) : base(string.Format(LocalizableStrings.MacroProcessingException_Message, config.VariableName, config.Type)) + { + } + + internal MacroProcessingException(IMacroConfig config, Exception innerException) : base(string.Format(LocalizableStrings.MacroProcessingException_Message, config.VariableName, config.Type), innerException) + { + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/MacroProcessor.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/MacroProcessor.cs new file mode 100644 index 000000000000..c673be30b046 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/MacroProcessor.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects +{ + internal static class MacroProcessor + { + /// + /// Processes the macros defined in . + /// + /// when config is invalid. + /// when the error occurs when macro is processed. + internal static void ProcessMacros( + IEngineEnvironmentSettings environmentSettings, + IReadOnlyList macroConfigs, + IVariableCollection variables) + { + bool deterministicMode = IsDeterministicModeEnabled(environmentSettings); + Dictionary knownMacros = environmentSettings.Components.OfType().ToDictionary(m => m.Type, m => m); + + foreach (IMacroConfig config in macroConfigs) + { + // Errors in macro dependencies are not supposed to interrupt template generation. + // Skip this macro and add info in the output. + if (config is IMacroConfigDependency macroWithDep && macroWithDep.Dependencies.Any()) + { + HashSet dependentMacros = GetDependentMacros(config, macroConfigs); + if (dependentMacros.OfType().Any(bmc => bmc.MacroErrors.Any())) + { + environmentSettings.Host.Logger.LogWarning( + string.Format( + LocalizableStrings.MacroProcessing_Warning_DependencyErrors, + config.VariableName, + string.Join(",", dependentMacros.OfType().SelectMany(d => d.MacroErrors)))); + + continue; + } + } + + try + { + if (knownMacros.TryGetValue(config.Type, out IMacro executingMacro)) + { + if (deterministicMode && executingMacro is IDeterministicModeMacro detMacro) + { + detMacro.EvaluateConfigDeterministically(environmentSettings, variables, config); + } + else + { + executingMacro.EvaluateConfig(environmentSettings, variables, config); + } + } + else + { + environmentSettings.Host.Logger.LogWarning(LocalizableStrings.MacroProcessor_Warning_UnknownMacro, config.VariableName, config.Type); + } + } + //TemplateAuthoringException means that config was invalid, just pass it. + catch (Exception ex) when (ex is not TemplateAuthoringException) + { + throw new MacroProcessingException(config, ex); + } + } + } + + internal static IReadOnlyList SortMacroConfigsByDependencies(IReadOnlyList symbols, IReadOnlyList macroConfigs) + { + IEnumerable<(IMacroConfig, HashSet)> preparedMacroConfigs = macroConfigs.Select(mc => + { + if (mc is IMacroConfigDependency macroWithDeps) + { + macroWithDeps.ResolveSymbolDependencies(symbols); + return (mc, GetDependentMacros(mc, macroConfigs)); + } + return (mc, new HashSet()); + }); + + DirectedGraph parametersDependenciesGraph = new(preparedMacroConfigs.ToDictionary(mc => mc.Item1, mc => mc.Item2)); + if (!parametersDependenciesGraph.TryGetTopologicalSort(out IReadOnlyList sortedConfigs) && parametersDependenciesGraph.HasCycle(out IReadOnlyList cycle)) + { + throw new TemplateAuthoringException( + string.Format( + LocalizableStrings.Authoring_CyclicDependencyInSymbols, + cycle.Select(p => p.VariableName).ToCsvString()), + "Symbol circle"); + } + return sortedConfigs; + } + + private static HashSet GetDependentMacros(IMacroConfig config, IReadOnlyList allMacroConfigs) + { + HashSet dependents = new(); + if (config is not IMacroConfigDependency macroWithDep) + { + return dependents; + } + + foreach (string dependent in macroWithDep.Dependencies) + { + IMacroConfig? macro = allMacroConfigs.FirstOrDefault(mc => mc.VariableName == dependent); + if (macro != null) + { + dependents.Add(macro); + } + } + return dependents; + } + + private static bool IsDeterministicModeEnabled(IEngineEnvironmentSettings environmentSettings) + { + string? unparsedValue = environmentSettings.Environment.GetEnvironmentVariable("TEMPLATE_ENGINE_ENABLE_DETERMINISTIC_MODE"); + return bool.TryParse(unparsedValue, out bool result) && result; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/BaseGeneratedSymbolMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/BaseGeneratedSymbolMacro.cs new file mode 100644 index 000000000000..1ae6f257f199 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/BaseGeneratedSymbolMacro.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + /// + /// Base class for the macro defined via generated symbol. + /// + /// The macro config. +#pragma warning disable CS0618 // Type or member is obsolete + internal abstract class BaseGeneratedSymbolMacro : BaseMacro, IGeneratedSymbolMacro, IGeneratedSymbolMacro, IDeferredMacro where T : BaseMacroConfig, IMacroConfig +#pragma warning restore CS0618 // Type or member is obsolete + { + public IMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IMacroConfig rawConfig) + { + if (rawConfig is not IGeneratedSymbolConfig deferredConfig) + { + throw new InvalidCastException($"Couldn't cast the {nameof(rawConfig)} as {nameof(IGeneratedSymbolConfig)}."); + } + return CreateConfig(environmentSettings, deferredConfig); + } + + public void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, IGeneratedSymbolConfig deferredConfig) + { + Evaluate(environmentSettings, vars, CreateConfig(environmentSettings, deferredConfig)); + } + + public abstract T CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig); + + IMacroConfig IGeneratedSymbolMacro.CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig generatedSymbolConfig) => CreateConfig(environmentSettings, generatedSymbolConfig); + } + + /// + /// Base class for the macro defined via generated symbol, that may run non-deterministically and implements deterministic mode for the macro. + /// Note: this class only implements deterministic mode for the macro as . To implement deterministic mode for direct generated symbol evaluation, use . + /// + /// The macro config. + internal abstract class BaseNondeterministicMacro : BaseGeneratedSymbolMacro, IDeterministicModeMacro where T : BaseMacroConfig, IMacroConfig + { + public abstract void EvaluateDeterministically(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, T config); + } + + /// + /// Base class for the macro defined via generated symbol, that may run non-deterministically and implements deterministic mode for the macro as generated symbol. + /// + /// The macro config. + internal abstract class BaseNondeterministicGenSymMacro : BaseNondeterministicMacro, IDeterministicModeMacro, IDeterministicModeMacro where T : BaseMacroConfig, IMacroConfig + { + //public void EvaluateDeterministically(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, IGeneratedSymbolConfig config) => EvaluateDeterministically(environmentSettings, variables, CreateConfig(environmentSettings, config)); + + public void EvaluateConfigDeterministically(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, IMacroConfig config) + { + if (config is not T calculatedConfig) + { + throw new InvalidCastException($"Couldn't cast the {nameof(config)} as {typeof(T)}."); + } + EvaluateDeterministically(environmentSettings, variables, calculatedConfig); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/BaseMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/BaseMacro.cs new file mode 100644 index 000000000000..1ac8940898da --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/BaseMacro.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal abstract class BaseMacro : IMacro where T : BaseMacroConfig, IMacroConfig + { + public abstract string Type { get; } + + public abstract Guid Id { get; } + + public abstract void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variableCollection, T config); + + [Obsolete] + public void EvaluateConfig(IEngineEnvironmentSettings environmentSettings, IVariableCollection variableCollection, IMacroConfig config) + { + if (config is not T castConfig) + { + throw new InvalidCastException($"Couldn't cast the {nameof(config)} as {typeof(T).Name}."); + } + Evaluate(environmentSettings, variableCollection, castConfig); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/BaseMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/BaseMacroConfig.cs new file mode 100644 index 000000000000..71ce191c146e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/BaseMacroConfig.cs @@ -0,0 +1,218 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Core.Expressions.Cpp2; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal abstract class BaseMacroConfig : IMacroConfig + { + protected BaseMacroConfig(string type, string variableName, string? dataType = null) + { + if (string.IsNullOrWhiteSpace(type)) + { + throw new ArgumentException($"'{nameof(type)}' cannot be null or whitespace.", nameof(type)); + } + Type = type; + if (string.IsNullOrWhiteSpace(variableName)) + { + throw new ArgumentException($"'{nameof(variableName)}' cannot be null or whitespace.", nameof(variableName)); + } + VariableName = variableName; + if (!string.IsNullOrWhiteSpace(dataType)) + { + DataType = dataType!; + } + } + + public string VariableName { get; } + + public string Type { get; } + + internal string DataType { get; } = "string"; + + internal IList MacroErrors { get; set; } = new List(); + } + + internal abstract class BaseMacroConfig : BaseMacroConfig, IMacroConfig + where TMacro : IMacro + where TMacroConfig : BaseMacroConfig, IMacroConfig + { + private HashSet _dependencies = new HashSet(); + + protected BaseMacroConfig(TMacro macro, string variableName, string? dataType = null) + : base(macro.Type, variableName, dataType) { } + + public HashSet Dependencies + { + get + { + if (!MacroDependenciesResolved) + { + throw new ArgumentException(string.Format( + LocalizableStrings.MacroConfig_Exception_AccessToDependencies, nameof(PopulateMacroConfigDependency), nameof(Dependencies))); + } + + return _dependencies; + } + set => _dependencies = value; + } + + protected bool MacroDependenciesResolved { get; set; } + + protected static string? GetOptionalParameterValue(IGeneratedSymbolConfig config, string parameterName, string? defaultValue = default) + { + return GetOptionalParameterValue(config, parameterName, ConvertJTokenToString, defaultValue); + } + + protected static TVal? GetOptionalParameterValue(IGeneratedSymbolConfig config, string parameterName, Func converter, TVal? defaultValue = default) + { + if (config.Parameters.TryGetValue(parameterName, out string token)) + { + return converter(token, config, parameterName); + } + return defaultValue; + } + + protected static bool ConvertJTokenToBool(string token, IGeneratedSymbolConfig config, string parameterName) + { + try + { + var jToken = JExtensions.ParseJsonNode(token); + if (jToken != null && jToken.TryParseBool(out bool result)) + { + return result; + } + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_ValueShouldBeBoolean, config.VariableName, parameterName), config.VariableName); + } + catch (Exception ex) when (ex is not TemplateAuthoringException) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_InvalidJSON, config.VariableName, parameterName), config.VariableName, ex); + } + } + + protected static int ConvertJTokenToInt(string token, IGeneratedSymbolConfig config, string parameterName) + { + try + { + var jToken = JExtensions.ParseJsonNode(token); + if (jToken != null && jToken.TryParseInt(out int result)) + { + return result; + } + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_ValueShouldBeInteger, config.VariableName, parameterName), config.VariableName); + } + catch (Exception ex) when (ex is not TemplateAuthoringException) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_InvalidJSON, config.VariableName, parameterName), config.VariableName, ex); + } + } + + protected static string ConvertJTokenToString(string token, IGeneratedSymbolConfig config, string parameterName) + { + try + { + var jToken = JExtensions.ParseJsonNode(token); + if (jToken is not JsonValue val) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_ValueShouldBeString, config.VariableName, parameterName), config.VariableName); + } + return val.GetValueKind() == JsonValueKind.String ? val.GetValue() : val.ToJsonString(); + } + catch (Exception ex) when (ex is not TemplateAuthoringException) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_InvalidJSON, config.VariableName, parameterName), config.VariableName, ex); + } + } + + protected static JsonArray ConvertJTokenToJArray(string token, IGeneratedSymbolConfig config, string parameterName) + { + try + { + var jToken = JExtensions.ParseJsonNode(token); + if (jToken == null || jToken.GetValueKind() != JsonValueKind.Array) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_ValueShouldBeArray, config.VariableName, parameterName), config.VariableName); + } + return (JsonArray)jToken; + } + catch (Exception ex) when (ex is not TemplateAuthoringException) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_InvalidJSON, config.VariableName, parameterName), config.VariableName, ex); + } + } + + protected static void IsValidRegex(string regex, IGeneratedSymbolConfig? generatedSymbolConfig = null) + { + try + { + Regex.Match(string.Empty, regex); + } + catch (ArgumentException) + { + if (generatedSymbolConfig is not null) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_InvalidRegex, generatedSymbolConfig.VariableName, regex), generatedSymbolConfig.VariableName); + } + else + { + throw new ArgumentException($"The pattern '{regex}' is invalid."); + } + } + } + + protected string GetMandatoryParameterValue(IGeneratedSymbolConfig config, string parameterName) + { + if (!config.Parameters.TryGetValue(parameterName, out string token)) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_MissingMandatoryProperty, config.VariableName, Type, parameterName), config.VariableName); + } + return ConvertJTokenToString(token, config, parameterName); + } + + protected TVal GetMandatoryParameterValue(IGeneratedSymbolConfig config, string parameterName, Func converter) + { + if (!config.Parameters.TryGetValue(parameterName, out string token)) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_MissingMandatoryProperty, config.VariableName, Type, parameterName), config.VariableName); + } + return converter(token, config, parameterName); + } + + protected JsonArray GetMandatoryParameterArray(IGeneratedSymbolConfig config, string parameterName) + { + if (!config.Parameters.TryGetValue(parameterName, out string token)) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_MissingMandatoryProperty, config.VariableName, Type, parameterName), config.VariableName); + } + return ConvertJTokenToJArray(token, config, parameterName); + } + + protected void PopulateMacroConfigDependencies( + string condition, + IReadOnlyList symbols) + { + var referencedVariablesKeys = new HashSet(); + var expression = Cpp2StyleEvaluatorDefinition.GetEvaluableExpression( + NullLogger.Instance, + condition, + new VariableCollection(null, symbols.ToDictionary(s => s, s => s as object)), + out var _, + referencedVariablesKeys); + + referencedVariablesKeys.ForEach(PopulateMacroConfigDependency); + } + + private void PopulateMacroConfigDependency(string referencedValue) + { + Dependencies.Add(referencedValue); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CaseChangeMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CaseChangeMacro.cs new file mode 100644 index 000000000000..94fa812acd82 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CaseChangeMacro.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class CaseChangeMacro : BaseGeneratedSymbolMacro + { + public override Guid Id { get; } = new Guid("10919118-4E13-4FA9-825C-3B4DA855578E"); + + public override string Type => "casing"; + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variableCollection, CaseChangeMacroConfig config) + { + string value = string.Empty; + if (!variableCollection.TryGetValue(config.Source, out object? working)) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: Source variable '{sourceVar}' was not found, skipping processing for macro '{var}'.", nameof(CaseChangeMacro), config.Source, config.VariableName); + return; + } + if (working == null) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: The value of source variable '{sourceVar}' is null, skipping processing for macro '{var}'.", nameof(CaseChangeMacro), config.Source, config.VariableName); + return; + } + value = working.ToString(); + value = config.ToLower ? value.ToLowerInvariant() : value.ToUpperInvariant(); + variableCollection[config.VariableName] = value; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Assigned variable '{var}' to '{value}'.", nameof(CaseChangeMacro), config.VariableName, value); + } + + public override CaseChangeMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) => new(this, deferredConfig); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CaseChangeMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CaseChangeMacroConfig.cs new file mode 100644 index 000000000000..0f6749e7dfb3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CaseChangeMacroConfig.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class CaseChangeMacroConfig : BaseMacroConfig, IMacroConfigDependency + { + internal CaseChangeMacroConfig(CaseChangeMacro macro, string variableName, string? dataType, string sourceVariable, bool toLower) + : base(macro, variableName, dataType) + { + if (string.IsNullOrWhiteSpace(sourceVariable)) + { + throw new System.ArgumentException($"'{nameof(sourceVariable)}' cannot be null or whitespace.", nameof(sourceVariable)); + } + + Source = sourceVariable; + ToLower = toLower; + } + + internal CaseChangeMacroConfig(CaseChangeMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) + : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType) + { + Source = GetMandatoryParameterValue(generatedSymbolConfig, "source"); + ToLower = GetOptionalParameterValue(generatedSymbolConfig, "toLower", ConvertJTokenToBool, defaultValue: true); + } + + public string Source { get; } + + internal bool ToLower { get; } + + public void ResolveSymbolDependencies(IReadOnlyList symbols) + { + MacroDependenciesResolved = true; + PopulateMacroConfigDependencies(Source, symbols); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CoalesceMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CoalesceMacro.cs new file mode 100644 index 000000000000..7a63659f221d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CoalesceMacro.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class CoalesceMacro : BaseGeneratedSymbolMacro + { + public override string Type => "coalesce"; + + public override Guid Id { get; } = new("11C6EACF-8D24-42FD-8FC6-84063FCD8F14"); + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variableCollection, CoalesceMacroConfig config) + { + if (variableCollection.TryGetValue(config.SourceVariableName, out object currentSourceValue) && currentSourceValue != null) + { + // The value is equal to the coalesce recognized default value (see coalesce macro doc for details). + if (config.DefaultValue != null && currentSourceValue.ToString().Equals(config.DefaultValue)) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: '{var}': source value '{source}' is not used, because it is equal to default value '{default}'.", nameof(CoalesceMacro), config.VariableName, currentSourceValue, config.DefaultValue); + } + // The value is not specified by user: either coming from default value or host specific default value, etc. + else if (variableCollection is ParameterBasedVariableCollection paramsVariableCollection && + paramsVariableCollection.ParameterSetData.TryGetValue(config.SourceVariableName, out ParameterData? parameterData) && + parameterData!.DataSource is not DataSource.User and not DataSource.DefaultIfNoValue) + { + environmentSettings.Host.Logger.LogDebug( + "[{macro}]: '{var}': source value '{source}' not specified by user (data source: '{dataSource}'), fall back.", + nameof(CoalesceMacro), + config.VariableName, + currentSourceValue, + parameterData.DataSource); + } + else if (currentSourceValue is string str && string.IsNullOrEmpty(str)) + { + //do nothing, empty value for string is equivalent to null. + environmentSettings.Host.Logger.LogDebug("[{macro}]: '{var}': source value '{source}' is an empty string, fall back.", nameof(CoalesceMacro), config.VariableName, currentSourceValue); + } + else + { + variableCollection[config.VariableName] = currentSourceValue; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Assigned variable '{var}' to '{value}'.", nameof(CoalesceMacro), config.VariableName, currentSourceValue); + return; + } + } + if (variableCollection.TryGetValue(config.FallbackVariableName, out object currentFallbackValue) && currentFallbackValue != null) + { + variableCollection[config.VariableName] = currentFallbackValue; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Assigned variable '{var}' to fallback value '{value}'.", nameof(CoalesceMacro), config.VariableName, currentFallbackValue); + return; + } + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was not assigned, neither source nor fallback variable was found.", nameof(CoalesceMacro), config.VariableName); + config.MacroErrors.Add(string.Format(LocalizableStrings.CoalesceMacro_Exception_MissedVariables, nameof(CoalesceMacro), config.VariableName)); + } + + public override CoalesceMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) + => new CoalesceMacroConfig(this, deferredConfig); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CoalesceMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CoalesceMacroConfig.cs new file mode 100644 index 000000000000..641c927ecde0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/CoalesceMacroConfig.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class CoalesceMacroConfig : BaseMacroConfig, IMacroConfigDependency + { + internal CoalesceMacroConfig( + CoalesceMacro macro, + string variableName, + string dataType, + string sourceVariableName, + string? defaultValue, + string fallbackVariableName) + : base(macro, variableName, dataType) + { + if (string.IsNullOrWhiteSpace(sourceVariableName)) + { + throw new ArgumentException($"'{nameof(sourceVariableName)}' cannot be null or whitespace.", nameof(sourceVariableName)); + } + + if (string.IsNullOrWhiteSpace(fallbackVariableName)) + { + throw new ArgumentException($"'{nameof(fallbackVariableName)}' cannot be null or whitespace.", nameof(fallbackVariableName)); + } + + SourceVariableName = sourceVariableName; + DefaultValue = defaultValue; + FallbackVariableName = fallbackVariableName; + } + + internal CoalesceMacroConfig(CoalesceMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) + : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType) + { + SourceVariableName = GetMandatoryParameterValue(generatedSymbolConfig, "sourceVariableName"); + FallbackVariableName = GetMandatoryParameterValue(generatedSymbolConfig, "fallbackVariableName"); + DefaultValue = GetOptionalParameterValue(generatedSymbolConfig, "defaultValue"); + } + + internal string SourceVariableName { get; } + + internal string? DefaultValue { get; } + + internal string FallbackVariableName { get; } + + public void ResolveSymbolDependencies(IReadOnlyList symbols) + { + MacroDependenciesResolved = true; + PopulateMacroConfigDependencies(SourceVariableName, symbols); + PopulateMacroConfigDependencies(FallbackVariableName, symbols); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ConstantMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ConstantMacro.cs new file mode 100644 index 000000000000..0a8d16987584 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ConstantMacro.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class ConstantMacro : BaseGeneratedSymbolMacro + { + public override Guid Id { get; } = new Guid("370996FE-2943-4AED-B2F6-EC03F0B75B4A"); + + public override string Type => "constant"; + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, ConstantMacroConfig config) + { + vars[config.VariableName] = config.Value; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}'.", nameof(ConstantMacro), config.VariableName, config.Value); + } + + public override ConstantMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) => new(this, deferredConfig); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ConstantMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ConstantMacroConfig.cs new file mode 100644 index 000000000000..c549e40616ea --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ConstantMacroConfig.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class ConstantMacroConfig : BaseMacroConfig + { + internal ConstantMacroConfig(ConstantMacro macro, string? dataType, string variableName, string value) + : base(macro, variableName, dataType) + { + Value = value ?? throw new ArgumentNullException(nameof(value)); + } + + internal ConstantMacroConfig(ConstantMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) + : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType) + { + Value = GetMandatoryParameterValue(generatedSymbolConfig, "value"); + } + + internal string Value { get; private set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/EvaluateMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/EvaluateMacro.cs new file mode 100644 index 000000000000..a8d957593c51 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/EvaluateMacro.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + // Symbol.type = "computed" is the only thing that becomes an evaluate macro. + internal class EvaluateMacro : BaseMacro + { + internal const string MacroType = "evaluate"; + + public override Guid Id { get; } = new Guid("BB625F71-6404-4550-98AF-B2E546F46C5F"); + + public override string Type => MacroType; + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variableCollection, EvaluateMacroConfig config) + { + bool result = config.Evaluator(environmentSettings.Host.Logger, config.Condition, variableCollection); + variableCollection[config.VariableName] = result; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}'.", nameof(EvaluateMacro), config.VariableName, result); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/EvaluateMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/EvaluateMacroConfig.cs new file mode 100644 index 000000000000..1f1370b1aeb2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/EvaluateMacroConfig.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class EvaluateMacroConfig : BaseMacroConfig, IMacroConfigDependency + { + private const EvaluatorType DefaultEvaluator = EvaluatorType.CPP2; + private static readonly EvaluateMacro DefaultMacro = new(); + + internal EvaluateMacroConfig(string variableName, string? dataType, string condition, string? evaluator = null) + : base(DefaultMacro, variableName, dataType) + { + if (string.IsNullOrWhiteSpace(condition)) + { + throw new ArgumentException($"'{nameof(condition)}' cannot be null or whitespace.", nameof(condition)); + } + + Condition = condition; + if (!string.IsNullOrEmpty(evaluator)) + { + Evaluator = EvaluatorSelector.SelectStringEvaluator(EvaluatorSelector.ParseEvaluatorName(evaluator, DefaultEvaluator)); + } + } + + internal string Condition { get; } + + internal ConditionStringEvaluator Evaluator { get; private set; } = EvaluatorSelector.SelectStringEvaluator(DefaultEvaluator); + + public void ResolveSymbolDependencies(IReadOnlyList symbols) + { + MacroDependenciesResolved = true; + PopulateMacroConfigDependencies(Condition, symbols); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GeneratePortNumberConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GeneratePortNumberConfig.cs new file mode 100644 index 000000000000..bd066056f04c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GeneratePortNumberConfig.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class GeneratePortNumberConfig : BaseMacroConfig + { + internal const int LowPortDefault = 1024; + internal const int HighPortDefault = 65535; + + // sources of unsafe ports: + // * chrome: https://chromium.googlesource.com/chromium/src.git/+/refs/heads/master/net/base/port_util.cc#27 + // * firefox: https://www-archive.mozilla.org/projects/netlib/portbanning#portlist + // * safari: https://github.com/WebKit/WebKit/blob/42f5a93823a7f087a800cd65c6bc0551dbeb55d3/Source/WTF/wtf/URL.cpp#L969 + private static readonly HashSet UnsafePorts = new HashSet() + { + 1719, // H323 (RAS) + 1720, // H323 (Q931) + 1723, // H323 (H245) + 2049, // NFS + 3659, // apple-sasl / PasswordServer [Apple addition] + 4045, // lockd + 4190, // ManageSieve [Apple addition] + 5060, // SIP + 5061, // SIPS + 6000, // X11 + 6566, // SANE + 6665, // Alternate IRC [Apple addition] + 6666, // Alternate IRC [Apple addition] + 6667, // Standard IRC [Apple addition] + 6668, // Alternate IRC [Apple addition] + 6669, // Alternate IRC [Apple addition] + 6679, // Alternate IRC SSL [Apple addition] + 6697, // IRC+SSL [Apple addition] + 10080, // amanda + }; + + internal GeneratePortNumberConfig(GeneratePortNumberMacro macro, string variableName, string? dataType, int fallback, int low, int high) + : base(macro, variableName, dataType) + { + if (low < LowPortDefault) + { + throw new ArgumentException($"{nameof(low)} should be greater than {LowPortDefault}.", nameof(low)); + } + + if (high > HighPortDefault) + { + throw new ArgumentException($"{nameof(high)} should be less than {HighPortDefault}.", nameof(high)); + } + + if (low > high) + { + throw new ArgumentException($"{nameof(low)} should be greater than {nameof(high)}.", nameof(low)); + } + + Fallback = fallback; + Low = low; + High = high; + Port = AllocatePort(low, high, fallback); + } + + internal GeneratePortNumberConfig(ILogger logger, GeneratePortNumberMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) + : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType) + { + int low = GetOptionalParameterValue(generatedSymbolConfig, "low", ConvertJTokenToInt, LowPortDefault); + int high = GetOptionalParameterValue(generatedSymbolConfig, "high", ConvertJTokenToInt, HighPortDefault); + if (low < LowPortDefault) + { + logger.LogWarning(LocalizableStrings.GeneratePortNumberConfig_Warning_InvalidLowBound, low, LowPortDefault); + low = LowPortDefault; + } + + if (high > HighPortDefault) + { + logger.LogWarning(LocalizableStrings.GeneratePortNumberConfig_Warning_InvalidHighBound, high, HighPortDefault); + high = HighPortDefault; + } + + if (low > high) + { + logger.LogWarning(LocalizableStrings.GeneratePortNumberConfig_Warning_InvalidLowHighBound, low, high, LowPortDefault, HighPortDefault); + low = LowPortDefault; + high = HighPortDefault; + } + + int fallback = GetOptionalParameterValue(generatedSymbolConfig, "fallback", ConvertJTokenToInt, 0); + + Fallback = fallback; + Low = low; + High = high; + Port = AllocatePort(low, high, fallback); + } + + internal int Port { get; } + + internal int Low { get; } + + internal int High { get; } + + internal int Fallback { get; } + + private static ConcurrentDictionary UnavailablePorts { get; } = new(UnsafePorts.ToDictionary(p => p)); + + private static int AllocatePort(int low, int high, int fallback = 0) + { + int startPort = CryptoRandom.NextInt(low, high); + + for (int testPort = startPort; testPort <= high; testPort++) + { + if (TryAllocatePort(testPort)) + { + return testPort; + } + } + + for (int testPort = low; testPort < startPort; testPort++) + { + if (TryAllocatePort(testPort)) + { + return testPort; + } + } + return fallback; + } + + private static bool TryAllocatePort(int testPort) + { + Socket? testSocket = null; + if (!UnavailablePorts.TryAdd(testPort, testPort)) + { + return false; + } + try + { + if (Socket.OSSupportsIPv4) + { + testSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + } + else if (Socket.OSSupportsIPv6) + { + testSocket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); + } + + if (testSocket is null) + { + return false; + } + IPEndPoint endPoint = new(testSocket.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any, testPort); + testSocket.Bind(endPoint); + return true; + } + catch + { + return false; + } + finally + { + testSocket?.Dispose(); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GeneratePortNumberMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GeneratePortNumberMacro.cs new file mode 100644 index 000000000000..1802c86111a0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GeneratePortNumberMacro.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class GeneratePortNumberMacro : BaseNondeterministicGenSymMacro + { + public override Guid Id { get; } = new Guid("D49B3690-B1E5-410F-A260-E1D7E873D8B2"); + + public override string Type => "port"; + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, GeneratePortNumberConfig config) + { + vars[config.VariableName] = config.Port; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}'.", nameof(GeneratePortNumberMacro), config.VariableName, config.Port); + } + + public override void EvaluateDeterministically( + IEngineEnvironmentSettings environmentSettings, + IVariableCollection variables, + GeneratePortNumberConfig config) + { + variables[config.VariableName] = config.Low; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}' in deterministic mode.", nameof(GeneratePortNumberMacro), config.VariableName, config.Low); + } + + public override GeneratePortNumberConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) => new(environmentSettings.Host.Logger, this, deferredConfig); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GuidMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GuidMacro.cs new file mode 100644 index 000000000000..606c5478152a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GuidMacro.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class GuidMacro : BaseNondeterministicGenSymMacro + { + internal const string MacroType = "guid"; + private static readonly Guid DeterministicModeValue = new("12345678-1234-1234-1234-1234567890AB"); + + public override Guid Id { get; } = new Guid("10919008-4E13-4FA8-825C-3B4DA855578E"); + + public override string Type => MacroType; + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, GuidMacroConfig config) + => EvaluateInternal(Guid.NewGuid(), environmentSettings, vars, config); + + public override void EvaluateDeterministically( + IEngineEnvironmentSettings environmentSettings, + IVariableCollection variables, + GuidMacroConfig config) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: deterministic mode enabled.", nameof(GuidMacro)); + EvaluateInternal(DeterministicModeValue, environmentSettings, variables, config); + } + + public override GuidMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) => new(this, deferredConfig); + + private void EvaluateInternal(Guid g, IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, GuidMacroConfig config) + { + for (int i = 0; i < config.Format.Length; ++i) + { + bool isUpperCase = char.IsUpper(config.Format[i]); + string value = g.ToString(config.Format[i].ToString()); + value = isUpperCase ? value.ToUpperInvariant() : value.ToLowerInvariant(); + + // Not breaking any dependencies on exact param names and on the + // case insensitive matching of parameters (https://github.com/dotnet/templating/blob/7e14ef44/src/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/RunnableProjectGenerator.cs#L726) + // we need to introduce new parameters - with distinct naming for upper- and lower- casing replacements + string legacyName = config.VariableName + "-" + config.Format[i]; + string newName = config.VariableName + + (isUpperCase ? GuidMacroConfig.UpperCaseDenominator : GuidMacroConfig.LowerCaseDenominator) + + config.Format[i]; + + vars[legacyName] = value; + vars[newName] = value; + + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}'.", nameof(GuidMacro), legacyName, value); + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}'.", nameof(GuidMacro), newName, value); + } + + string defaultValue = char.IsUpper(config.DefaultFormat[0]) ? + g.ToString(config.DefaultFormat).ToUpperInvariant() : + g.ToString(config.DefaultFormat).ToLowerInvariant(); + + vars[config.VariableName] = defaultValue; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}'.", nameof(GuidMacro), config.VariableName, defaultValue); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GuidMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GuidMacroConfig.cs new file mode 100644 index 000000000000..932de2e2b28c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/GuidMacroConfig.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + //needs better validation if formats are correct + internal class GuidMacroConfig : BaseMacroConfig + { + internal const string DefaultFormats = "ndbpxNDPBX"; + internal const string UpperCaseDenominator = "-uc-"; + internal const string LowerCaseDenominator = "-lc-"; + private static readonly GuidMacro DefaultMacro = new(); + + internal GuidMacroConfig(string variableName, string? dataType, string? format, string? defaultFormat) + : base(DefaultMacro, variableName, dataType) + { + if (!string.IsNullOrEmpty(format)) + { + Format = format!; + } + if (!string.IsNullOrEmpty(defaultFormat)) + { + DefaultFormat = defaultFormat!; + } + } + + internal GuidMacroConfig(GuidMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) + : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType) + { + string? configuredFormat = GetOptionalParameterValue(generatedSymbolConfig, "format"); + if (!string.IsNullOrEmpty(configuredFormat)) + { + Format = configuredFormat!; + } + string? configuredDefaultFormat = GetOptionalParameterValue(generatedSymbolConfig, "defaultFormat"); + if (!string.IsNullOrEmpty(configuredDefaultFormat)) + { + DefaultFormat = configuredDefaultFormat!; + } + } + + internal string DefaultFormat { get; } = "D"; + + internal string Format { get; } = DefaultFormats; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/JoinMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/JoinMacro.cs new file mode 100644 index 000000000000..8d4a20712847 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/JoinMacro.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class JoinMacro : BaseGeneratedSymbolMacro + { + public override Guid Id { get; } = new Guid("6A2C58E5-8743-484B-AF3C-536770D31CEE"); + + public override string Type => "join"; + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, JoinMacroConfig config) + { + List values = new(); + foreach ((JoinMacroConfig.JoinType Type, string Value) symbol in config.Symbols) + { + switch (symbol.Type) + { + case JoinMacroConfig.JoinType.Ref: + object? working = null; + if (!vars.TryGetValue(symbol.Value, out working) || working == null) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was not found, using empty value instead'.", nameof(JoinMacro), symbol.Value); + values.Add(string.Empty); + } + else if (working is MultiValueParameter multiValue) + { + values.AddRange(multiValue.Values); + } + else + { + values.Add(working.ToString()); + } + break; + case JoinMacroConfig.JoinType.Const: + values.Add(symbol.Value); + break; + } + } + + string result = string.Join(config.Separator, values.Where(v => !config.RemoveEmptyValues || !string.IsNullOrEmpty(v))); + vars[config.VariableName] = result; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}'.", nameof(JoinMacro), config.VariableName, result); + } + + public override JoinMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) => new(this, deferredConfig); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/JoinMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/JoinMacroConfig.cs new file mode 100644 index 000000000000..485b2749c6e2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/JoinMacroConfig.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class JoinMacroConfig : BaseMacroConfig, IMacroConfigDependency + { + private const string SymbolsPropertyName = "symbols"; + private const string SymbolsTypePropertyName = "type"; + private const string SymbolsValuePropertyName = "value"; + + internal JoinMacroConfig(JoinMacro macro, string variableName, string? dataType, IReadOnlyList<(JoinType, string)> symbols, string? separator = "", bool removeEmptyValues = false) + : base(macro, variableName, dataType) + { + Symbols = symbols ?? throw new ArgumentNullException(nameof(symbols)); + Separator = separator ?? string.Empty; + RemoveEmptyValues = removeEmptyValues; + } + + internal JoinMacroConfig(JoinMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) + : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType) + { + Separator = GetOptionalParameterValue(generatedSymbolConfig, "separator") ?? string.Empty; + RemoveEmptyValues = GetOptionalParameterValue(generatedSymbolConfig, "removeEmptyValues", ConvertJTokenToBool); + + List<(JoinType Type, string Value)> symbolsList = new(); + + JsonArray jArray = GetMandatoryParameterArray(generatedSymbolConfig, SymbolsPropertyName); + + foreach (JsonNode? entry in jArray) + { + if (entry is not JsonObject jObj) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_ArrayShouldContainObjects, generatedSymbolConfig.VariableName, SymbolsPropertyName), generatedSymbolConfig.VariableName); + } + JoinType type = jObj.ToEnum(SymbolsTypePropertyName, JoinType.Const, ignoreCase: true); + string? value = jObj.ToString(SymbolsValuePropertyName) + ?? throw new TemplateAuthoringException( + string.Format( + LocalizableStrings.MacroConfig_Exception_MissingValueProperty, + generatedSymbolConfig.VariableName, + SymbolsPropertyName, + SymbolsValuePropertyName), + generatedSymbolConfig.VariableName); + if (type == JoinType.Ref && string.IsNullOrWhiteSpace(value)) + { + throw new TemplateAuthoringException( + string.Format( + LocalizableStrings.JoinMacroConfig_Exception_ValuePropertyIsEmpty, + generatedSymbolConfig.VariableName, + SymbolsPropertyName, + SymbolsValuePropertyName, + SymbolsTypePropertyName, + JoinType.Ref.ToString("g")), + generatedSymbolConfig.VariableName); + } + + symbolsList.Add((type, value)); + } + Symbols = symbolsList; + + } + + internal enum JoinType + { + Const, + Ref + } + + internal IReadOnlyList<(JoinType Type, string Value)> Symbols { get; } + + internal string Separator { get; private set; } + + internal bool RemoveEmptyValues { get; private set; } + + public void ResolveSymbolDependencies(IReadOnlyList symbols) + { + MacroDependenciesResolved = true; + Symbols.ForEach(s => PopulateMacroConfigDependencies(s.Value, symbols)); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/NowMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/NowMacro.cs new file mode 100644 index 000000000000..bdc419a1cad6 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/NowMacro.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class NowMacro : BaseNondeterministicGenSymMacro + { + public override Guid Id { get; } = new Guid("F2B423D7-3C23-4489-816A-41D8D2A98596"); + + public override string Type => "now"; + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variableCollection, NowMacroConfig config) + { + DateTime time = config.Utc ? DateTime.UtcNow : DateTime.Now; + string value = time.ToString(config.Format); + variableCollection[config.VariableName] = value; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}'.", nameof(NowMacro), config.VariableName, value); + } + + public override void EvaluateDeterministically( + IEngineEnvironmentSettings environmentSettings, + IVariableCollection variables, + NowMacroConfig config) + { + DateTime time = new DateTime(1900, 01, 01); + string value = time.ToString(config.Format); + variables[config.VariableName] = value; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}' in deterministic mode.", nameof(NowMacro), config.VariableName, value); + } + + public override NowMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) => new(this, deferredConfig); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/NowMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/NowMacroConfig.cs new file mode 100644 index 000000000000..d396aa7f379c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/NowMacroConfig.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class NowMacroConfig : BaseMacroConfig + { + internal NowMacroConfig(NowMacro macro, string variableName, string? format = null, bool utc = false) + : base(macro, variableName, "string") + { + Format = format; + Utc = utc; + } + + internal NowMacroConfig(NowMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) + : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType) + { + Format = GetOptionalParameterValue(generatedSymbolConfig, "format"); + Utc = GetOptionalParameterValue(generatedSymbolConfig, "utc", ConvertJTokenToBool); + } + + internal string? Format { get; private set; } + + internal bool Utc { get; private set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ProcessValueFormMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ProcessValueFormMacro.cs new file mode 100644 index 000000000000..e5df5a143214 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ProcessValueFormMacro.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class ProcessValueFormMacro : BaseMacro + { + internal const string MacroType = "processValueForm"; + + public override string Type => MacroType; + + public override Guid Id { get; } = new Guid("642E0443-F82B-4A4B-A797-CC1EB42221AE"); + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, ProcessValueFormMacroConfig config) + { + if (!vars.TryGetValue(config.SourceVariable, out object working)) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: Source variable '{sourceVar}' was not found, skipping processing for macro '{var}'.", nameof(ProcessValueFormMacro), config.SourceVariable, config.VariableName); + return; + } + if (working == null) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: The value of source variable '{sourceVar}' is null, skipping processing for macro '{var}'.", nameof(ProcessValueFormMacro), config.SourceVariable, config.VariableName); + return; + } + + string? result = config.Form.Process(working.ToString(), config.Forms); + if (result == null) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: Processing form {formName} on {val} resulted in null.", nameof(JoinMacro), config.Forms, working); + return; + } + vars[config.VariableName] = result; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}'.", nameof(JoinMacro), config.VariableName, result); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ProcessValueFormMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ProcessValueFormMacroConfig.cs new file mode 100644 index 000000000000..1e2027cfd463 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/ProcessValueFormMacroConfig.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ValueForms; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class ProcessValueFormMacroConfig : BaseMacroConfig, IMacroConfigDependency + { + private static readonly ProcessValueFormMacro DefaultMacro = new(); + + internal ProcessValueFormMacroConfig(string sourceSymbol, string symbolName, string? dataType, string valueForm, IReadOnlyDictionary forms) + : base(DefaultMacro, symbolName, dataType) + { + if (string.IsNullOrWhiteSpace(sourceSymbol)) + { + throw new ArgumentException($"'{nameof(sourceSymbol)}' cannot be null or whitespace.", nameof(sourceSymbol)); + } + + if (string.IsNullOrWhiteSpace(valueForm)) + { + throw new ArgumentException($"'{nameof(valueForm)}' cannot be null or whitespace.", nameof(valueForm)); + } + + SourceVariable = sourceSymbol; + Forms = forms; + Form = forms.TryGetValue(valueForm, out IValueForm? form) + ? form + : throw new InvalidOperationException($"''{nameof(symbolName)}': form '{nameof(valueForm)}' cannot be found."); + } + + internal string SourceVariable { get; } + + internal IReadOnlyDictionary Forms { get; } + + internal IValueForm Form { get; } + + public void ResolveSymbolDependencies(IReadOnlyList symbols) + { + MacroDependenciesResolved = true; + PopulateMacroConfigDependencies(SourceVariable, symbols); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RandomMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RandomMacro.cs new file mode 100644 index 000000000000..eca9ed7f1e33 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RandomMacro.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class RandomMacro : BaseNondeterministicGenSymMacro + { + public override Guid Id { get; } = new Guid("011E8DC1-8544-4360-9B40-65FD916049B7"); + + public override string Type => "random"; + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, RandomMacroConfig config) + { + int value = CryptoRandom.NextInt(config.Low, config.High); + vars[config.VariableName] = value; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}'.", nameof(RandomMacro), config.VariableName, value); + } + + public override void EvaluateDeterministically( + IEngineEnvironmentSettings environmentSettings, + IVariableCollection variables, + RandomMacroConfig config) + { + variables[config.VariableName] = config.Low; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Variable '{var}' was assigned to value '{value}' in deterministic mode.", nameof(RandomMacro), config.VariableName, config.Low); + } + + public override RandomMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) => new(this, deferredConfig); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RandomMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RandomMacroConfig.cs new file mode 100644 index 000000000000..e36597e311c9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RandomMacroConfig.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class RandomMacroConfig : BaseMacroConfig + { + internal RandomMacroConfig(RandomMacro macro, string variableName, string? dataType, int low, int? high) + : base(macro, variableName, dataType) + { + Low = low; + High = high ?? int.MaxValue; + } + + internal RandomMacroConfig(RandomMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) + : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType) + { + Low = GetMandatoryParameterValue(generatedSymbolConfig, "low", ConvertJTokenToInt); + High = GetOptionalParameterValue(generatedSymbolConfig, "high", ConvertJTokenToInt, int.MaxValue); + } + + internal int Low { get; private set; } + + internal int High { get; private set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMacro.cs new file mode 100644 index 000000000000..b4871399e62a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMacro.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class RegexMacro : BaseGeneratedSymbolMacro + { + public override Guid Id { get; } = new Guid("8A4D4937-E23F-426D-8398-3BDBD1873ADB"); + + public override string Type => "regex"; + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variableCollection, RegexMacroConfig config) + { + if (!variableCollection.TryGetValue(config.Source, out object working)) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: Source variable '{sourceVar}' was not found, skipping processing for macro '{var}'.", nameof(RegexMacro), config.Source, config.VariableName); + return; + } + if (working == null) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: The value of source variable '{sourceVar}' is null, skipping processing for macro '{var}'.", nameof(RegexMacro), config.Source, config.VariableName); + return; + } + string value = working.ToString(); + + foreach ((string Regex, string Replacement) stepInfo in config.Steps) + { + value = Regex.Replace(value, stepInfo.Regex, stepInfo.Replacement); + } + variableCollection[config.VariableName] = value; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Assigned variable '{var}' to '{value}'.", nameof(RegexMacro), config.VariableName, value); + } + + public override RegexMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) => new(this, deferredConfig); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMacroConfig.cs new file mode 100644 index 000000000000..5f8d27b14ec1 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMacroConfig.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class RegexMacroConfig : BaseMacroConfig, IMacroConfigDependency + { + private const string StepsPropertyName = "steps"; + private const string StepsRegexPropertyName = "regex"; + private const string StepsReplacementPropertyName = "replacement"; + + internal RegexMacroConfig(RegexMacro macro, string variableName, string? dataType, string sourceVariable, IReadOnlyList<(string, string)> steps) + : base(macro, variableName, dataType) + { + if (string.IsNullOrWhiteSpace(sourceVariable)) + { + throw new ArgumentException($"'{nameof(sourceVariable)}' cannot be null or whitespace.", nameof(sourceVariable)); + } + + Source = sourceVariable; + Steps = steps; + } + + internal RegexMacroConfig(RegexMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) + : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType) + { + Source = GetMandatoryParameterValue(generatedSymbolConfig, "source"); + + List<(string Type, string Value)> steps = new(); + JsonArray jArray = GetMandatoryParameterArray(generatedSymbolConfig, StepsPropertyName); + + foreach (JsonNode? entry in jArray) + { + if (entry is not JsonObject jObj) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_ArrayShouldContainObjects, generatedSymbolConfig.VariableName, StepsPropertyName), generatedSymbolConfig.VariableName); + } + string? regex = jObj.ToString(StepsRegexPropertyName); + string? replacement = jObj.ToString(StepsReplacementPropertyName); + + if (string.IsNullOrEmpty(regex)) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_MissingValueProperty, generatedSymbolConfig.VariableName, StepsPropertyName, StepsRegexPropertyName), generatedSymbolConfig.VariableName); + } + IsValidRegex(regex!, generatedSymbolConfig); + + if (replacement == null) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_MissingValueProperty, generatedSymbolConfig.VariableName, StepsPropertyName, StepsReplacementPropertyName), generatedSymbolConfig.VariableName); + } + + steps.Add((regex!, replacement)); + } + + Steps = steps; + } + + internal string Source { get; } + + internal IReadOnlyList<(string Regex, string Replacement)> Steps { get; private set; } + + public void ResolveSymbolDependencies(IReadOnlyList symbols) + { + MacroDependenciesResolved = true; + PopulateMacroConfigDependencies(Source, symbols); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMatchMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMatchMacro.cs new file mode 100644 index 000000000000..08ec4f6dbedf --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMatchMacro.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class RegexMatchMacro : BaseGeneratedSymbolMacro + { + public override Guid Id { get; } = new Guid("AA5957B0-07B1-4B68-847F-83713973E86F"); + + public override string Type => "regexMatch"; + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variableCollection, RegexMatchMacroConfig config) + { + if (!variableCollection.TryGetValue(config.Source, out object working)) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: Source variable '{sourceVar}' was not found, skipping processing for macro '{var}'.", nameof(RegexMatchMacro), config.Source, config.VariableName); + return; + } + if (working == null) + { + environmentSettings.Host.Logger.LogDebug("[{macro}]: The value of source variable '{sourceVar}' is null, skipping processing for macro '{var}'.", nameof(RegexMatchMacro), config.Source, config.VariableName); + return; + } + string value = working.ToString(); + bool result = Regex.IsMatch(value, config.Pattern); + variableCollection[config.VariableName] = result; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Assigned variable '{var}' to '{value}'.", nameof(RegexMatchMacro), config.VariableName, result); + } + + public override RegexMatchMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) => new(this, deferredConfig); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMatchMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMatchMacroConfig.cs new file mode 100644 index 000000000000..a1ba4818e12b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/RegexMatchMacroConfig.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class RegexMatchMacroConfig : BaseMacroConfig, IMacroConfigDependency + { + internal RegexMatchMacroConfig(RegexMatchMacro macro, string variableName, string? dataType, string sourceVariable, string pattern) + : base(macro, variableName, dataType ?? "bool") + { + if (string.IsNullOrWhiteSpace(sourceVariable)) + { + throw new ArgumentException($"'{nameof(sourceVariable)}' cannot be null or whitespace.", nameof(sourceVariable)); + } + + if (string.IsNullOrEmpty(pattern)) + { + throw new ArgumentException($"'{nameof(pattern)}' cannot be null or empty.", nameof(pattern)); + } + + Source = sourceVariable; + IsValidRegex(pattern); + Pattern = pattern; + } + + internal RegexMatchMacroConfig(RegexMatchMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) + : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType) + { + Source = GetMandatoryParameterValue(generatedSymbolConfig, "source"); + Pattern = GetMandatoryParameterValue(generatedSymbolConfig, "pattern"); + IsValidRegex(Pattern!, generatedSymbolConfig); + } + + internal string Source { get; } + + internal string Pattern { get; } + + public void ResolveSymbolDependencies(IReadOnlyList symbols) + { + MacroDependenciesResolved = true; + PopulateMacroConfigDependencies(Source, symbols); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/SwitchMacro.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/SwitchMacro.cs new file mode 100644 index 000000000000..02ee72e38ba0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/SwitchMacro.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class SwitchMacro : BaseGeneratedSymbolMacro + { + public override Guid Id { get; } = new Guid("B57D64E0-9B4F-4ABE-9366-711170FD5294"); + + public override string Type => "switch"; + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variableCollection, SwitchMacroConfig config) + { + string result = string.Empty; // default if no condition assigns a value + + foreach ((string? condition, string value) in config.Cases) + { + if (string.IsNullOrEmpty(condition)) + { + // no condition, this is the default. + result = value; + break; + } + + if (config.Evaluator(environmentSettings.Host.Logger, condition!, variableCollection)) + { + result = value; + break; + } + } + variableCollection[config.VariableName] = result; + environmentSettings.Host.Logger.LogDebug("[{macro}]: Assigned variable '{var}' to '{value}'.", nameof(SwitchMacro), config.VariableName, result); + } + + public override SwitchMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) => new(this, deferredConfig); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/SwitchMacroConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/SwitchMacroConfig.cs new file mode 100644 index 000000000000..8e82ffe796b5 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Macros/SwitchMacroConfig.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros +{ + internal class SwitchMacroConfig : BaseMacroConfig, IMacroConfigDependency + { + private const string CasesPropertyName = "cases"; + private const string CasesConditionPropertyName = "condition"; + private const string CasesValuePropertyName = "value"; + private const EvaluatorType DefaultEvaluator = EvaluatorType.CPP2; + + internal SwitchMacroConfig(SwitchMacro macro, string variableName, string evaluator, string dataType, IReadOnlyList<(string?, string)> cases) + : base(macro, variableName, dataType) + { + Evaluator = EvaluatorSelector.SelectStringEvaluator(EvaluatorSelector.ParseEvaluatorName(evaluator, DefaultEvaluator)); + Cases = cases; + } + + internal SwitchMacroConfig(SwitchMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) + : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType) + { + string? evaluator = GetOptionalParameterValue(generatedSymbolConfig, "evaluator"); + if (!string.IsNullOrWhiteSpace(evaluator)) + { + Evaluator = EvaluatorSelector.SelectStringEvaluator(EvaluatorSelector.ParseEvaluatorName(evaluator, DefaultEvaluator)); + } + List<(string? Condition, string Value)> cases = new(); + JsonArray jArray = GetMandatoryParameterArray(generatedSymbolConfig, CasesPropertyName); + + foreach (JsonNode? entry in jArray) + { + if (entry is not JsonObject jObj) + { + throw new TemplateAuthoringException(string.Format(LocalizableStrings.MacroConfig_Exception_ArrayShouldContainObjects, generatedSymbolConfig.VariableName, CasesPropertyName), generatedSymbolConfig.VariableName); + } + string? condition = jObj.ToString(CasesConditionPropertyName); + string? value = jObj.ToString(CasesValuePropertyName) + ?? throw new TemplateAuthoringException( + string.Format( + LocalizableStrings.MacroConfig_Exception_MissingValueProperty, + generatedSymbolConfig.VariableName, + CasesPropertyName, + CasesValuePropertyName), + generatedSymbolConfig.VariableName); + cases.Add((condition, value)); + } + Cases = cases; + } + + internal ConditionStringEvaluator Evaluator { get; private set; } = EvaluatorSelector.SelectStringEvaluator(DefaultEvaluator); + + internal IReadOnlyList<(string? Condition, string Value)> Cases { get; } + + public void ResolveSymbolDependencies(IReadOnlyList symbols) + { + MacroDependenciesResolved = true; + foreach ((string? condition, string _) in Cases) + { + if (!string.IsNullOrEmpty(condition)) + { + PopulateMacroConfigDependencies(condition!, symbols); + } + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.csproj b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.csproj new file mode 100644 index 000000000000..537f201c6c3d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.csproj @@ -0,0 +1,45 @@ + + + + $(NetMinimum);$(NetCurrent);netstandard2.0;$(NetFrameworkMinimum) + An extension for Template Engine that allows projects that still run to be used as templates + true + true + + true + + + + + + annotations + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/BalancedNestingConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/BalancedNestingConfig.cs new file mode 100644 index 000000000000..d64a25a83d21 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/BalancedNestingConfig.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig +{ + internal class BalancedNestingConfig : IOperationConfig + { + public string Key => BalancedNesting.OperationName; + + public Guid Id => new Guid("3147965A-08E5-4523-B869-02C8E9A8AAA1"); + + public IEnumerable ConfigureFromJson(string configuration, IDirectory templateRoot) + { + JsonObject rawConfiguration = JExtensions.ParseJsonObject(configuration); + string? startToken = rawConfiguration.ToString("startToken"); + string? realEndToken = rawConfiguration.ToString("realEndToken"); + string? pseudoEndToken = rawConfiguration.ToString("pseudoEndToken"); + string? id = rawConfiguration.ToString("id"); + string? resetFlag = rawConfiguration.ToString("resetFlag"); + bool onByDefault = rawConfiguration.ToBool("onByDefault"); + + yield return new BalancedNesting(startToken.TokenConfig(), realEndToken.TokenConfig(), pseudoEndToken.TokenConfig(), id, resetFlag, onByDefault); + } + + internal static JsonObject CreateConfiguration(string startToken, string realEndToken, string pseudoEndToken, string id, string resetFlag) + { + JsonObject config = new JsonObject + { + ["startToken"] = startToken, + ["realEndToken"] = realEndToken, + ["pseudoEndToken"] = pseudoEndToken, + ["id"] = id, + ["resetFlag"] = resetFlag + }; + + return config; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalBlockCommentConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalBlockCommentConfig.cs new file mode 100644 index 000000000000..31e4f3d2dda4 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalBlockCommentConfig.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig +{ + internal static class ConditionalBlockCommentConfig + { + internal static List ConfigureFromJObject(JsonObject rawConfiguration) + { + string? startToken = rawConfiguration.ToString("startToken"); + string? endToken = rawConfiguration.ToString("endToken"); + + if (string.IsNullOrWhiteSpace(startToken)) + { + throw new TemplateAuthoringException($"Template authoring error. StartToken must be defined", "StartToken"); + } + else if (string.IsNullOrWhiteSpace(endToken)) + { + throw new TemplateAuthoringException($"Template authoring error. EndToken must be defined", "EndToken"); + } + + string? pseudoEndToken = rawConfiguration.ToString("pseudoEndToken"); + + ConditionalKeywords keywords = ConditionalKeywords.FromJObject(rawConfiguration); + ConditionalOperationOptions options = ConditionalOperationOptions.FromJObject(rawConfiguration); + + if (string.IsNullOrWhiteSpace(pseudoEndToken)) + { + return GenerateConditionalSetup(startToken, endToken, keywords, options); + } + else + { + return GenerateConditionalSetup(startToken, endToken, pseudoEndToken, keywords, options); + } + } + + internal static List GenerateConditionalSetup(string startToken, string endToken) + { + return GenerateConditionalSetup(startToken, endToken, new ConditionalKeywords(), new ConditionalOperationOptions()); + } + + internal static List GenerateConditionalSetup(string? startToken, string? endToken, ConditionalKeywords keywords, ConditionalOperationOptions options) + { + string? pseudoEndComment; + + if (endToken is null || endToken.Length < 2) + { + // end comment must be at least two characters to have a programmatically determined pseudo-comment + pseudoEndComment = null; + } + else + { + // add a space just before the final character of the end comment + pseudoEndComment = endToken.Substring(0, endToken.Length - 1) + " " + endToken.Substring(endToken.Length - 1); + } + + return GenerateConditionalSetup(startToken, endToken, pseudoEndComment, keywords, options); + } + + internal static List GenerateConditionalSetup(string? startToken, string? endToken, string? pseudoEndToken) + { + return GenerateConditionalSetup(startToken, endToken, pseudoEndToken, new ConditionalKeywords(), new ConditionalOperationOptions()); + } + + internal static List GenerateConditionalSetup(string? startToken, string? endToken, string? pseudoEndToken, ConditionalKeywords keywords, ConditionalOperationOptions options) + { + ConditionEvaluator evaluator = EvaluatorSelector.Select(options.EvaluatorType); + + List endIfTokens = new List(); + foreach (string endIfKeyword in keywords.EndIfKeywords) + { + endIfTokens.Add($"{keywords.KeywordPrefix}{endIfKeyword}".TokenConfig()); + endIfTokens.Add($"{startToken}{keywords.KeywordPrefix}{endIfKeyword}".TokenConfig()); + } + + List actionableIfTokens = new List(); + foreach (string ifKeyword in keywords.IfKeywords) + { + actionableIfTokens.Add($"{startToken}{keywords.KeywordPrefix}{ifKeyword}".TokenConfig()); + } + + List actionableElseTokens = new List(); + foreach (string elseKeyword in keywords.ElseKeywords) + { + actionableElseTokens.Add($"{keywords.KeywordPrefix}{elseKeyword}".TokenConfig()); + actionableElseTokens.Add($"{startToken}{keywords.KeywordPrefix}{elseKeyword}".TokenConfig()); + } + + List actionableElseIfTokens = new List(); + foreach (string elseIfKeyword in keywords.ElseIfKeywords) + { + actionableElseIfTokens.Add($"{keywords.KeywordPrefix}{elseIfKeyword}".TokenConfig()); + actionableElseIfTokens.Add($"{startToken}{keywords.KeywordPrefix}{elseIfKeyword}".TokenConfig()); + } + + ConditionalTokens tokens = new ConditionalTokens + { + EndIfTokens = endIfTokens, + ActionableIfTokens = actionableIfTokens, + ActionableElseTokens = actionableElseTokens, + ActionableElseIfTokens = actionableElseIfTokens + }; + + if (!string.IsNullOrWhiteSpace(pseudoEndToken)) + { + Guid operationIdGuid = default; + string commentFixOperationId = $"Fix pseudo tokens ({pseudoEndToken} {operationIdGuid})"; + string commentFixResetId = $"Reset pseudo token fixer ({pseudoEndToken} {operationIdGuid})"; + + tokens.ActionableOperations = new[] { commentFixOperationId, commentFixResetId }; + + IOperationProvider balancedComments = new BalancedNesting(startToken.TokenConfig(), endToken.TokenConfig(), pseudoEndToken.TokenConfig(), commentFixOperationId, commentFixResetId, options.OnByDefault); + IOperationProvider conditional = new Conditional(tokens, options.WholeLine, options.TrimWhitespace, evaluator, options.Id, options.OnByDefault); + + return new List() + { + conditional, + balancedComments + }; + } + else + { + IOperationProvider conditional = new Conditional(tokens, options.WholeLine, options.TrimWhitespace, evaluator, options.Id, options.OnByDefault); + return new List() + { + conditional + }; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalConfig.cs new file mode 100644 index 000000000000..193519a829ac --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalConfig.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig +{ + internal class ConditionalConfig : IOperationConfig + { + public string Key => Conditional.OperationName; + + public Guid Id => new Guid("3E8BCBF0-D631-45BA-A12D-FBF1DE03AA38"); + + public IEnumerable ConfigureFromJson(string configuration, IDirectory templateRoot) + { + JsonObject rawConfiguration = JExtensions.ParseJsonObject(configuration); + string? commentStyle = rawConfiguration.ToString("style"); + IEnumerable operations = string.IsNullOrEmpty(commentStyle) || string.Equals(commentStyle, "custom", StringComparison.OrdinalIgnoreCase) + ? ConditionalCustomConfig.ConfigureFromJObject(rawConfiguration) + : string.Equals(commentStyle, "line", StringComparison.OrdinalIgnoreCase) + ? ConditionalLineCommentConfig.ConfigureFromJObject(rawConfiguration) + : string.Equals(commentStyle, "block", StringComparison.OrdinalIgnoreCase) + ? (IEnumerable)ConditionalBlockCommentConfig.ConfigureFromJObject(rawConfiguration) + : throw new TemplateAuthoringException($"Template authoring error. Invalid comment style [{commentStyle}].", "style"); + foreach (IOperationProvider op in operations) + { + yield return op; + } + } + + internal static IReadOnlyList ConditionalSetup(ConditionalType style, EvaluatorType evaluatorType, bool wholeLine, bool trimWhiteSpace, string? id) + { + List setup; + + switch (style) + { + case ConditionalType.MSBuild: + setup = new List(); + setup.AddRange(MSBuildConditionalSetup(evaluatorType, wholeLine, trimWhiteSpace, "msbuild-conditional")); + setup.AddRange(ConditionalBlockCommentConfig.GenerateConditionalSetup("")); + break; + case ConditionalType.Xml: + setup = ConditionalBlockCommentConfig.GenerateConditionalSetup(""); + break; + case ConditionalType.Razor: + setup = ConditionalBlockCommentConfig.GenerateConditionalSetup("@*", "*@"); + break; + case ConditionalType.CLineComments: + setup = ConditionalLineCommentConfig.GenerateConditionalSetup("//"); + break; + case ConditionalType.CNoComments: + setup = CStyleNoCommentsConditionalSetup(evaluatorType, wholeLine, trimWhiteSpace, id); + break; + case ConditionalType.CBlockComments: + setup = ConditionalBlockCommentConfig.GenerateConditionalSetup("/*", "*/"); + break; + case ConditionalType.HashSignLineComment: + // Most line comment conditional tags use: + // But for this one, the '#' comment symbol is all that's needed, so it uses an empty keyword prefix. + // So we end up with regular conditionals such as '#if', '#else' + // and actions such as '##if' + ConditionalKeywords keywords = new ConditionalKeywords() + { + KeywordPrefix = string.Empty + }; + setup = ConditionalLineCommentConfig.GenerateConditionalSetup("#", keywords, new ConditionalOperationOptions()); + break; + case ConditionalType.RemLineComment: + setup = ConditionalLineCommentConfig.GenerateConditionalSetup("rem "); + break; + case ConditionalType.HamlLineComment: + setup = ConditionalLineCommentConfig.GenerateConditionalSetup("-#"); + break; + case ConditionalType.JsxBlockComment: + setup = ConditionalBlockCommentConfig.GenerateConditionalSetup("{/*", "*/}"); + break; + case ConditionalType.VB: + setup = ConditionalLineCommentConfig.GenerateConditionalSetup( + string.Empty, + new ConditionalKeywords + { + IfKeywords = new[] { "If" }, + ElseIfKeywords = new[] { "ElseIf" }, + ElseKeywords = new[] { "Else" }, + EndIfKeywords = new[] { "End If" }, + KeywordPrefix = "#" + }, + new ConditionalOperationOptions + { + EvaluatorType = EvaluatorType.VB, + WholeLine = true + }); + break; + default: + throw new Exception($"Unrecognized conditional type {style}"); + } + + return setup; + } + + // Nice to have: Generalize this type of setup similarly to Line, Block, & Custom + internal static List MSBuildConditionalSetup(EvaluatorType evaluatorType, bool wholeLine, bool trimWhiteSpace, string id) + { + ConditionEvaluator evaluator = EvaluatorSelector.Select(evaluatorType); + IOperationProvider conditional = new InlineMarkupConditional( + new MarkupTokens( + "<".TokenConfig(), + "".TokenConfig(), + "/>".TokenConfig(), + "Condition=\"".TokenConfig(), + "\"".TokenConfig(), + "".TokenConfig()), + wholeLine, + trimWhiteSpace, + evaluator, + "$({0})", + id, + true); + + return new List() + { + conditional + }; + } + + // Nice to have: Generalize this type of setup similarly to Line, Block, & Custom + internal static List CStyleNoCommentsConditionalSetup(EvaluatorType evaluatorType, bool wholeLine, bool trimWhiteSpace, string? id) + { + ConditionalKeywords defaultKeywords = new ConditionalKeywords(); + + List ifTokens = new List(); + List elseifTokens = new List(); + List elseTokens = new List(); + List endifTokens = new List(); + + foreach (string ifKeyword in defaultKeywords.IfKeywords) + { + ifTokens.Add($"{defaultKeywords.KeywordPrefix}{ifKeyword}".TokenConfig()); + } + + foreach (string elseifKeyword in defaultKeywords.ElseIfKeywords) + { + elseifTokens.Add($"{defaultKeywords.KeywordPrefix}{elseifKeyword}".TokenConfig()); + } + + foreach (string elseKeyword in defaultKeywords.ElseKeywords) + { + elseTokens.Add($"{defaultKeywords.KeywordPrefix}{elseKeyword}".TokenConfig()); + } + + foreach (string endifKeyword in defaultKeywords.EndIfKeywords) + { + endifTokens.Add($"{defaultKeywords.KeywordPrefix}{endifKeyword}".TokenConfig()); + } + + ConditionalTokens tokens = new ConditionalTokens + { + IfTokens = ifTokens, + ElseTokens = elseTokens, + ElseIfTokens = elseifTokens, + EndIfTokens = endifTokens + }; + + ConditionEvaluator evaluator = EvaluatorSelector.Select(evaluatorType); + IOperationProvider conditional = new Conditional(tokens, wholeLine, trimWhiteSpace, evaluator, id, true); + + return new List() + { + conditional + }; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalCustomConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalCustomConfig.cs new file mode 100644 index 000000000000..9926aa084777 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalCustomConfig.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig +{ + internal static class ConditionalCustomConfig + { + internal static List ConfigureFromJObject(JsonObject rawConfiguration) + { + IReadOnlyList ifToken = rawConfiguration.ArrayAsStrings("if"); + IReadOnlyList elseToken = rawConfiguration.ArrayAsStrings("else"); + IReadOnlyList elseIfToken = rawConfiguration.ArrayAsStrings("elseif"); + IReadOnlyList actionableIfToken = rawConfiguration.ArrayAsStrings("actionableIf"); + IReadOnlyList actionableElseToken = rawConfiguration.ArrayAsStrings("actionableElse"); + IReadOnlyList actionableElseIfToken = rawConfiguration.ArrayAsStrings("actionableElseif"); + IReadOnlyList actionsToken = rawConfiguration.ArrayAsStrings("actions"); + IReadOnlyList endIfToken = rawConfiguration.ArrayAsStrings("endif"); + string? id = rawConfiguration.ToString("id"); + bool trim = rawConfiguration.ToBool("trim"); + bool wholeLine = rawConfiguration.ToBool("wholeLine"); + bool onByDefault = rawConfiguration.ToBool("onByDefault"); + + string? evaluatorName = rawConfiguration.ToString("evaluator"); + ConditionEvaluator evaluator = EvaluatorSelector.Select(EvaluatorSelector.ParseEvaluatorName(evaluatorName)); + + ConditionalTokens tokenVariants = new ConditionalTokens + { + IfTokens = ifToken.TokenConfigs(), + ElseTokens = elseToken.TokenConfigs(), + ElseIfTokens = elseIfToken.TokenConfigs(), + EndIfTokens = endIfToken.TokenConfigs(), + ActionableElseIfTokens = actionableElseIfToken.TokenConfigs(), + ActionableElseTokens = actionableElseToken.TokenConfigs(), + ActionableIfTokens = actionableIfToken.TokenConfigs(), + ActionableOperations = actionsToken + }; + + return new List() + { + new Conditional(tokenVariants, wholeLine, trim, evaluator, id, onByDefault) + }; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalKeywords.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalKeywords.cs new file mode 100644 index 000000000000..cb0ba0671296 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalKeywords.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig +{ + internal class ConditionalKeywords + { + private const string DefaultPrefix = "#"; + private static readonly IReadOnlyList DefaultIfKeywords = new[] { "if" }; + private static readonly IReadOnlyList DefaultElseIfKeywords = new[] { "elseif", "elif" }; + private static readonly IReadOnlyList DefaultElseKeywords = new[] { "else" }; + private static readonly IReadOnlyList DefaultEndIfKeywords = new[] { "endif" }; + + internal ConditionalKeywords() + { + KeywordPrefix = DefaultPrefix; + IfKeywords = DefaultIfKeywords; + ElseIfKeywords = DefaultElseIfKeywords; + ElseKeywords = DefaultElseKeywords; + EndIfKeywords = DefaultEndIfKeywords; + } + + internal string KeywordPrefix { get; set; } + + internal IReadOnlyList IfKeywords { get; set; } + + internal IReadOnlyList ElseIfKeywords { get; set; } + + internal IReadOnlyList ElseKeywords { get; set; } + + internal IReadOnlyList EndIfKeywords { get; set; } + + // TODO: Allow the rawConfiguration elements to be either strings (as-is) or arrays of strings. + // The code that consumes instances of this class is already setup to deal with multiple forms of each keyword type. + internal static ConditionalKeywords FromJObject(JsonObject rawConfiguration) + { + ConditionalKeywords keywords = new ConditionalKeywords(); + string? ifKeyword = rawConfiguration.ToString("ifKeyword"); + if (!string.IsNullOrWhiteSpace(ifKeyword)) + { + keywords.IfKeywords = new[] { ifKeyword! }; + } + + string? elseIfKeyword = rawConfiguration.ToString("elseIfKeyword"); + if (!string.IsNullOrWhiteSpace(elseIfKeyword)) + { + keywords.ElseIfKeywords = new[] { elseIfKeyword! }; + } + + string? elseKeyword = rawConfiguration.ToString("elseKeyword"); + if (!string.IsNullOrWhiteSpace(elseKeyword)) + { + keywords.ElseKeywords = new[] { elseKeyword! }; + } + + string? endIfKeyword = rawConfiguration.ToString("endIfKeyword"); + if (!string.IsNullOrWhiteSpace(endIfKeyword)) + { + keywords.EndIfKeywords = new[] { endIfKeyword! }; + } + + string? prefixString = rawConfiguration.ToString("keywordPrefix"); + if (prefixString != null) + { + // Empty string is a valid value for keywordPrefix, null is not. + // If the "keywordPrefix" key isn't present in the config, the value will be null and not used. + keywords.KeywordPrefix = prefixString; + } + + return keywords; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalLineCommentConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalLineCommentConfig.cs new file mode 100644 index 000000000000..88ba8aedd886 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalLineCommentConfig.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig +{ + internal static class ConditionalLineCommentConfig + { + internal static List ConfigureFromJObject(JsonObject rawConfiguration) + { + string? token = rawConfiguration.ToString("token"); + + if (string.IsNullOrWhiteSpace(token)) + { + // this is the only required data, all the rest is optional + throw new TemplateAuthoringException("Template authoring error. Token must be defined", "token"); + } + + ConditionalKeywords keywords = ConditionalKeywords.FromJObject(rawConfiguration); + ConditionalOperationOptions options = ConditionalOperationOptions.FromJObject(rawConfiguration); + + return GenerateConditionalSetup(token, keywords, options); + } + + internal static List GenerateConditionalSetup(string token) + { + return GenerateConditionalSetup(token, new ConditionalKeywords(), new ConditionalOperationOptions()); + } + + internal static List GenerateConditionalSetup(string? token, ConditionalKeywords keywords, ConditionalOperationOptions options) + { + string uncommentOperationId = $"Uncomment (line): {token} -> ()"; + string reduceCommentOperationId = $"Reduce comment (line): ({token}{token}) -> ({token})"; + IOperationProvider uncomment = new Replacement(token.TokenConfig(), string.Empty, uncommentOperationId, options.OnByDefault); + IOperationProvider reduceComment = new Replacement($"{token}{token}".TokenConfig(), token, reduceCommentOperationId, options.OnByDefault); + + List ifTokens = new List(); + List actionableIfTokens = new List(); + foreach (string ifKeyword in keywords.IfKeywords) + { + ifTokens.Add($"{token}{keywords.KeywordPrefix}{ifKeyword}".TokenConfig()); + actionableIfTokens.Add($"{token}{token}{keywords.KeywordPrefix}{ifKeyword}".TokenConfig()); + } + + List elseTokens = new List(); + List actionableElseTokens = new List(); + foreach (string elseKeyword in keywords.ElseKeywords) + { + elseTokens.Add($"{token}{keywords.KeywordPrefix}{elseKeyword}".TokenConfig()); + actionableElseTokens.Add($"{token}{token}{keywords.KeywordPrefix}{elseKeyword}".TokenConfig()); + } + + List elseIfTokens = new List(); + List actionalElseIfTokens = new List(); + foreach (string elseIfKeyword in keywords.ElseIfKeywords) + { + elseIfTokens.Add($"{token}{keywords.KeywordPrefix}{elseIfKeyword}".TokenConfig()); + actionalElseIfTokens.Add($"{token}{token}{keywords.KeywordPrefix}{elseIfKeyword}".TokenConfig()); + } + + List endIfTokens = new List(); + foreach (string endIfKeyword in keywords.EndIfKeywords) + { + endIfTokens.Add($"{token}{keywords.KeywordPrefix}{endIfKeyword}".TokenConfig()); + endIfTokens.Add($"{token}{token}{keywords.KeywordPrefix}{endIfKeyword}".TokenConfig()); + } + + ConditionalTokens conditionalTokens = new ConditionalTokens + { + IfTokens = ifTokens, + ElseTokens = elseTokens, + ElseIfTokens = elseIfTokens, + EndIfTokens = endIfTokens, + ActionableIfTokens = actionableIfTokens, + ActionableElseTokens = actionableElseTokens, + ActionableElseIfTokens = actionalElseIfTokens, + ActionableOperations = new[] { uncommentOperationId, reduceCommentOperationId } + }; + + ConditionEvaluator evaluator = EvaluatorSelector.Select(options.EvaluatorType); + IOperationProvider conditional = new Conditional(conditionalTokens, options.WholeLine, options.TrimWhitespace, evaluator, options.Id, options.OnByDefault); + + return new List() + { + conditional, + reduceComment, + uncomment + }; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalOperationOptions.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalOperationOptions.cs new file mode 100644 index 000000000000..0033424d020b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalOperationOptions.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig +{ + internal class ConditionalOperationOptions + { + private const EvaluatorType DefaultEvaluatorType = EvaluatorType.CPP; + private const bool DefaultWholeLine = true; + private const bool DefaultTrimWhitespace = true; + + internal ConditionalOperationOptions() + { + EvaluatorType = DefaultEvaluatorType; + WholeLine = DefaultWholeLine; + TrimWhitespace = DefaultTrimWhitespace; + } + + internal EvaluatorType EvaluatorType { get; set; } + + internal bool WholeLine { get; set; } + + internal bool TrimWhitespace { get; set; } + + internal string? Id { get; set; } + + internal bool OnByDefault { get; set; } + + internal static ConditionalOperationOptions FromJObject(JsonObject rawConfiguration) + { + ConditionalOperationOptions options = new ConditionalOperationOptions(); + + string? evaluatorType = rawConfiguration.ToString("evaluator"); + options.EvaluatorType = EvaluatorSelector.ParseEvaluatorName(evaluatorType, DefaultEvaluatorType); + options.TrimWhitespace = rawConfiguration.ToBool("trim", DefaultTrimWhitespace); + options.WholeLine = rawConfiguration.ToBool("wholeLine", DefaultWholeLine); + options.OnByDefault = rawConfiguration.ToBool("onByDefault"); + + string? id = rawConfiguration.ToString("id"); + if (!string.IsNullOrWhiteSpace(id)) + { + options.Id = id!; + } + + return options; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalType.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalType.cs new file mode 100644 index 000000000000..e7031ae292bd --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/ConditionalType.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig +{ + // The commonly used conditional types. If more get added to ConditionalConfig.cs, they should be added here too. + internal enum ConditionalType + { + None, + Xml, + Razor, + CNoComments, + CLineComments, + CBlockComments, + HashSignLineComment, + RemLineComment, + MSBuild, + HamlLineComment, + JsxBlockComment, + VB + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/FlagsConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/FlagsConfig.cs new file mode 100644 index 000000000000..f741de589d7e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/FlagsConfig.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig +{ + internal class FlagsConfig : IOperationConfig + { + private const string NoEmitSuffix = ":noEmit"; + private const string FlagConditionalSuffix = ":cnd"; + private const string FlagReplacementSuffix = ":replacements"; + private const string FlagExpandVariablesSuffix = ":vars"; + private const string FlagIncludeSuffix = ":include"; + private const string FlagFlagsSuffix = ":flags"; + + public string Key => SetFlag.OperationName; + + public Guid Id => new Guid("A1E27A4B-9608-47F1-B3B8-F70DF62DC521"); + + public IEnumerable ConfigureFromJson(string configuration, IDirectory templateRoot) + { + JsonObject rawConfiguration = JExtensions.ParseJsonObject(configuration); + string? flag = rawConfiguration.ToString("name"); + string on = rawConfiguration.ToString("on") ?? string.Empty; + string off = rawConfiguration.ToString("off") ?? string.Empty; + string onNoEmit = rawConfiguration.ToString("onNoEmit") ?? string.Empty; + string offNoEmit = rawConfiguration.ToString("offNoEmit") ?? string.Empty; + string? defaultStr = rawConfiguration.ToString("default"); + string? id = rawConfiguration.ToString("id"); + bool onByDefault = rawConfiguration.ToBool("onByDefault"); + bool? @default = null; + + if (defaultStr != null) + { + @default = bool.Parse(defaultStr); + } + + yield return new SetFlag(flag, on.TokenConfig(), off.TokenConfig(), onNoEmit.TokenConfig(), offNoEmit.TokenConfig(), id, onByDefault, @default); + } + + // Returns a default flags operations setup for the given switchPrefix + internal static IReadOnlyList FlagsDefaultSetup(string switchPrefix) + { + List flagOperations = new List(); + string on = string.Format("{0}+{1}", switchPrefix, FlagConditionalSuffix); + string off = string.Format("{0}-{1}", switchPrefix, FlagConditionalSuffix); + flagOperations.Add(new SetFlag(Conditional.OperationName, on.TokenConfig(), off.TokenConfig(), (on + NoEmitSuffix).TokenConfig(), (off + NoEmitSuffix).TokenConfig(), null, true)); + + on = string.Format("{0}+{1}", switchPrefix, FlagReplacementSuffix); + off = string.Format("{0}-{1}", switchPrefix, FlagReplacementSuffix); + flagOperations.Add(new SetFlag(Replacement.OperationName, on.TokenConfig(), off.TokenConfig(), (on + NoEmitSuffix).TokenConfig(), (off + NoEmitSuffix).TokenConfig(), null, true)); + + on = string.Format("{0}+{1}", switchPrefix, FlagExpandVariablesSuffix); + off = string.Format("{0}-{1}", switchPrefix, FlagExpandVariablesSuffix); + flagOperations.Add(new SetFlag(ExpandVariables.OperationName, on.TokenConfig(), off.TokenConfig(), (on + NoEmitSuffix).TokenConfig(), (off + NoEmitSuffix).TokenConfig(), null, true)); + + on = string.Format("{0}+{1}", switchPrefix, FlagIncludeSuffix); + off = string.Format("{0}-{1}", switchPrefix, FlagIncludeSuffix); + flagOperations.Add(new SetFlag(Include.OperationName, on.TokenConfig(), off.TokenConfig(), (on + NoEmitSuffix).TokenConfig(), (off + NoEmitSuffix).TokenConfig(), null, true)); + + // no off for the flag-flag + on = string.Format("{0}+{1}", switchPrefix, FlagFlagsSuffix); + flagOperations.Add(new SetFlag(SetFlag.OperationName, on.TokenConfig(), string.Empty.TokenConfig(), (on + NoEmitSuffix).TokenConfig(), string.Empty.TokenConfig(), null, true)); + + return flagOperations; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/IncludeConfig.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/IncludeConfig.cs new file mode 100644 index 000000000000..1a120f9bea45 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/IncludeConfig.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig +{ + internal class IncludeConfig : IOperationConfig + { + public string Key => Include.OperationName; + + public Guid Id => new Guid("3FAE1942-7257-4247-B44D-2DDE07CB4A4A"); + + public IEnumerable ConfigureFromJson(string configuration, IDirectory templateRoot) + { + JsonObject rawConfiguration = JExtensions.ParseJsonObject(configuration); + string? startToken = rawConfiguration.ToString("start"); + string? endToken = rawConfiguration.ToString("end"); + string? id = rawConfiguration.ToString("id"); + bool onByDefault = rawConfiguration.ToBool("onByDefault"); + + yield return new Include(startToken.TokenConfig(), endToken.TokenConfig(), x => templateRoot.FileInfo(x)?.OpenRead(), id, onByDefault); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/OperationConfigDefault.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/OperationConfigDefault.cs new file mode 100644 index 000000000000..1f76c6891137 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects/OperationConfig/OperationConfigDefault.cs @@ -0,0 +1,144 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig +{ + internal class OperationConfigDefault + { + internal OperationConfigDefault(string glob, string flagPrefix, EvaluatorType evaluator, ConditionalType type) + { + Evaluator = evaluator; + Glob = glob; + FlagPrefix = flagPrefix; + ConditionalStyle = type; + } + + /// + /// Gets default operation config. + /// + internal static OperationConfigDefault Default { get; } = new(glob: string.Empty, flagPrefix: string.Empty, evaluator: EvaluatorType.CPP, type: ConditionalType.None); + + /// + /// Gets default global operation config. + /// + internal static OperationConfigDefault DefaultGlobalConfig { get; } = new(glob: string.Empty, flagPrefix: "//", evaluator: EvaluatorType.CPP, type: ConditionalType.CLineComments); + + /// + /// Gets default special operation config. + /// + internal static IReadOnlyList DefaultSpecialConfig { get; } = new[] + { + new OperationConfigDefault("**/*.js", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.es", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.es6", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.ts", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.json", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.jsonld", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.hjson", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.json5", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.geojson", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.topojson", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.bowerrc", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.npmrc", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.job", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.postcssrc", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.babelrc", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.csslintrc", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.eslintrc", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.jade-lintrc", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.pug-lintrc", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.jshintrc", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.stylelintrc", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.yarnrc", "//", EvaluatorType.CPP, ConditionalType.CLineComments), + new OperationConfigDefault("**/*.css.min", "/*", EvaluatorType.CPP, ConditionalType.CBlockComments), + new OperationConfigDefault("**/*.css", "/*", EvaluatorType.CPP, ConditionalType.CBlockComments), + new OperationConfigDefault("**/*.cshtml", "@*", EvaluatorType.CPP, ConditionalType.Razor), + new OperationConfigDefault("**/*.razor", "@*", EvaluatorType.CPP, ConditionalType.Razor), + new OperationConfigDefault("**/*.vbhtml", "@*", EvaluatorType.VB, ConditionalType.Razor), + new OperationConfigDefault("**/*.cs", "//", EvaluatorType.CPP, ConditionalType.CNoComments), + new OperationConfigDefault("**/*.fs", "//", EvaluatorType.CPP, ConditionalType.CNoComments), + new OperationConfigDefault("**/*.c", "//", EvaluatorType.CPP, ConditionalType.CNoComments), + new OperationConfigDefault("**/*.cpp", "//", EvaluatorType.CPP, ConditionalType.CNoComments), + new OperationConfigDefault("**/*.cxx", "//", EvaluatorType.CPP, ConditionalType.CNoComments), + new OperationConfigDefault("**/*.h", "//", EvaluatorType.CPP, ConditionalType.CNoComments), + new OperationConfigDefault("**/*.hpp", "//", EvaluatorType.CPP, ConditionalType.CNoComments), + new OperationConfigDefault("**/*.hxx", "//", EvaluatorType.CPP, ConditionalType.CNoComments), + new OperationConfigDefault("**/*.cake", "//", EvaluatorType.CPP, ConditionalType.CNoComments), + new OperationConfigDefault("**/*.*proj", " + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Key [{0}] is already present. + {0} contains the actual key + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/LocalizationLocator.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/LocalizationLocator.cs new file mode 100644 index 000000000000..e5d0ac966155 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/LocalizationLocator.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Utils +{ + public class LocalizationLocator : ILocalizationLocator + { + public LocalizationLocator( + string locale, + string configPlace, + string identity, + string author, + string name, + string description, + IReadOnlyDictionary parameterSymbols) + { + Locale = locale; + ConfigPlace = configPlace; + Identity = identity; + Author = author; + Name = name; + Description = description; + ParameterSymbols = parameterSymbols; + } + + public string Locale { get; } + + public string ConfigPlace { get; } + + public string Identity { get; } + + public string Author { get; } + + public string Name { get; } + + public string Description { get; } + + public IReadOnlyDictionary ParameterSymbols { get; } + + IReadOnlyList IValidationInfo.ValidationErrors => []; + + bool IValidationInfo.IsValid => true; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/Microsoft.TemplateEngine.Utils.csproj b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/Microsoft.TemplateEngine.Utils.csproj new file mode 100644 index 000000000000..59c4595003b9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/Microsoft.TemplateEngine.Utils.csproj @@ -0,0 +1,41 @@ + + + + $(NetMinimum);$(NetCurrent);netstandard2.0;$(NetFrameworkMinimum) + Components used by all Template Engine extensions and consumers + true + true + true + + true + + + + + + annotations + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/MultiValueParameter.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/MultiValueParameter.cs new file mode 100644 index 000000000000..99d3fb9e9cd5 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/MultiValueParameter.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Utils +{ + /// + /// Container type holding the values for parameters with multiple values. + /// + public class MultiValueParameter + { + /// + /// Separator of multi valued parameters (currently applicable only to choices). + /// + public const char MultiValueSeparator = '|'; + + /// + /// Initializes a new instance of the class. + /// + /// + public MultiValueParameter(IReadOnlyList values) + { + Values = values; + } + + /// + /// Set of characters that can be used for separating multi valued parameters (currently applicable only to choices). + /// + public static char[] MultiValueSeparators { get; } = new[] { MultiValueSeparator, ',' }; + + /// + /// The actual atomic values specified for the parameter. + /// + public IReadOnlyList Values { get; } + + 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/TemplateEngine/Microsoft.TemplateEngine.Utils/ParameterSetDataExtensions.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/ParameterSetDataExtensions.cs new file mode 100644 index 000000000000..6062cfae6cb1 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/ParameterSetDataExtensions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Parameters; + +namespace Microsoft.TemplateEngine.Utils +{ + public static class ParameterSetDataExtensions + { + /// + /// Creates instance of from the legacy . + /// + /// Legacy parameter set to be converted. + /// + [Obsolete("IParameterSet should not be used - it is replaced with IParameterSetData", false)] + public static IParameterSetData ToParameterSetData(this IParameterSet parameterSet) + { + IParameterDefinitionSet parametersDefinition = new ParameterDefinitionSet(parameterSet.ParameterDefinitions); + IReadOnlyList data = parameterSet.ResolvedValues.Select, ParameterData>(p => + new ParameterData(p.Key, p.Value, DataSource.User)) + .ToList(); + return new ParameterSetData(parametersDefinition, data); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/ParserExtensions.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/ParserExtensions.cs new file mode 100644 index 000000000000..e049b07dcd13 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/ParserExtensions.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; + +namespace Microsoft.TemplateEngine.Utils +{ + public static class ParserExtensions + { + /// + /// extension that try to parse first in current culture, then in invariant culture. + /// + /// The value to parse. + /// Parsed double value if can be parsed. + /// + /// true when can be parsed in current or invariant culture. + /// false when cannot be parsed in current or invariant culture. + /// + public static bool DoubleTryParseCurrentOrInvariant(string? stringValue, out double doubleValue) + { + if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.CurrentCulture, out doubleValue)) + { + return true; + } + else + { + return double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out doubleValue); + } + } + + /// + /// extension that try to parse first in current culture, then in invariant culture. + /// + /// The value to parse. + /// Parsed double value if can be parsed. + /// is not in an appropriate format for a type. + public static double ConvertToDoubleCurrentOrInvariant(object value) + { + try + { + if (value is string s) + { + return double.Parse(s, NumberStyles.Float); + } + return Convert.ToDouble(value, CultureInfo.CurrentCulture); + } + catch (FormatException) + { + if (value is string s) + { + return double.Parse(s, NumberStyles.Float, CultureInfo.InvariantCulture); + } + return Convert.ToDouble(value, CultureInfo.InvariantCulture); + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/PhysicalFileSystem.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/PhysicalFileSystem.cs new file mode 100644 index 000000000000..28667dc8f299 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/PhysicalFileSystem.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; + +namespace Microsoft.TemplateEngine.Utils +{ + /// + /// Local file system implementation of . + /// + /// + public class PhysicalFileSystem : IPhysicalFileSystem + { + public bool DirectoryExists(string directory) + { + return Directory.Exists(directory); + } + + public bool FileExists(string file) + { + return File.Exists(file); + } + + public Stream CreateFile(string path) + { + return File.Create(path); + } + + public void CreateDirectory(string path) + { + _ = Directory.CreateDirectory(path); + } + + public string GetCurrentDirectory() + { + return Directory.GetCurrentDirectory(); + } + + public IEnumerable EnumerateFileSystemEntries(string directoryName, string pattern, SearchOption searchOption) + { + return Directory.EnumerateFileSystemEntries(directoryName, pattern, searchOption); + } + + public void FileCopy(string source, string target, bool overwrite) + { + File.Copy(source, target, overwrite); + } + + public void DirectoryDelete(string path, bool recursive) + { + Directory.Delete(path, recursive); + } + + public string ReadAllText(string path) + { + return File.ReadAllText(path); + } + + public byte[] ReadAllBytes(string path) + { + return File.ReadAllBytes(path); + } + + public void WriteAllText(string path, string value) + { + File.WriteAllText(path, value); + } + + public IEnumerable EnumerateDirectories(string path, string pattern, SearchOption searchOption) + { + return Directory.EnumerateDirectories(path, pattern, searchOption); + } + + public IEnumerable EnumerateFiles(string path, string pattern, SearchOption searchOption) + { + return Directory.EnumerateFiles(path, pattern, searchOption); + } + + public Stream OpenRead(string path) + { + return File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + public void FileDelete(string path) + { + File.Delete(path); + } + + public FileAttributes GetFileAttributes(string file) + { + return File.GetAttributes(file); + } + + public void SetFileAttributes(string file, FileAttributes attributes) + { + File.SetAttributes(file, attributes); + } + + public DateTime GetLastWriteTimeUtc(string file) + { + return File.GetLastWriteTimeUtc(file); + } + + public void SetLastWriteTimeUtc(string file, DateTime lastWriteTimeUtc) + { + File.SetLastWriteTimeUtc(file, lastWriteTimeUtc); + } + + public string PathRelativeTo(string target, string relativeTo) + { + string resultPath; + try + { + string basePath = Path.GetFullPath(relativeTo); + string sourceFullPath = Path.GetFullPath(target); + resultPath = PathRelativeToInternal(sourceFullPath, basePath); + } + catch (Exception) + { + resultPath = NormalizePath(target); + } + + return resultPath; + } + + public IDisposable WatchFileChanges(string filePath, FileSystemEventHandler fileChanged) + { + FileSystemWatcher watcher = new FileSystemWatcher(Path.GetDirectoryName(filePath), Path.GetFileName(filePath)); + watcher.Changed += fileChanged; + watcher.NotifyFilter = NotifyFilters.LastWrite; + watcher.EnableRaisingEvents = true; + return watcher; + } + + internal static string PathRelativeToInternal(string target, string relativeTo) + { + string resultPath = target; + relativeTo = relativeTo.TrimEnd('/', '\\'); + if (target.StartsWith(relativeTo, StringComparison.CurrentCulture)) + { + resultPath = target.Substring(relativeTo.Length + 1); + } + + return NormalizePath(resultPath); + } + + private static string NormalizePath(string path) + { + char desiredDirectorySeparator = Path.DirectorySeparatorChar; + char undesiredSeparatorChar = desiredDirectorySeparator switch + { + '/' => '\\', + '\\' => '/', + _ => desiredDirectorySeparator, + }; + return path.Replace(undesiredSeparatorChar, desiredDirectorySeparator); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/PublicAPI.Shipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..77c0d61c507f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/PublicAPI.Shipped.txt @@ -0,0 +1,277 @@ +#nullable enable +Microsoft.TemplateEngine.Utils.ArrayExtensions +Microsoft.TemplateEngine.Utils.CacheParameter +Microsoft.TemplateEngine.Utils.CacheParameter.CacheParameter() -> void +Microsoft.TemplateEngine.Utils.CacheParameter.DataType.get -> string? +Microsoft.TemplateEngine.Utils.CacheParameter.DataType.set -> void +Microsoft.TemplateEngine.Utils.CacheParameter.DefaultIfOptionWithoutValue.get -> string? +Microsoft.TemplateEngine.Utils.CacheParameter.DefaultIfOptionWithoutValue.set -> void +Microsoft.TemplateEngine.Utils.CacheParameter.DefaultValue.get -> string? +Microsoft.TemplateEngine.Utils.CacheParameter.DefaultValue.set -> void +Microsoft.TemplateEngine.Utils.CacheParameter.Description.get -> string? +Microsoft.TemplateEngine.Utils.CacheParameter.Description.set -> void +Microsoft.TemplateEngine.Utils.CacheParameter.DisplayName.get -> string? +Microsoft.TemplateEngine.Utils.CacheParameter.DisplayName.set -> void +Microsoft.TemplateEngine.Utils.CacheParameter.ShouldSerializeDefaultIfOptionWithoutValue() -> bool +Microsoft.TemplateEngine.Utils.CacheTag +Microsoft.TemplateEngine.Utils.CacheTag.CacheTag(string? displayName, string? description, System.Collections.Generic.IReadOnlyDictionary! choices, string? defaultValue) -> void +Microsoft.TemplateEngine.Utils.CacheTag.CacheTag(string? displayName, string? description, System.Collections.Generic.IReadOnlyDictionary! choices, string? defaultValue, string? defaultIfOptionWithoutValue) -> void +Microsoft.TemplateEngine.Utils.CacheTag.Choices.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Utils.CacheTag.DefaultIfOptionWithoutValue.get -> string? +Microsoft.TemplateEngine.Utils.CacheTag.DefaultIfOptionWithoutValue.set -> void +Microsoft.TemplateEngine.Utils.CacheTag.DefaultValue.get -> string? +Microsoft.TemplateEngine.Utils.CacheTag.Description.get -> string? +Microsoft.TemplateEngine.Utils.CacheTag.DisplayName.get -> string? +Microsoft.TemplateEngine.Utils.CacheTag.ShouldSerializeDefaultIfOptionWithoutValue() -> bool +Microsoft.TemplateEngine.Utils.CombinedList +Microsoft.TemplateEngine.Utils.CombinedList.Count.get -> int +Microsoft.TemplateEngine.Utils.CombinedList.this[int index].get -> T +Microsoft.TemplateEngine.Utils.ContentGenerationException +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost +Microsoft.TemplateEngine.Utils.DefaultTemplatePackageProvider +Microsoft.TemplateEngine.Utils.DefaultTemplatePackageProvider.DefaultTemplatePackageProvider(Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProviderFactory! factory, Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, System.Collections.Generic.IEnumerable? nupkgs = null, System.Collections.Generic.IEnumerable? folders = null) -> void +Microsoft.TemplateEngine.Utils.DefaultTemplatePackageProvider.Factory.get -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProviderFactory! +Microsoft.TemplateEngine.Utils.DefaultTemplatePackageProvider.GetAllTemplatePackagesAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateEngine.Utils.DefaultTemplatePackageProvider.TemplatePackagesChanged -> System.Action? +Microsoft.TemplateEngine.Utils.DefaultTemplatePackageProvider.UpdatePackages(System.Collections.Generic.IEnumerable? nupkgs = null, System.Collections.Generic.IEnumerable? folders = null) -> void +Microsoft.TemplateEngine.Utils.DictionaryExtensions +Microsoft.TemplateEngine.Utils.EngineEnvironmentSettingsExtensions +Microsoft.TemplateEngine.Utils.EngineInitializationException +Microsoft.TemplateEngine.Utils.EqualityExtensions +Microsoft.TemplateEngine.Utils.ExactVersionSpecification +Microsoft.TemplateEngine.Utils.FileFindHelpers +Microsoft.TemplateEngine.Utils.FileSystemInfoExtensions +Microsoft.TemplateEngine.Utils.Glob +Microsoft.TemplateEngine.Utils.IVersionSpecification +Microsoft.TemplateEngine.Utils.IVersionSpecification.CheckIfVersionIsValid(string! versionToCheck) -> bool +Microsoft.TemplateEngine.Utils.InMemoryFileSystem +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.CreateDirectory(string! path) -> void +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.CreateFile(string! path) -> System.IO.Stream! +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.DirectoryDelete(string! path, bool recursive) -> void +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.DirectoryExists(string! directory) -> bool +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.EnumerateDirectories(string! path, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.EnumerateFiles(string! path, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.EnumerateFileSystemEntries(string! directoryName, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.FileCopy(string! sourcePath, string! targetPath, bool overwrite) -> void +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.FileDelete(string! path) -> void +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.FileExists(string! file) -> bool +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.GetCurrentDirectory() -> string! +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.GetFileAttributes(string! file) -> System.IO.FileAttributes +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.GetLastWriteTimeUtc(string! file) -> System.DateTime +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.InMemoryFileSystem(string! root, Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem! basis) -> void +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.OpenRead(string! path) -> System.IO.Stream! +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.ReadAllBytes(string! path) -> byte[]! +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.ReadAllText(string! path) -> string! +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.SetFileAttributes(string! file, System.IO.FileAttributes attributes) -> void +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.SetLastWriteTimeUtc(string! file, System.DateTime lastWriteTimeUtc) -> void +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.WatchFileChanges(string! filePath, System.IO.FileSystemEventHandler! fileChanged) -> System.IDisposable! +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.WriteAllText(string! path, string! value) -> void +Microsoft.TemplateEngine.Utils.InstallRequestPathResolution +Microsoft.TemplateEngine.Utils.ListExtensions +Microsoft.TemplateEngine.Utils.LocalizationLocator +Microsoft.TemplateEngine.Utils.LocalizationLocator.Author.get -> string! +Microsoft.TemplateEngine.Utils.LocalizationLocator.ConfigPlace.get -> string! +Microsoft.TemplateEngine.Utils.LocalizationLocator.Description.get -> string! +Microsoft.TemplateEngine.Utils.LocalizationLocator.Identity.get -> string! +Microsoft.TemplateEngine.Utils.LocalizationLocator.Locale.get -> string! +Microsoft.TemplateEngine.Utils.LocalizationLocator.LocalizationLocator(string! locale, string! configPlace, string! identity, string! author, string! name, string! description, System.Collections.Generic.IReadOnlyDictionary! parameterSymbols) -> void +Microsoft.TemplateEngine.Utils.LocalizationLocator.Name.get -> string! +Microsoft.TemplateEngine.Utils.LocalizationLocator.ParameterSymbols.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Utils.ParserExtensions +Microsoft.TemplateEngine.Utils.PhysicalFileSystem +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.CreateDirectory(string! path) -> void +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.CreateFile(string! path) -> System.IO.Stream! +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.DirectoryDelete(string! path, bool recursive) -> void +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.DirectoryExists(string! directory) -> bool +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.EnumerateDirectories(string! path, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.EnumerateFiles(string! path, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.EnumerateFileSystemEntries(string! directoryName, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.FileCopy(string! source, string! target, bool overwrite) -> void +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.FileDelete(string! path) -> void +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.FileExists(string! file) -> bool +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.GetCurrentDirectory() -> string! +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.GetFileAttributes(string! file) -> System.IO.FileAttributes +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.GetLastWriteTimeUtc(string! file) -> System.DateTime +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.OpenRead(string! path) -> System.IO.Stream! +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.PhysicalFileSystem() -> void +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.ReadAllBytes(string! path) -> byte[]! +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.ReadAllText(string! path) -> string! +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.SetFileAttributes(string! file, System.IO.FileAttributes attributes) -> void +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.SetLastWriteTimeUtc(string! file, System.DateTime lastWriteTimeUtc) -> void +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.WatchFileChanges(string! filePath, System.IO.FileSystemEventHandler! fileChanged) -> System.IDisposable! +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.WriteAllText(string! path, string! value) -> void +Microsoft.TemplateEngine.Utils.RangeVersionSpecification +Microsoft.TemplateEngine.Utils.RangeVersionSpecification.IsEndInclusive.get -> bool +Microsoft.TemplateEngine.Utils.RangeVersionSpecification.IsStartInclusive.get -> bool +Microsoft.TemplateEngine.Utils.TemplateAuthoringException +Microsoft.TemplateEngine.Utils.TemplateInfoExtensions +Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions +Microsoft.TemplateEngine.Utils.TemplateParameter +Microsoft.TemplateEngine.Utils.TemplateParameter.Choices.get -> System.Collections.Generic.IReadOnlyDictionary? +Microsoft.TemplateEngine.Utils.TemplateParameter.DataType.get -> string! +Microsoft.TemplateEngine.Utils.TemplateParameter.DefaultIfOptionWithoutValue.get -> string? +Microsoft.TemplateEngine.Utils.TemplateParameter.DefaultValue.get -> string? +Microsoft.TemplateEngine.Utils.TemplateParameter.Description.get -> string? +Microsoft.TemplateEngine.Utils.TemplateParameter.DisplayName.get -> string? +Microsoft.TemplateEngine.Utils.TemplateParameter.Documentation.get -> string? +Microsoft.TemplateEngine.Utils.TemplateParameter.IsName.get -> bool +Microsoft.TemplateEngine.Utils.TemplateParameter.Name.get -> string! +Microsoft.TemplateEngine.Utils.TemplateParameter.Priority.get -> Microsoft.TemplateEngine.Abstractions.TemplateParameterPriority +Microsoft.TemplateEngine.Utils.TemplateParameter.Type.get -> string! +Microsoft.TemplateEngine.Utils.TemplateParameterExtensions +Microsoft.TemplateEngine.Utils.Timing +Microsoft.TemplateEngine.Utils.Timing.Dispose() -> void +Microsoft.TemplateEngine.Utils.VersionStringHelpers +Microsoft.TemplateEngine.Utils.WellKnownSearchFilters +static Microsoft.TemplateEngine.Utils.ExactVersionSpecification.TryParse(string! version, out Microsoft.TemplateEngine.Utils.IVersionSpecification? specification) -> bool +static Microsoft.TemplateEngine.Utils.RangeVersionSpecification.TryParse(string! range, out Microsoft.TemplateEngine.Utils.IVersionSpecification? specification) -> bool +static Microsoft.TemplateEngine.Utils.TemplateInfoExtensions.GetChoiceParameter(this Microsoft.TemplateEngine.Abstractions.ITemplateInfo! template, string! parameterName) -> Microsoft.TemplateEngine.Abstractions.ITemplateParameter? +static Microsoft.TemplateEngine.Utils.TemplateInfoExtensions.GetLanguage(this Microsoft.TemplateEngine.Abstractions.ITemplateInfo! template) -> string? +static Microsoft.TemplateEngine.Utils.TemplateInfoExtensions.GetParameter(this Microsoft.TemplateEngine.Abstractions.ITemplateInfo! template, string! parameterName) -> Microsoft.TemplateEngine.Abstractions.ITemplateParameter? +static Microsoft.TemplateEngine.Utils.TemplateInfoExtensions.GetTagValue(this Microsoft.TemplateEngine.Abstractions.ITemplateInfo! template, string! tagName) -> string? +static Microsoft.TemplateEngine.Utils.TemplateInfoExtensions.GetTemplateType(this Microsoft.TemplateEngine.Abstractions.ITemplateInfo! template) -> string? +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasAuthorExactMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasAuthorMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasAuthorMismatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasAuthorPartialMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasBaselineMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasBaselineMismatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasClassificationMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasClassificationMismatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasLanguageMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasLanguageMismatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasNameExactMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasNameMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasNameMismatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasNamePartialMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasShortNameExactMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasShortNameMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasShortNameMismatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasShortNamePartialMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasTypeMatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateMatchInfoExtensions.HasTypeMismatch(this Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo! templateMatchInfo) -> bool +static Microsoft.TemplateEngine.Utils.TemplateParameterExtensions.IsChoice(this Microsoft.TemplateEngine.Abstractions.ITemplateParameter! parameter) -> bool +static Microsoft.TemplateEngine.Utils.WellKnownSearchFilters.MatchesAllCriteria.get -> System.Func! +static Microsoft.TemplateEngine.Utils.WellKnownSearchFilters.MatchesAtLeastOneCriteria.get -> System.Func! +static Microsoft.TemplateEngine.Utils.WellKnownSearchFilters.NameFilter(string! name) -> System.Func! +static Microsoft.TemplateEngine.Utils.WellKnownSearchFilters.TypeFilter(string? inputType) -> System.Func! +Microsoft.TemplateEngine.Utils.ExactVersionSpecification.CheckIfVersionIsValid(string! versionToCheck) -> bool +Microsoft.TemplateEngine.Utils.ExactVersionSpecification.ExactVersionSpecification(string! version) -> void +Microsoft.TemplateEngine.Utils.ExactVersionSpecification.RequiredVersion.get -> string! +Microsoft.TemplateEngine.Utils.RangeVersionSpecification.CheckIfVersionIsValid(string! versionToCheck) -> bool +Microsoft.TemplateEngine.Utils.RangeVersionSpecification.MaxVersion.get -> string! +Microsoft.TemplateEngine.Utils.RangeVersionSpecification.MinVersion.get -> string! +Microsoft.TemplateEngine.Utils.RangeVersionSpecification.RangeVersionSpecification(string! min, string! max, bool isStartInclusive, bool isEndInclusive) -> void +static Microsoft.TemplateEngine.Utils.EngineEnvironmentSettingsExtensions.TryGetMountPoint(this Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! engineEnvironment, string! mountPointUri, out Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint? mountPoint) -> bool +static Microsoft.TemplateEngine.Utils.InstallRequestPathResolution.ExpandMaskedPath(string! maskedPath, Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Utils.IFileSystemInfoExtensions +static Microsoft.TemplateEngine.Utils.IFileSystemInfoExtensions.GetDisplayPath(this Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo! fileSystemInfo) -> string! +const Microsoft.TemplateEngine.Utils.MultiValueParameter.MultiValueSeparator = '|' -> char +Microsoft.TemplateEngine.Utils.BaselineInfo +Microsoft.TemplateEngine.Utils.BaselineInfo.BaselineInfo(System.Collections.Generic.IReadOnlyDictionary! defaultOverrides, string? description = null) -> void +Microsoft.TemplateEngine.Utils.BaselineInfo.DefaultOverrides.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateEngine.Utils.BaselineInfo.Description.get -> string? +Microsoft.TemplateEngine.Utils.CombinedList.CombinedList(System.Collections.Generic.IReadOnlyList! first, System.Collections.Generic.IReadOnlyList! second) -> void +Microsoft.TemplateEngine.Utils.CombinedList.GetEnumerator() -> System.Collections.Generic.IEnumerator! +Microsoft.TemplateEngine.Utils.ContentGenerationException.ContentGenerationException(string! message) -> void +Microsoft.TemplateEngine.Utils.ContentGenerationException.ContentGenerationException(string! message, System.Exception! innerException) -> void +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.DefaultTemplateEngineHost(string! hostIdentifier, string! version) -> void +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.DefaultTemplateEngineHost(string! hostIdentifier, string! version, System.Collections.Generic.Dictionary! defaults, System.Collections.Generic.IReadOnlyList! fallbackHostTemplateConfigNames) -> void +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.DefaultTemplateEngineHost(string! hostIdentifier, string! version, System.Collections.Generic.Dictionary? defaults) -> void +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.DefaultTemplateEngineHost(string! hostIdentifier, string! version, System.Collections.Generic.Dictionary? defaults, System.Collections.Generic.IReadOnlyList<(System.Type! InterfaceType, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! Instance)>? builtIns) -> void +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.DefaultTemplateEngineHost(string! hostIdentifier, string! version, System.Collections.Generic.Dictionary? defaults, System.Collections.Generic.IReadOnlyList<(System.Type! InterfaceType, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! Instance)>? builtIns, System.Collections.Generic.IReadOnlyList? fallbackHostTemplateConfigNames) -> void +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.FallbackHostTemplateConfigNames.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.FileSystem.get -> Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem! +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.HostIdentifier.get -> string! +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.LogDiagnosticMessage(string! message, string! category, params string![]! details) -> void +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.Logger.get -> Microsoft.Extensions.Logging.ILogger! +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.LoggerFactory.get -> Microsoft.Extensions.Logging.ILoggerFactory! +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.LogTiming(string! label, System.TimeSpan duration, int depth) -> void +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.OnConfirmPartialMatch(string! name) -> bool +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.OnLogTiming.get -> System.Action? +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.OnLogTiming.set -> void +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.OnPotentiallyDestructiveChangesDetected(System.Collections.Generic.IReadOnlyList! changes, System.Collections.Generic.IReadOnlyList! destructiveChanges) -> bool +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.RegisterDiagnosticLogger(string! category, System.Action! messageHandler) -> void +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.Version.get -> string! +Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.VirtualizeDirectory(string! path) -> void +Microsoft.TemplateEngine.Utils.DirectedGraph +Microsoft.TemplateEngine.Utils.DirectedGraph.DirectedGraph(System.Collections.Generic.Dictionary!>! dependenciesMap) -> void +Microsoft.TemplateEngine.Utils.DirectedGraph.GetDependents(System.Collections.Generic.IEnumerable! vertices) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.Utils.DirectedGraph.GetSubGraphDependentOnVertices(System.Collections.Generic.IReadOnlyList! vertices, bool includeSeedVertices = false) -> Microsoft.TemplateEngine.Utils.DirectedGraph! +Microsoft.TemplateEngine.Utils.DirectedGraph.HasCycle(out System.Collections.Generic.IReadOnlyList! cycle) -> bool +Microsoft.TemplateEngine.Utils.DirectedGraph.TryGetTopologicalSort(out System.Collections.Generic.IReadOnlyList! sortedElements) -> bool +Microsoft.TemplateEngine.Utils.EngineInitializationException.EngineInitializationException(string! message, string! settingsItem) -> void +Microsoft.TemplateEngine.Utils.EngineInitializationException.EngineInitializationException(string! message, string! settingsItem, System.Exception! innerException) -> void +Microsoft.TemplateEngine.Utils.EngineInitializationException.SettingsItem.get -> string! +Microsoft.TemplateEngine.Utils.Glob.IsMatch(string! test) -> bool +Microsoft.TemplateEngine.Utils.InMemoryFileSystem.PathRelativeTo(string! target, string! relativeTo) -> string! +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.ParameterSetDataExtensions +Microsoft.TemplateEngine.Utils.PhysicalFileSystem.PathRelativeTo(string! target, string! relativeTo) -> string! +Microsoft.TemplateEngine.Utils.RuntimeValueUtil +Microsoft.TemplateEngine.Utils.TemplateAuthoringException.TemplateAuthoringException(string! message, string! configItem) -> void +Microsoft.TemplateEngine.Utils.TemplateParameter.AllowMultipleValues.get -> bool +Microsoft.TemplateEngine.Utils.TemplateParameter.Equals(Microsoft.TemplateEngine.Abstractions.ITemplateParameter! other) -> bool +Microsoft.TemplateEngine.Utils.TemplateParameter.Precedence.get -> Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence! +Microsoft.TemplateEngine.Utils.TemplateParameter.TemplateParameter(string! name, string! type, string! datatype, Microsoft.TemplateEngine.Abstractions.TemplateParameterPrecedence? precedence = null, 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! +override Microsoft.TemplateEngine.Utils.TemplateParameter.Equals(object! obj) -> bool +override Microsoft.TemplateEngine.Utils.TemplateParameter.GetHashCode() -> int +override Microsoft.TemplateEngine.Utils.TemplateParameter.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! +static Microsoft.TemplateEngine.Utils.DirectedGraph.implicit operator Microsoft.TemplateEngine.Utils.DirectedGraph!(System.Collections.Generic.Dictionary!>! dependenciesMap) -> Microsoft.TemplateEngine.Utils.DirectedGraph! +static Microsoft.TemplateEngine.Utils.EnumerableExtensions.AddRange(this System.Collections.Generic.Queue! queue, System.Collections.Generic.IEnumerable! elements) -> void +static Microsoft.TemplateEngine.Utils.EnumerableExtensions.ForEach(this System.Collections.Generic.IEnumerable! sequence, System.Action! action) -> void +static Microsoft.TemplateEngine.Utils.EnumerableExtensions.GetDuplicates(this System.Collections.Generic.IEnumerable? sequence, System.Collections.Generic.IEqualityComparer? comparer = null) -> System.Collections.Generic.IEnumerable! +static Microsoft.TemplateEngine.Utils.EnumerableExtensions.HasDuplicates(this System.Collections.Generic.IEnumerable? sequence, System.Collections.Generic.IEqualityComparer? comparer = null) -> bool +static Microsoft.TemplateEngine.Utils.EnumerableExtensions.ToCsvString(this System.Collections.Generic.IEnumerable? source, bool useSpace = true) -> string! +static Microsoft.TemplateEngine.Utils.EqualityExtensions.AllAreTheSame(this System.Collections.Generic.IEnumerable! items, System.Func! selector) -> bool +static Microsoft.TemplateEngine.Utils.EqualityExtensions.AllAreTheSame(this System.Collections.Generic.IEnumerable! items, System.Func! selector, System.Collections.Generic.IEqualityComparer! comparer) -> bool +static Microsoft.TemplateEngine.Utils.EqualityExtensions.AllAreTheSame(this System.Collections.Generic.IEnumerable! items, System.Func! selector, System.Func! comparer) -> bool +static Microsoft.TemplateEngine.Utils.FileFindHelpers.FindFilesAtOrAbovePath(Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem! fileSystem, string! startPath, string! matchPattern, System.Func? secondaryFilter = null) -> System.Collections.Generic.IReadOnlyList! +static Microsoft.TemplateEngine.Utils.FileSystemInfoExtensions.CombinePaths(this string! basePath, params string![]! paths) -> string! +static Microsoft.TemplateEngine.Utils.FileSystemInfoExtensions.CopyTo(this Microsoft.TemplateEngine.Abstractions.Mount.IDirectory! source, string! target) -> void +static Microsoft.TemplateEngine.Utils.FileSystemInfoExtensions.DirectoryInfo(this Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo! info, string! path) -> Microsoft.TemplateEngine.Abstractions.Mount.IDirectory? +static Microsoft.TemplateEngine.Utils.FileSystemInfoExtensions.FileInfo(this Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo! info, string! path) -> Microsoft.TemplateEngine.Abstractions.Mount.IFile? +static Microsoft.TemplateEngine.Utils.FileSystemInfoExtensions.FileSystemInfo(this Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo! info, string! path) -> Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo? +static Microsoft.TemplateEngine.Utils.FileSystemInfoExtensions.NormalizePath(this string! path) -> string! +static Microsoft.TemplateEngine.Utils.FileSystemInfoExtensions.PathRelativeTo(this Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo! info, Microsoft.TemplateEngine.Abstractions.Mount.IFileSystemInfo! relativeTo) -> string! +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.ParameterSetDataExtensions.ToParameterSetData(this Microsoft.TemplateEngine.Abstractions.IParameterSet! parameterSet) -> Microsoft.TemplateEngine.Abstractions.Parameters.IParameterSetData! +static Microsoft.TemplateEngine.Utils.ParserExtensions.ConvertToDoubleCurrentOrInvariant(object! value) -> double +static Microsoft.TemplateEngine.Utils.ParserExtensions.DoubleTryParseCurrentOrInvariant(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 +static Microsoft.TemplateEngine.Utils.TemplateParameterExtensions.IsValidMultiValueParameterValue(this string! value) -> bool +static Microsoft.TemplateEngine.Utils.TemplateParameterExtensions.TokenizeMultiValueParameter(this string! literal) -> System.Collections.Generic.IReadOnlyList! +override Microsoft.TemplateEngine.Utils.RangeVersionSpecification.ToString() -> string! +static Microsoft.TemplateEngine.Utils.Timing.Over(Microsoft.Extensions.Logging.ILogger! logger, string! label) -> Microsoft.TemplateEngine.Utils.Timing! +static Microsoft.TemplateEngine.Utils.VersionStringHelpers.CompareVersions(string? version1, string? version2) -> int? +static Microsoft.TemplateEngine.Utils.VersionStringHelpers.IsVersionWellFormed(string? version) -> bool +static Microsoft.TemplateEngine.Utils.VersionStringHelpers.TryParseVersionSpecification(string! versionString, out Microsoft.TemplateEngine.Utils.IVersionSpecification? specification) -> bool +static Microsoft.TemplateEngine.Utils.WellKnownSearchFilters.AuthorFilter(string? author) -> System.Func! +static Microsoft.TemplateEngine.Utils.WellKnownSearchFilters.BaselineFilter(string? baselineName) -> System.Func! +static Microsoft.TemplateEngine.Utils.WellKnownSearchFilters.ClassificationFilter(string? classification) -> System.Func! +static Microsoft.TemplateEngine.Utils.WellKnownSearchFilters.ConstraintFilters(System.Collections.Generic.IEnumerable! constraintDefinitions) -> System.Collections.Generic.IEnumerable!>! +static Microsoft.TemplateEngine.Utils.WellKnownSearchFilters.LanguageFilter(string? language) -> System.Func! +virtual Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.BuiltInComponents.get -> System.Collections.Generic.IReadOnlyList<(System.Type! InterfaceType, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! Instance)>! +virtual Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.LogMessage(string! message) -> void +virtual Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.OnCriticalError(string! code, string! message, string! currentFile, long currentPosition) -> void +virtual Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.OnNonCriticalError(string! code, string! message, string! currentFile, long currentPosition) -> bool +virtual Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.OnParameterError(Microsoft.TemplateEngine.Abstractions.ITemplateParameter! parameter, string! receivedValue, string! message, out string? newValue) -> bool +virtual Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.OnSymbolUsed(string! symbol, object! value) -> void +virtual Microsoft.TemplateEngine.Utils.DefaultTemplateEngineHost.TryGetHostParamDefault(string! paramName, out string! value) -> bool + + diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/PublicAPI.Unshipped.txt b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..01d1b4d0de13 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/PublicAPI.Unshipped.txt @@ -0,0 +1,26 @@ +Microsoft.TemplateEngine.Utils.AsyncLazy +Microsoft.TemplateEngine.Utils.IScanTemplateInfoExtensions +static Microsoft.TemplateEngine.Utils.DictionaryExtensions.TryAdd(this System.Collections.Generic.IDictionary! dict, TKey key, TValue value, System.Predicate! condition) -> bool +static Microsoft.TemplateEngine.Utils.IScanTemplateInfoExtensions.ToITemplateInfo(this Microsoft.TemplateEngine.Abstractions.IScanTemplateInfo! templateInfo, string? locFilePath = null, string? hostFilePath = null) -> Microsoft.TemplateEngine.Abstractions.ITemplateInfo! +Microsoft.TemplateEngine.Utils.TemplateAuthoringException.ConfigItem.get -> string? +Microsoft.TemplateEngine.Utils.TemplateAuthoringException.TemplateAuthoringException(string! message) -> void +Microsoft.TemplateEngine.Utils.TemplateAuthoringException.TemplateAuthoringException(string! message, string? configItem, System.Exception! innerException) -> void +Microsoft.TemplateEngine.Utils.AsyncLazy.AsyncLazy(System.Func!>! taskFactory) -> void +Microsoft.TemplateEngine.Utils.AsyncLazy.AsyncLazy(System.Func! valueFactory) -> void +Microsoft.TemplateEngine.Utils.AsyncLazy.GetAwaiter() -> System.Runtime.CompilerServices.TaskAwaiter +Microsoft.TemplateEngine.Utils.DictionaryExtensions.ConflictingKeysResolution +Microsoft.TemplateEngine.Utils.DictionaryExtensions.ConflictingKeysResolution.Overwrite = 0 -> Microsoft.TemplateEngine.Utils.DictionaryExtensions.ConflictingKeysResolution +Microsoft.TemplateEngine.Utils.DictionaryExtensions.ConflictingKeysResolution.PreserveOriginal = 1 -> Microsoft.TemplateEngine.Utils.DictionaryExtensions.ConflictingKeysResolution +Microsoft.TemplateEngine.Utils.DictionaryExtensions.ConflictingKeysResolution.Throw = 2 -> Microsoft.TemplateEngine.Utils.DictionaryExtensions.ConflictingKeysResolution +Microsoft.TemplateEngine.Utils.IPatternMatcher +Microsoft.TemplateEngine.Utils.IPatternMatcher.IsMatch(string! test) -> bool +Microsoft.TemplateEngine.Utils.TemplateParameter.AllowMultipleValues.init -> void +Microsoft.TemplateEngine.Utils.TemplateParameter.Choices.init -> void +Microsoft.TemplateEngine.Utils.TemplateParameter.DefaultIfOptionWithoutValue.init -> void +Microsoft.TemplateEngine.Utils.TemplateParameter.DefaultValue.init -> void +Microsoft.TemplateEngine.Utils.TemplateParameter.Description.init -> void +Microsoft.TemplateEngine.Utils.TemplateParameter.DisplayName.init -> void +Microsoft.TemplateEngine.Utils.TemplateParameter.IsName.init -> void +Microsoft.TemplateEngine.Utils.TemplateParameter.Precedence.init -> void +static Microsoft.TemplateEngine.Utils.DictionaryExtensions.Merge(this System.Collections.Generic.IDictionary! dict, System.Collections.Generic.IReadOnlyDictionary! another, Microsoft.TemplateEngine.Utils.DictionaryExtensions.ConflictingKeysResolution conflictingKeysResolution = Microsoft.TemplateEngine.Utils.DictionaryExtensions.ConflictingKeysResolution.Overwrite) -> void +static readonly Microsoft.TemplateEngine.Utils.Glob.MatchAll -> Microsoft.TemplateEngine.Utils.IPatternMatcher! diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/RangeVersionSpecification.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/RangeVersionSpecification.cs new file mode 100644 index 000000000000..979b93e904ed --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/RangeVersionSpecification.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Utils +{ + public class RangeVersionSpecification : IVersionSpecification + { + public RangeVersionSpecification(string min, string max, bool isStartInclusive, bool isEndInclusive) + { + MinVersion = min; + MaxVersion = max; + IsStartInclusive = isStartInclusive; + IsEndInclusive = isEndInclusive; + } + + public string MinVersion { get; } + + public string MaxVersion { get; } + + public bool IsStartInclusive { get; } + + public bool IsEndInclusive { get; } + + public static bool TryParse(string range, out IVersionSpecification? specification) + { + bool startInclusive = false; + bool endInclusive = false; + + if (range.StartsWith("[")) + { + startInclusive = true; + } + else if (!range.StartsWith("(")) + { + specification = null; + return false; + } + + if (range.EndsWith("]")) + { + endInclusive = true; + } + else if (!range.EndsWith(")")) + { + specification = null; + return false; + } + + string[] parts = range.Split('-'); + if (parts.Length != 2) + { + specification = null; + return false; + } + + string startVersion = parts[0].Substring(1); + string endVersion = parts[1].Substring(0, parts[1].Length - 1); + + if (IsWildcardVersion(startVersion) && IsWildcardVersion(endVersion)) + { + specification = null; + return false; + } + else if (!IsWildcardVersion(startVersion) && !VersionStringHelpers.IsVersionWellFormed(startVersion)) + { + specification = null; + return false; + } + else if (!IsWildcardVersion(endVersion) && !VersionStringHelpers.IsVersionWellFormed(endVersion)) + { + specification = null; + return false; + } + + specification = new RangeVersionSpecification(startVersion, endVersion, startInclusive, endInclusive); + return true; + } + + public bool CheckIfVersionIsValid(string versionToCheck) + { + bool isStartValid; + bool isEndValid; + + if (!IsWildcardVersion(MinVersion)) + { + int? startComparison = VersionStringHelpers.CompareVersions(MinVersion, versionToCheck); + + if (startComparison == null) + { + return false; + } + + isStartValid = IsStartInclusive ? startComparison.Value <= 0 : startComparison.Value < 0; + } + else + { + isStartValid = true; + } + + if (!IsWildcardVersion(MaxVersion)) + { + int? endComparison = VersionStringHelpers.CompareVersions(versionToCheck, MaxVersion); + + if (endComparison == null) + { + return false; + } + + isEndValid = IsEndInclusive ? endComparison.Value <= 0 : endComparison.Value < 0; + } + else + { + isEndValid = true; + } + + return isStartValid && isEndValid; + } + + public override string ToString() + { + return $"{(IsStartInclusive ? "[" : "(")}{MinVersion}-{MaxVersion}{(IsEndInclusive ? "]" : ")")}"; + } + + private static bool IsWildcardVersion(string version) + { + return string.Equals(version, "*"); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/RuntimeValueUtils.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/RuntimeValueUtils.cs new file mode 100644 index 000000000000..6d09ea1cd68d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/RuntimeValueUtils.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Utils +{ + [Obsolete("Use IBindSymbolSource component instead to get or provide the values to be bound.")] + public static class RuntimeValueUtil + { + [Obsolete("Use IBindSymbolSource components instead to get the values to be bound.")] + public static bool TryGetRuntimeValue(this IParameterSet parameters, IEngineEnvironmentSettings environmentSettings, string name, out object? value, bool skipEnvironmentVariableSearch = false) + { + if (parameters.TryGetParameterDefinition(name, out ITemplateParameter param) + && parameters.ResolvedValues.TryGetValue(param, out object? newValueObject) + && newValueObject != null) + { + value = newValueObject; + return true; + } + + if ((environmentSettings.Host.TryGetHostParamDefault(name, out string? newValue) && newValue != null) + || (!skipEnvironmentVariableSearch && environmentSettings.Environment.GetEnvironmentVariables().TryGetValue(name, out newValue) && newValue != null)) + { + value = newValue; + return true; + } + + value = null; + return false; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateAuthoringException.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateAuthoringException.cs new file mode 100644 index 000000000000..aaaf0a083966 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateAuthoringException.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Utils +{ + public class TemplateAuthoringException : Exception + { + public TemplateAuthoringException(string message) : base(message) + { + } + + public TemplateAuthoringException(string message, string configItem) + : base(message) + { + ConfigItem = configItem; + } + + public TemplateAuthoringException(string message, string? configItem, Exception innerException) + : base(message, innerException) + { + ConfigItem = configItem; + } + + public string? ConfigItem { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateInfoExtensions.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateInfoExtensions.cs new file mode 100644 index 000000000000..44220da524d0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateInfoExtensions.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Utils +{ + /// + /// The class provides ITemplateInfo extension methods. + /// + public static class TemplateInfoExtensions + { + /// + /// Gets the language defined in . + /// + /// template definition. + /// The language defined in the template or null if no language is defined. + // The tags are read in SimpleConfigModel.ConvertedDeprecatedTagsToParameterSymbols method. The single value for the tag is guaranteed. + public static string? GetLanguage(this ITemplateInfo template) + { + return template.GetTagValue("language"); + } + + /// + /// Gets the type defined in . + /// + /// template definition. + /// The type defined in the template or null if no type is defined. + // The tags are read in SimpleConfigModel.ConvertedDeprecatedTagsToParameterSymbols method. The single value for the tag is guaranteed. + public static string? GetTemplateType(this ITemplateInfo template) + { + return template.GetTagValue("type"); + } + + /// + /// Gets the possible values for the tag in . + /// + /// template definition. + /// tag name. + /// The value of tag defined in the template or null if the tag is not defined in the template. + public static string? GetTagValue(this ITemplateInfo template, string tagName) + { + if (template.TagsCollection == null || !template.TagsCollection.TryGetValue(tagName, out string tag)) + { + return null; + } + return tag; + } + + /// + /// Gets the template parameter by . + /// + /// template. + /// parameter name. + /// first with or null if the parameter with such name does not exist. + public static ITemplateParameter? GetParameter(this ITemplateInfo template, string parameterName) + { + return template.ParameterDefinitions.FirstOrDefault( + param => param.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Gets the choice template parameter by . + /// + /// template. + /// parameter name. + /// first choice with or null if the parameter with such name does not exist. + public static ITemplateParameter? GetChoiceParameter(this ITemplateInfo template, string parameterName) + { + return template.ParameterDefinitions.FirstOrDefault( + param => param.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase) + && param.IsChoice()); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateMatchInfoExtensions.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateMatchInfoExtensions.cs new file mode 100644 index 000000000000..2a92cafe35d2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateMatchInfoExtensions.cs @@ -0,0 +1,173 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.TemplateFiltering; + +namespace Microsoft.TemplateEngine.Utils +{ + /// + /// Set of useful extensions when working with . + /// + public static class TemplateMatchInfoExtensions + { + /// + /// Returns true when has or match on . + /// + public static bool HasNameMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Name && (x.Kind == MatchKind.Exact || x.Kind == MatchKind.Partial)); + } + + /// + /// Returns true when has match on . + /// + public static bool HasNameExactMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Name && x.Kind == MatchKind.Exact); + } + + /// + /// Returns true when has match on . + /// + public static bool HasNamePartialMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Name && x.Kind == MatchKind.Partial); + } + + /// + /// Returns true when has on . + /// + public static bool HasNameMismatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Name && x.Kind == MatchKind.Mismatch); + } + + /// + /// Returns true when has or match on . + /// + public static bool HasShortNameMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.ShortName && (x.Kind == MatchKind.Exact || x.Kind == MatchKind.Partial)); + } + + /// + /// Returns true when has match on . + /// + public static bool HasShortNameExactMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.ShortName && x.Kind == MatchKind.Exact); + } + + /// + /// Returns true when has match on . + /// + public static bool HasShortNamePartialMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.ShortName && x.Kind == MatchKind.Partial); + } + + /// + /// Returns true when has on . + /// + public static bool HasShortNameMismatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.ShortName && x.Kind == MatchKind.Mismatch); + } + + /// + /// Returns true when has match on . + /// + public static bool HasTypeMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Type && x.Kind == MatchKind.Exact); + } + + /// + /// Returns true when has on . + /// + public static bool HasTypeMismatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Type && x.Kind == MatchKind.Mismatch); + } + + /// + /// Returns true when has match on . + /// + public static bool HasClassificationMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Classification && x.Kind == MatchKind.Exact); + } + + /// + /// Returns true when has on . + /// + public static bool HasClassificationMismatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Classification && x.Kind == MatchKind.Mismatch); + } + + /// + /// Returns true when has match on . + /// + public static bool HasLanguageMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Language && x.Kind == MatchKind.Exact); + } + + /// + /// Returns true when has on . + /// + public static bool HasLanguageMismatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Language && x.Kind == MatchKind.Mismatch); + } + + /// + /// Returns true when has match on . + /// + public static bool HasBaselineMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Baseline && x.Kind == MatchKind.Exact); + } + + /// + /// Returns true when has on . + /// + public static bool HasBaselineMismatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Baseline && x.Kind == MatchKind.Mismatch); + } + + /// + /// Returns true when has or match on . + /// + public static bool HasAuthorMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Author && (x.Kind == MatchKind.Exact || x.Kind == MatchKind.Partial)); // CodeQL [cs/campaign/constantine] False Positive: CodeQL wrongly detected "Author" + } + + /// + /// Returns true when has match on . + /// + public static bool HasAuthorExactMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Author && x.Kind == MatchKind.Exact); // CodeQL [cs/campaign/constantine] False Positive: CodeQL wrongly detected "Author" + } + + /// + /// Returns true when has match on . + /// + public static bool HasAuthorPartialMatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Author && x.Kind == MatchKind.Partial); // CodeQL [cs/campaign/constantine] False Positive: CodeQL wrongly detected "Author" + } + + /// + /// Returns true when has on . + /// + public static bool HasAuthorMismatch(this ITemplateMatchInfo templateMatchInfo) + { + return templateMatchInfo.MatchDisposition.Any(x => x.Name == MatchInfo.BuiltIn.Author && x.Kind == MatchKind.Mismatch); // CodeQL [cs/campaign/constantine] False Positive: CodeQL wrongly detected "Author" + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateParameter.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateParameter.cs new file mode 100644 index 000000000000..2f29c61383ce --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateParameter.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Utils +{ +#pragma warning disable CS0618 // Type or member is obsolete - compatibility + public class TemplateParameter : ITemplateParameter, IAllowDefaultIfOptionWithoutValue +#pragma warning restore CS0618 // Type or member is obsolete + { + private string? _defaultIfOptionWithoutValue; + + public TemplateParameter( + string name, + string type, + string datatype, + TemplateParameterPrecedence? precedence = default, + bool isName = false, + string? defaultValue = null, + string? defaultIfOptionWithoutValue = null, + string? description = null, + string? displayName = null, + bool allowMultipleValues = false, + IReadOnlyDictionary? choices = null) + { + Name = name; + Type = type; + DataType = datatype; + IsName = isName; + DefaultValue = defaultValue; + DefaultIfOptionWithoutValue = defaultIfOptionWithoutValue; + Description = description; + DisplayName = displayName; + AllowMultipleValues = allowMultipleValues; + Precedence = precedence ?? TemplateParameterPrecedence.Default; + + if (this.IsChoice()) + { + Choices = choices ?? new Dictionary(); + } + } + + [Obsolete("Use Description instead.")] + public string? Documentation => Description; + + public string Name { get; } + + [Obsolete("Use Precedence instead.")] + public TemplateParameterPriority Priority => Precedence.PrecedenceDefinition.ToTemplateParameterPriority(); + + public TemplateParameterPrecedence Precedence { get; init; } = TemplateParameterPrecedence.Default; + + public string Type { get; } + + public bool IsName { get; init; } + + public string? DefaultValue { get; init; } + + public string DataType { get; } + + public string? DefaultIfOptionWithoutValue + { + get => _defaultIfOptionWithoutValue; + init => _defaultIfOptionWithoutValue = value; + } + + public IReadOnlyDictionary? Choices { get; init; } + + public string? Description { get; init; } + + public string? DisplayName { get; init; } + + public bool AllowMultipleValues { get; init; } + + string? IAllowDefaultIfOptionWithoutValue.DefaultIfOptionWithoutValue + { + get => _defaultIfOptionWithoutValue; + set => _defaultIfOptionWithoutValue = value; + } + + public override string ToString() + { + return $"{Name} ({Type})"; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is ITemplateParameter parameter) + { + return Equals(parameter); + } + + return false; + } + + public override int GetHashCode() => Name != null ? Name.GetHashCode() : 0; + + public bool Equals(ITemplateParameter other) => !string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(other.Name) && Name == other.Name; + } + +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateParameterExtensions.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateParameterExtensions.cs new file mode 100644 index 000000000000..b07518c44f05 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/TemplateParameterExtensions.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Utils +{ + /// + /// Extensions helper methods for work with . + /// + public static class TemplateParameterExtensions + { + /// + /// Indicates whether the input parameter is of a choice type. + /// + /// Parameter to be inspected. + /// True if given parameter is of a choice type, false otherwise. + public static bool IsChoice(this ITemplateParameter parameter) + { + return parameter.DataType?.Equals("choice", StringComparison.OrdinalIgnoreCase) ?? false; + } + + /// + /// Splits a string value representing a multi valued parameter (currently applicable only to choices) into atomic tokens. + /// + /// A string representing multi valued parameter. + /// List of atomic string tokens. + public static IReadOnlyList TokenizeMultiValueParameter(this string literal) + { + return literal.Split(MultiValueParameter.MultiValueSeparators, StringSplitOptions.RemoveEmptyEntries); + } + + /// + /// Check a multi valued parameter value (currently applicable only to choices), whether it doesn't contain any disallowed (separator) characters. + /// + /// Parameter value to be checked. + /// True if given value doesn't contain any disallowed characters, false otherwise. + public static bool IsValidMultiValueParameterValue(this string value) + { + return value.IndexOfAny(MultiValueParameter.MultiValueSeparators) == -1; + } + } + +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/Timing.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/Timing.cs new file mode 100644 index 000000000000..6b96bcb9a0e3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/Timing.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace Microsoft.TemplateEngine.Utils +{ + public sealed class Timing : IDisposable + { + private readonly ILogger _logger; + private readonly string _label; + private readonly Stopwatch _stopwatch; + private readonly IDisposable? _disposable; + + public Timing(ILogger logger, string label) + { + _logger = logger; + _label = label; + _stopwatch = Stopwatch.StartNew(); + _disposable = logger.BeginScope(_label); + _logger.LogDebug($"{_label} started"); + } + + public static Timing Over(ILogger logger, string label) + { + return new Timing(logger, label); + } + + public void Dispose() + { + _stopwatch.Stop(); + _logger.LogDebug($"{_label} finished, took {_stopwatch.ElapsedMilliseconds} ms"); + _disposable?.Dispose(); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/VersionStringHelpers.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/VersionStringHelpers.cs new file mode 100644 index 000000000000..45f43538b035 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/VersionStringHelpers.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Utils +{ + public static class VersionStringHelpers + { + public static bool TryParseVersionSpecification(string versionString, out IVersionSpecification? specification) + { + if (string.IsNullOrEmpty(versionString)) + { + specification = null; + return false; + } + else if (versionString.Contains("-")) + { + return RangeVersionSpecification.TryParse(versionString, out specification); + } + else + { + return ExactVersionSpecification.TryParse(versionString, out specification); + } + } + + // returns the relative order of the versions: + // null if either is not a valid version. + // -1 if version1 < version2 + // 0 if version1 == version2 + // 1 if version1 > version2 + public static int? CompareVersions(string? version1, string? version2) + { + if (!TryParseVersionString(version1, out int[]? parts1) || !TryParseVersionString(version2, out int[]? parts2)) + { + return null; + } + + for (int i = 0; i < 4; i++) + { + if (parts1![i] > parts2![i]) + { + return 1; + } + else if (parts1[i] < parts2[i]) + { + return -1; + } + } + + return 0; + } + + public static bool IsVersionWellFormed(string? version) + { + return TryParseVersionString(version, out _); + } + + // tries to parse a version into 4 int parts, zero-padding on the right if needed. + // more than 4 parts, return false. + // Not parse-able, return false. + private static bool TryParseVersionString(string? version, out int[]? parsed) + { + if (string.IsNullOrEmpty(version)) + { + parsed = null; + return false; + } + + string[]? parts = version?.Split(new[] { '.' }); + + if (parts != null) + { + if (parts.Length is < 2 or > 4) + { + parsed = null; + return false; + } + } + + parsed = new[] { 0, 0, 0, 0 }; + + for (int i = 0; i < parts?.Length; i++) + { + if (int.TryParse(parts[i], out int intPart)) + { + parsed[i] = intPart; + } + else + { + parsed = null; + return false; + } + } + + return true; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/WellKnownSearchFilters.cs b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/WellKnownSearchFilters.cs new file mode 100644 index 000000000000..317a8a6f109a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/WellKnownSearchFilters.cs @@ -0,0 +1,258 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.TemplateFiltering; + +namespace Microsoft.TemplateEngine.Utils +{ + /// + /// Collection of the predicates to be used for filtering templates by the most used properties. + /// + public static class WellKnownSearchFilters + { + /// + /// The template should match all filters: should have all dispositions of or . + /// + public static Func MatchesAllCriteria => + t => t.MatchDisposition.Count > 0 && t.MatchDisposition.All(x => x.Kind is MatchKind.Exact or MatchKind.Partial); + + /// + /// The template should match at least one filter: should have at least one disposition of or . + /// + public static Func MatchesAtLeastOneCriteria => + t => t.MatchDisposition.Any(x => x.Kind is MatchKind.Exact or MatchKind.Partial); + + /// + /// The template filter that matches on the following criteria:
+ /// - if is null or empty, adds match disposition with ;
+ /// - if is equal to (case insensitive), adds match disposition with ;
+ /// - if is equal to one of the short names in (case insensitive), adds match disposition with ;
+ /// - if contains (case insensitive), adds match disposition with ;
+ /// - if one of the short names in contains (case insensitive), adds match disposition with ;
+ /// - adds match disposition with otherwise.
+ ///
+ /// the predicate to be used when filtering the templates. + public static Func NameFilter(string name) + { + return (template) => + { + if (string.IsNullOrEmpty(name)) + { + return new MatchInfo(MatchInfo.BuiltIn.Name, name, MatchKind.Partial); + } + + int nameIndex = template.Name.IndexOf(name, StringComparison.CurrentCultureIgnoreCase); + + if (nameIndex == 0 && template.Name.Length == name.Length) + { + return new MatchInfo(MatchInfo.BuiltIn.Name, name, MatchKind.Exact); + } + + bool hasShortNamePartialMatch = false; + + foreach (string shortName in template.ShortNameList) + { + int shortNameIndex = shortName.IndexOf(name, StringComparison.OrdinalIgnoreCase); + + if (shortNameIndex == 0 && shortName.Length == name.Length) + { + return new MatchInfo(MatchInfo.BuiltIn.ShortName, name, MatchKind.Exact); + } + + hasShortNamePartialMatch |= shortNameIndex > -1; + } + + if (nameIndex > -1) + { + return new MatchInfo(MatchInfo.BuiltIn.Name, name, MatchKind.Partial); + } + + if (hasShortNamePartialMatch) + { + return new MatchInfo(MatchInfo.BuiltIn.ShortName, name, MatchKind.Partial); + } + + return new MatchInfo(MatchInfo.BuiltIn.Name, name, MatchKind.Mismatch); + }; + } + + /// + /// The template filter that matches on the following criteria:
+ /// - if is null or empty, does not add match disposition;
+ /// - if is equal to tag named 'type' from (case insensitive), adds match disposition with ;
+ /// - adds match disposition with otherwise.
+ ///
+ /// the predicate to be used when filtering the templates. + public static Func TypeFilter(string? inputType) + { + string? type = inputType?.ToLowerInvariant(); + + return (template) => + { + if (string.IsNullOrEmpty(type)) + { + return null; + } + if (template.GetTemplateType()?.Equals(type, StringComparison.OrdinalIgnoreCase) ?? false) + { + return new MatchInfo(MatchInfo.BuiltIn.Type, type, MatchKind.Exact); + } + else + { + return new MatchInfo(MatchInfo.BuiltIn.Type, type, MatchKind.Mismatch); + } + }; + } + + /// + /// The template filter that matches on the following criteria:
+ /// - if is null or empty, does not add match disposition;
+ /// - if is equal to any entry from (case insensitive), adds match disposition with ;
+ /// - adds match disposition with otherwise.
+ ///
+ /// the predicate to be used when filtering the templates.. + public static Func ClassificationFilter(string? classification) + { + return (template) => + { + if (string.IsNullOrWhiteSpace(classification)) + { + return null; + } + if (template.Classifications?.Contains(classification, StringComparer.CurrentCultureIgnoreCase) ?? false) + { + return new MatchInfo(MatchInfo.BuiltIn.Classification, classification, MatchKind.Exact); + } + return new MatchInfo(MatchInfo.BuiltIn.Classification, classification, MatchKind.Mismatch); + }; + } + + /// + /// The template filter that matches on the following criteria:
+ /// - if is null or empty, does not add match disposition;
+ /// - if is equal to tag named 'language' from (case insensitive), adds match disposition with ;
+ /// - adds match disposition with otherwise.
+ ///
+ /// the predicate to be used when filtering the templates. + public static Func LanguageFilter(string? language) + { + return (template) => + { + if (string.IsNullOrEmpty(language)) + { + return null; + } + + if (template.GetLanguage()?.Equals(language, StringComparison.OrdinalIgnoreCase) ?? false) + { + return new MatchInfo(MatchInfo.BuiltIn.Language, language, MatchKind.Exact); + } + else + { + return new MatchInfo(MatchInfo.BuiltIn.Language, language, MatchKind.Mismatch); + } + }; + } + + /// + /// The template filter that matches on the following criteria:
+ /// - if is null or empty, does not add match disposition;
+ /// - if is equal to key from (case insensitive), adds match disposition with ;
+ /// - adds match disposition with otherwise.
+ ///
+ /// the predicate to be used when filtering the templates. + public static Func BaselineFilter(string? baselineName) + { + return (template) => + { + if (string.IsNullOrEmpty(baselineName)) + { + return null; + } + + if (template.BaselineInfo != null && template.BaselineInfo.ContainsKey(baselineName!)) + { + return new MatchInfo(MatchInfo.BuiltIn.Baseline, baselineName, MatchKind.Exact); + } + else + { + return new MatchInfo(MatchInfo.BuiltIn.Baseline, baselineName, MatchKind.Mismatch); + } + }; + } + + /// + /// The template filter that matches on the following criteria:
+ /// - if is null or empty, does not add match disposition;
+ /// - if is null or empty, adds match disposition with ;
+ /// - if is equal to (case insensitive), adds match disposition with ;
+ /// - if contains (case insensitive), adds match disposition with ;
+ /// - with otherwise.
+ ///
+ /// the predicate to be used when filtering the templates. + public static Func AuthorFilter(string? author) + { + return (template) => + { + if (string.IsNullOrWhiteSpace(author)) + { + return null; + } + + if (string.IsNullOrWhiteSpace(template.Author)) + { + return new MatchInfo(MatchInfo.BuiltIn.Author, author, MatchKind.Mismatch); + } + + int authorIndex = template.Author!.IndexOf(author, StringComparison.CurrentCultureIgnoreCase); + + if (authorIndex == 0 && template.Author.Length == author!.Length) + { + return new MatchInfo(MatchInfo.BuiltIn.Author, author, MatchKind.Exact); + } + + if (authorIndex > -1) + { + return new MatchInfo(MatchInfo.BuiltIn.Author, author, MatchKind.Partial); + } + return new MatchInfo(MatchInfo.BuiltIn.Author, author, MatchKind.Mismatch); + }; + } + + /// + /// Gets the list of template filters for template constraints definition
+ /// For each constraint the template filter will be created:
+ /// - if the constraint is not used in the template, does not add match disposition;
+ /// - if the template meets the constraint, adds match disposition with ;
+ /// - if the template does not meet the constraint or constraint cannot be evaluated, adds match disposition with .
+ /// The match info name used is 'Constraint.<constraint type>'. + ///
+ /// the list of predicates to be used when filtering the templates. + public static IEnumerable> ConstraintFilters(IEnumerable constraintDefinitions) + { + foreach (ITemplateConstraint constraintDefinition in constraintDefinitions) + { + yield return (template) => + { + var matchingConstraints = template.Constraints.Where(c => c.Type == constraintDefinition.Type); + if (!matchingConstraints.Any()) + { + //no constraint of such type defined in the template + return null; + } + foreach (var constraint in matchingConstraints) + { + var result = constraintDefinition.Evaluate(constraint.Args); + if (result.EvaluationStatus != TemplateConstraintResult.Status.Allowed) + { + return new MatchInfo($"{MatchInfo.BuiltIn.Constraint}.{constraintDefinition.Type}", null, MatchKind.Mismatch); + } + } + return new MatchInfo($"{MatchInfo.BuiltIn.Constraint}.{constraintDefinition.Type}", null, MatchKind.Exact); + }; + } + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.cs.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.cs.xlf new file mode 100644 index 000000000000..e70b41ef5e11 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.cs.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + Klíč [{0}] už existuje. + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.de.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.de.xlf new file mode 100644 index 000000000000..9be54b971921 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.de.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + Der Schlüssel [{0}] ist bereits vorhanden. + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.es.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.es.xlf new file mode 100644 index 000000000000..764ab7bdb4ff --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.es.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + La clave [{0}] ya está presente. + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.fr.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.fr.xlf new file mode 100644 index 000000000000..38206115988a --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.fr.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + La clé [{0}] est déjà présente. + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.it.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.it.xlf new file mode 100644 index 000000000000..6d94991b3832 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.it.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + La chiave [{0}] è già presente. + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.ja.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.ja.xlf new file mode 100644 index 000000000000..25587e2c5f39 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.ja.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + キー [{0}] は既に存在します。 + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.ko.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.ko.xlf new file mode 100644 index 000000000000..c50ad6c8dd45 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.ko.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + 키 [{0}]이(가) 이미 있습니다. + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.pl.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.pl.xlf new file mode 100644 index 000000000000..e33e300f4c1b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.pl.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + Klucz [{0}] już istnieje. + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.pt-BR.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.pt-BR.xlf new file mode 100644 index 000000000000..2eaf265583b9 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.pt-BR.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + A chave [{0}] já está presente. + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.ru.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.ru.xlf new file mode 100644 index 000000000000..c61fbb9b27a3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.ru.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + Ключ [{0}] уже имеется. + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.tr.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.tr.xlf new file mode 100644 index 000000000000..e594a300c8df --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.tr.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + [{0}] anahtarı zaten var. + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.zh-Hans.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.zh-Hans.xlf new file mode 100644 index 000000000000..bc2cf0c2e5bd --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.zh-Hans.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + 键 [{0}] 已存在。 + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.zh-Hant.xlf b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.zh-Hant.xlf new file mode 100644 index 000000000000..159b81324677 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.Utils/xlf/LocalizableStrings.zh-Hant.xlf @@ -0,0 +1,12 @@ + + + + + + Key [{0}] is already present. + 索引鍵 [{0}] 已存在。 + {0} contains the actual key + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateEngine.sln b/src/TemplateEngine/Microsoft.TemplateEngine.sln new file mode 100644 index 000000000000..fb6a5b8be009 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateEngine.sln @@ -0,0 +1,652 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31429.26 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7DAC892E-ADAE-4CEB-8A0C-EDC452A5826A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{8B498D0C-F488-4B38-8A7D-B20BF9DB6F60}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Core", "src\Microsoft.TemplateEngine.Core\Microsoft.TemplateEngine.Core.csproj", "{72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Abstractions", "src\Microsoft.TemplateEngine.Abstractions\Microsoft.TemplateEngine.Abstractions.csproj", "{0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Orchestrator.RunnableProjects", "src\Microsoft.TemplateEngine.Orchestrator.RunnableProjects\Microsoft.TemplateEngine.Orchestrator.RunnableProjects.csproj", "{578CE255-E412-4CC7-9A03-1EDCA522DA27}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Core.UnitTests", "test\Microsoft.TemplateEngine.Core.UnitTests\Microsoft.TemplateEngine.Core.UnitTests.csproj", "{62BD1600-18A2-400E-94F5-C33BBD67CA97}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Edge", "src\Microsoft.TemplateEngine.Edge\Microsoft.TemplateEngine.Edge.csproj", "{E22B59BD-A658-4E32-9407-C8D9570B9FDF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Utils", "src\Microsoft.TemplateEngine.Utils\Microsoft.TemplateEngine.Utils.csproj", "{A86924AE-4B77-47A8-A690-EDA395F075A9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Core.Contracts", "src\Microsoft.TemplateEngine.Core.Contracts\Microsoft.TemplateEngine.Core.Contracts.csproj", "{9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Utils.UnitTests", "test\Microsoft.TemplateEngine.Utils.UnitTests\Microsoft.TemplateEngine.Utils.UnitTests.csproj", "{62BD1609-18A2-400E-94F5-C33BBD67CA97}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Mocks", "test\Microsoft.TemplateEngine.Mocks\Microsoft.TemplateEngine.Mocks.csproj", "{865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests", "test\Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests\Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.csproj", "{D9B16B88-4B85-4208-8F1C-39DFB248250C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.TestHelper", "test\Microsoft.TemplateEngine.TestHelper\Microsoft.TemplateEngine.TestHelper.csproj", "{FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.IDE", "src\Microsoft.TemplateEngine.IDE\Microsoft.TemplateEngine.IDE.csproj", "{D26D7FC1-3DCD-434C-8261-63FCCEF27278}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Edge.UnitTests", "test\Microsoft.TemplateEngine.Edge.UnitTests\Microsoft.TemplateEngine.Edge.UnitTests.csproj", "{C416006F-FAE9-4263-8290-81B2AA81E024}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateSearch.Common", "src\Microsoft.TemplateSearch.Common\Microsoft.TemplateSearch.Common.csproj", "{4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateSearch.TemplateDiscovery", "tools\Microsoft.TemplateSearch.TemplateDiscovery\Microsoft.TemplateSearch.TemplateDiscovery.csproj", "{FBEBB725-F645-40DC-856C-D1BC7FB52CF3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.TestTemplates", "test\Microsoft.TemplateEngine.TestTemplates\Microsoft.TemplateEngine.TestTemplates.csproj", "{FC7516FB-7F44-4786-ADF2-589EF06C2EDE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.IDE.IntegrationTests", "test\Microsoft.TemplateEngine.IDE.IntegrationTests\Microsoft.TemplateEngine.IDE.IntegrationTests.csproj", "{0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateSearch.Common.UnitTests", "test\Microsoft.TemplateSearch.Common.UnitTests\Microsoft.TemplateSearch.Common.UnitTests.csproj", "{15304624-1774-4990-A0CC-7B9DA27A8FF6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.CLI", "tools\Microsoft.TemplateEngine.Authoring.CLI\Microsoft.TemplateEngine.Authoring.CLI.csproj", "{F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.TemplateLocalizer.Core", "tools\Microsoft.TemplateEngine.TemplateLocalizer.Core\Microsoft.TemplateEngine.TemplateLocalizer.Core.csproj", "{ED5D6873-220B-4F9D-A30B-B99E950F8E26}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.TemplateLocalizer.Core.UnitTests", "test\Microsoft.TemplateEngine.TemplateLocalizer.Core.UnitTests\Microsoft.TemplateEngine.TemplateLocalizer.Core.UnitTests.csproj", "{858B2E28-1FF8-4ED2-A356-B576BD793B71}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests", "test\Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests\Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests.csproj", "{BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests", "test\Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests\Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests.csproj", "{B0330A2C-3F10-4C46-97DF-13D187564F70}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.Tasks", "tools\Microsoft.TemplateEngine.Authoring.Tasks\Microsoft.TemplateEngine.Authoring.Tasks.csproj", "{BD758B10-A47F-4159-B9A1-997723AF7349}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests", "test\Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests\Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests.csproj", "{2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C5186341-2064-49AA-B398-CDF4302D2823}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + exclusion.dic = exclusion.dic + Directory.Packages.props = Directory.Packages.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{B794BF86-4185-4DCE-AC86-C27D5D966B9B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.TemplateVerifier", "tools\Microsoft.TemplateEngine.Authoring.TemplateVerifier\Microsoft.TemplateEngine.Authoring.TemplateVerifier.csproj", "{12764D81-61A7-437A-90B6-9F245E43F457}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.TemplateVerifier.XunitV3", "tools\Microsoft.TemplateEngine.Authoring.TemplateVerifier.XunitV3\Microsoft.TemplateEngine.Authoring.TemplateVerifier.XunitV3.csproj", "{CC3780CC-83CF-415C-A759-E5D0D4A64768}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests", "test\Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests\Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.csproj", "{B1DDA327-F55E-466A-AF3E-7F039B9B51A9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.XunitV3", "test\Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.XunitV3\Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.XunitV3.csproj", "{3C9F1344-E773-42FF-B98D-BD7277608903}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests", "test\Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests\Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.csproj", "{D478568D-CA20-4331-9019-F585B564425E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.XunitV3", "test\Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.XunitV3\Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.XunitV3.csproj", "{33D7A35E-A457-42FB-B849-3E752A372D23}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.CLI.UnitTests", "test\Microsoft.TemplateEngine.Authoring.CLI.UnitTests\Microsoft.TemplateEngine.Authoring.CLI.UnitTests.csproj", "{E8B9226E-879F-495A-BDAD-2607844D048C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eng", "eng", "{D190251C-5649-4DD6-A158-16D119116352}" + ProjectSection(SolutionItems) = preProject + eng\Versions.props = eng\Versions.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{43053BC4-32B4-4404-B62D-410F367CE0CE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.TemplateEngine.CommandUtils", "Microsoft.TemplateEngine.CommandUtils", "{EE8CD472-D8C4-4CD0-BC84-6C305F5971AE}" + ProjectSection(SolutionItems) = preProject + tools\Shared\Microsoft.TemplateEngine.CommandUtils\ArgumentEscaper.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\ArgumentEscaper.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\BasicCommand.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\BasicCommand.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\Command.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\Command.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\CommandResult.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\CommandResult.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\CommandResultAssertions.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\CommandResultAssertions.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\CommandResultExtensions.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\CommandResultExtensions.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\DotnetCommand.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\DotnetCommand.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\DotnetNewCommand.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\DotnetNewCommand.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\NativeMethods.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\NativeMethods.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\ProcessReaper.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\ProcessReaper.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\SdkCommandSpec.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\SdkCommandSpec.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\StreamForwarder.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\StreamForwarder.cs + tools\Shared\Microsoft.TemplateEngine.CommandUtils\TestCommand.cs = tools\Shared\Microsoft.TemplateEngine.CommandUtils\TestCommand.cs + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.TemplateApiVerifier", "tools\Microsoft.TemplateEngine.Authoring.TemplateApiVerifier\Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.csproj", "{449B9DDA-F18C-411E-9A74-3930652BB78A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.XunitV3", "tools\Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.XunitV3\Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.XunitV3.csproj", "{3F3F695B-537E-44DD-93FE-A1D1B88F4589}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "template-samples", "template-samples", "{3A2D12C2-0455-4471-9EBB-91749BA3A60F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Samples", "dotnet-template-samples\Microsoft.TemplateEngine.Samples.csproj", "{DFB06A25-719F-4C8B-B84D-55D2D601BEF6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "template-feed", "template-feed", "{FF433F83-22C2-46E0-99B3-4FFCA5190A94}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.Templates", "template_feed\Microsoft.TemplateEngine.Authoring.Templates\Microsoft.TemplateEngine.Authoring.Templates.csproj", "{1F74F8F5-99E4-47AE-8608-6D470325E81C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests", "test\Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests\Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests.csproj", "{8651DEEB-0291-4E49-92AA-3B097DAF9D37}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.TemplateEngine.TestHelper.XunitV3", "test\Microsoft.TemplateEngine.TestHelper.XunitV3\Microsoft.TemplateEngine.TestHelper.XunitV3.csproj", "{AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.TemplateEngine.Mocks.XunitV3", "test\Microsoft.TemplateEngine.Mocks.XunitV3\Microsoft.TemplateEngine.Mocks.XunitV3.csproj", "{F7C1932C-49E3-4869-A123-8184534AD1E2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Debug|x64.ActiveCfg = Debug|Any CPU + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Debug|x64.Build.0 = Debug|Any CPU + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Debug|x86.ActiveCfg = Debug|Any CPU + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Debug|x86.Build.0 = Debug|Any CPU + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Release|Any CPU.Build.0 = Release|Any CPU + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Release|x64.ActiveCfg = Release|Any CPU + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Release|x64.Build.0 = Release|Any CPU + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Release|x86.ActiveCfg = Release|Any CPU + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF}.Release|x86.Build.0 = Release|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Debug|x64.ActiveCfg = Debug|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Debug|x64.Build.0 = Debug|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Debug|x86.ActiveCfg = Debug|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Debug|x86.Build.0 = Debug|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Release|Any CPU.Build.0 = Release|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Release|x64.ActiveCfg = Release|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Release|x64.Build.0 = Release|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Release|x86.ActiveCfg = Release|Any CPU + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4}.Release|x86.Build.0 = Release|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Debug|x64.ActiveCfg = Debug|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Debug|x64.Build.0 = Debug|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Debug|x86.ActiveCfg = Debug|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Debug|x86.Build.0 = Debug|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Release|Any CPU.Build.0 = Release|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Release|x64.ActiveCfg = Release|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Release|x64.Build.0 = Release|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Release|x86.ActiveCfg = Release|Any CPU + {578CE255-E412-4CC7-9A03-1EDCA522DA27}.Release|x86.Build.0 = Release|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Debug|x64.ActiveCfg = Debug|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Debug|x64.Build.0 = Debug|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Debug|x86.ActiveCfg = Debug|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Debug|x86.Build.0 = Debug|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Release|Any CPU.Build.0 = Release|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Release|x64.ActiveCfg = Release|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Release|x64.Build.0 = Release|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Release|x86.ActiveCfg = Release|Any CPU + {62BD1600-18A2-400E-94F5-C33BBD67CA97}.Release|x86.Build.0 = Release|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Debug|x64.ActiveCfg = Debug|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Debug|x64.Build.0 = Debug|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Debug|x86.ActiveCfg = Debug|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Debug|x86.Build.0 = Debug|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Release|Any CPU.Build.0 = Release|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Release|x64.ActiveCfg = Release|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Release|x64.Build.0 = Release|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Release|x86.ActiveCfg = Release|Any CPU + {E22B59BD-A658-4E32-9407-C8D9570B9FDF}.Release|x86.Build.0 = Release|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Debug|x64.ActiveCfg = Debug|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Debug|x64.Build.0 = Debug|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Debug|x86.ActiveCfg = Debug|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Debug|x86.Build.0 = Debug|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Release|Any CPU.Build.0 = Release|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Release|x64.ActiveCfg = Release|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Release|x64.Build.0 = Release|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Release|x86.ActiveCfg = Release|Any CPU + {A86924AE-4B77-47A8-A690-EDA395F075A9}.Release|x86.Build.0 = Release|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Debug|x64.ActiveCfg = Debug|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Debug|x64.Build.0 = Debug|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Debug|x86.ActiveCfg = Debug|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Debug|x86.Build.0 = Debug|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Release|Any CPU.Build.0 = Release|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Release|x64.ActiveCfg = Release|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Release|x64.Build.0 = Release|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Release|x86.ActiveCfg = Release|Any CPU + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D}.Release|x86.Build.0 = Release|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Debug|x64.ActiveCfg = Debug|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Debug|x64.Build.0 = Debug|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Debug|x86.ActiveCfg = Debug|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Debug|x86.Build.0 = Debug|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Release|Any CPU.Build.0 = Release|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Release|x64.ActiveCfg = Release|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Release|x64.Build.0 = Release|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Release|x86.ActiveCfg = Release|Any CPU + {62BD1609-18A2-400E-94F5-C33BBD67CA97}.Release|x86.Build.0 = Release|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Debug|x64.ActiveCfg = Debug|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Debug|x64.Build.0 = Debug|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Debug|x86.ActiveCfg = Debug|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Debug|x86.Build.0 = Debug|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Release|Any CPU.Build.0 = Release|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Release|x64.ActiveCfg = Release|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Release|x64.Build.0 = Release|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Release|x86.ActiveCfg = Release|Any CPU + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56}.Release|x86.Build.0 = Release|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Debug|x64.ActiveCfg = Debug|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Debug|x64.Build.0 = Debug|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Debug|x86.ActiveCfg = Debug|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Debug|x86.Build.0 = Debug|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Release|Any CPU.Build.0 = Release|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Release|x64.ActiveCfg = Release|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Release|x64.Build.0 = Release|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Release|x86.ActiveCfg = Release|Any CPU + {D9B16B88-4B85-4208-8F1C-39DFB248250C}.Release|x86.Build.0 = Release|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Debug|x64.ActiveCfg = Debug|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Debug|x64.Build.0 = Debug|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Debug|x86.ActiveCfg = Debug|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Debug|x86.Build.0 = Debug|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Release|Any CPU.Build.0 = Release|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Release|x64.ActiveCfg = Release|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Release|x64.Build.0 = Release|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Release|x86.ActiveCfg = Release|Any CPU + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534}.Release|x86.Build.0 = Release|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Debug|x64.ActiveCfg = Debug|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Debug|x64.Build.0 = Debug|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Debug|x86.ActiveCfg = Debug|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Debug|x86.Build.0 = Debug|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Release|Any CPU.Build.0 = Release|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Release|x64.ActiveCfg = Release|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Release|x64.Build.0 = Release|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Release|x86.ActiveCfg = Release|Any CPU + {D26D7FC1-3DCD-434C-8261-63FCCEF27278}.Release|x86.Build.0 = Release|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Debug|x64.ActiveCfg = Debug|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Debug|x64.Build.0 = Debug|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Debug|x86.ActiveCfg = Debug|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Debug|x86.Build.0 = Debug|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Release|Any CPU.Build.0 = Release|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Release|x64.ActiveCfg = Release|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Release|x64.Build.0 = Release|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Release|x86.ActiveCfg = Release|Any CPU + {C416006F-FAE9-4263-8290-81B2AA81E024}.Release|x86.Build.0 = Release|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Debug|x64.ActiveCfg = Debug|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Debug|x64.Build.0 = Debug|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Debug|x86.ActiveCfg = Debug|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Debug|x86.Build.0 = Debug|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Release|Any CPU.Build.0 = Release|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Release|x64.ActiveCfg = Release|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Release|x64.Build.0 = Release|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Release|x86.ActiveCfg = Release|Any CPU + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B}.Release|x86.Build.0 = Release|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Debug|x64.ActiveCfg = Debug|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Debug|x64.Build.0 = Debug|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Debug|x86.ActiveCfg = Debug|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Debug|x86.Build.0 = Debug|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Release|Any CPU.Build.0 = Release|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Release|x64.ActiveCfg = Release|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Release|x64.Build.0 = Release|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Release|x86.ActiveCfg = Release|Any CPU + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3}.Release|x86.Build.0 = Release|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Debug|x64.ActiveCfg = Debug|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Debug|x64.Build.0 = Debug|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Debug|x86.ActiveCfg = Debug|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Debug|x86.Build.0 = Debug|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Release|Any CPU.Build.0 = Release|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Release|x64.ActiveCfg = Release|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Release|x64.Build.0 = Release|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Release|x86.ActiveCfg = Release|Any CPU + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE}.Release|x86.Build.0 = Release|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Debug|x64.ActiveCfg = Debug|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Debug|x64.Build.0 = Debug|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Debug|x86.ActiveCfg = Debug|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Debug|x86.Build.0 = Debug|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Release|Any CPU.Build.0 = Release|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Release|x64.ActiveCfg = Release|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Release|x64.Build.0 = Release|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Release|x86.ActiveCfg = Release|Any CPU + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78}.Release|x86.Build.0 = Release|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Debug|x64.ActiveCfg = Debug|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Debug|x64.Build.0 = Debug|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Debug|x86.ActiveCfg = Debug|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Debug|x86.Build.0 = Debug|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Release|Any CPU.Build.0 = Release|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Release|x64.ActiveCfg = Release|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Release|x64.Build.0 = Release|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Release|x86.ActiveCfg = Release|Any CPU + {15304624-1774-4990-A0CC-7B9DA27A8FF6}.Release|x86.Build.0 = Release|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Debug|x64.ActiveCfg = Debug|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Debug|x64.Build.0 = Debug|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Debug|x86.ActiveCfg = Debug|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Debug|x86.Build.0 = Debug|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Release|Any CPU.Build.0 = Release|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Release|x64.ActiveCfg = Release|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Release|x64.Build.0 = Release|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Release|x86.ActiveCfg = Release|Any CPU + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403}.Release|x86.Build.0 = Release|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Debug|x64.ActiveCfg = Debug|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Debug|x64.Build.0 = Debug|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Debug|x86.ActiveCfg = Debug|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Debug|x86.Build.0 = Debug|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Release|Any CPU.Build.0 = Release|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Release|x64.ActiveCfg = Release|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Release|x64.Build.0 = Release|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Release|x86.ActiveCfg = Release|Any CPU + {ED5D6873-220B-4F9D-A30B-B99E950F8E26}.Release|x86.Build.0 = Release|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Debug|x64.ActiveCfg = Debug|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Debug|x64.Build.0 = Debug|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Debug|x86.ActiveCfg = Debug|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Debug|x86.Build.0 = Debug|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Release|Any CPU.ActiveCfg = Release|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Release|Any CPU.Build.0 = Release|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Release|x64.ActiveCfg = Release|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Release|x64.Build.0 = Release|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Release|x86.ActiveCfg = Release|Any CPU + {858B2E28-1FF8-4ED2-A356-B576BD793B71}.Release|x86.Build.0 = Release|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Debug|x64.Build.0 = Debug|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Debug|x86.Build.0 = Debug|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Release|Any CPU.Build.0 = Release|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Release|x64.ActiveCfg = Release|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Release|x64.Build.0 = Release|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Release|x86.ActiveCfg = Release|Any CPU + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5}.Release|x86.Build.0 = Release|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Debug|x64.ActiveCfg = Debug|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Debug|x64.Build.0 = Debug|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Debug|x86.ActiveCfg = Debug|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Debug|x86.Build.0 = Debug|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Release|Any CPU.Build.0 = Release|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Release|x64.ActiveCfg = Release|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Release|x64.Build.0 = Release|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Release|x86.ActiveCfg = Release|Any CPU + {B0330A2C-3F10-4C46-97DF-13D187564F70}.Release|x86.Build.0 = Release|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Debug|x64.ActiveCfg = Debug|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Debug|x64.Build.0 = Debug|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Debug|x86.ActiveCfg = Debug|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Debug|x86.Build.0 = Debug|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Release|Any CPU.Build.0 = Release|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Release|x64.ActiveCfg = Release|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Release|x64.Build.0 = Release|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Release|x86.ActiveCfg = Release|Any CPU + {BD758B10-A47F-4159-B9A1-997723AF7349}.Release|x86.Build.0 = Release|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Debug|x64.ActiveCfg = Debug|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Debug|x64.Build.0 = Debug|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Debug|x86.ActiveCfg = Debug|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Debug|x86.Build.0 = Debug|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Release|Any CPU.Build.0 = Release|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Release|x64.ActiveCfg = Release|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Release|x64.Build.0 = Release|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Release|x86.ActiveCfg = Release|Any CPU + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE}.Release|x86.Build.0 = Release|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Debug|x64.ActiveCfg = Debug|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Debug|x64.Build.0 = Debug|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Debug|x86.ActiveCfg = Debug|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Debug|x86.Build.0 = Debug|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Release|Any CPU.Build.0 = Release|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Release|x64.ActiveCfg = Release|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Release|x64.Build.0 = Release|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Release|x86.ActiveCfg = Release|Any CPU + {12764D81-61A7-437A-90B6-9F245E43F457}.Release|x86.Build.0 = Release|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Debug|x64.Build.0 = Debug|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Debug|x86.Build.0 = Debug|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Release|Any CPU.Build.0 = Release|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Release|x64.ActiveCfg = Release|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Release|x64.Build.0 = Release|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Release|x86.ActiveCfg = Release|Any CPU + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9}.Release|x86.Build.0 = Release|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Debug|x64.ActiveCfg = Debug|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Debug|x64.Build.0 = Debug|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Debug|x86.ActiveCfg = Debug|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Debug|x86.Build.0 = Debug|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Release|Any CPU.Build.0 = Release|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Release|x64.ActiveCfg = Release|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Release|x64.Build.0 = Release|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Release|x86.ActiveCfg = Release|Any CPU + {D478568D-CA20-4331-9019-F585B564425E}.Release|x86.Build.0 = Release|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Debug|x64.ActiveCfg = Debug|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Debug|x64.Build.0 = Debug|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Debug|x86.ActiveCfg = Debug|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Debug|x86.Build.0 = Debug|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Release|Any CPU.Build.0 = Release|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Release|x64.ActiveCfg = Release|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Release|x64.Build.0 = Release|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Release|x86.ActiveCfg = Release|Any CPU + {E8B9226E-879F-495A-BDAD-2607844D048C}.Release|x86.Build.0 = Release|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|x64.ActiveCfg = Debug|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|x64.Build.0 = Debug|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|x86.ActiveCfg = Debug|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Debug|x86.Build.0 = Debug|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|Any CPU.Build.0 = Release|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|x64.ActiveCfg = Release|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|x64.Build.0 = Release|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|x86.ActiveCfg = Release|Any CPU + {449B9DDA-F18C-411E-9A74-3930652BB78A}.Release|x86.Build.0 = Release|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Debug|x64.ActiveCfg = Debug|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Debug|x64.Build.0 = Debug|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Debug|x86.ActiveCfg = Debug|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Debug|x86.Build.0 = Debug|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Release|Any CPU.Build.0 = Release|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Release|x64.ActiveCfg = Release|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Release|x64.Build.0 = Release|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Release|x86.ActiveCfg = Release|Any CPU + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6}.Release|x86.Build.0 = Release|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Debug|x64.Build.0 = Debug|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Debug|x86.Build.0 = Debug|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Release|Any CPU.Build.0 = Release|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Release|x64.ActiveCfg = Release|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Release|x64.Build.0 = Release|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Release|x86.ActiveCfg = Release|Any CPU + {1F74F8F5-99E4-47AE-8608-6D470325E81C}.Release|x86.Build.0 = Release|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Debug|x64.ActiveCfg = Debug|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Debug|x64.Build.0 = Debug|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Debug|x86.ActiveCfg = Debug|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Debug|x86.Build.0 = Debug|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Release|Any CPU.Build.0 = Release|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Release|x64.ActiveCfg = Release|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Release|x64.Build.0 = Release|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Release|x86.ActiveCfg = Release|Any CPU + {8651DEEB-0291-4E49-92AA-3B097DAF9D37}.Release|x86.Build.0 = Release|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Debug|x64.Build.0 = Debug|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Debug|x86.Build.0 = Debug|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Release|Any CPU.Build.0 = Release|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Release|x64.ActiveCfg = Release|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Release|x64.Build.0 = Release|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Release|x86.ActiveCfg = Release|Any CPU + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8}.Release|x86.Build.0 = Release|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Debug|x64.ActiveCfg = Debug|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Debug|x64.Build.0 = Debug|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Debug|x86.ActiveCfg = Debug|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Debug|x86.Build.0 = Debug|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Release|Any CPU.Build.0 = Release|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Release|x64.ActiveCfg = Release|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Release|x64.Build.0 = Release|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Release|x86.ActiveCfg = Release|Any CPU + {F7C1932C-49E3-4869-A123-8184534AD1E2}.Release|x86.Build.0 = Release|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Debug|x64.ActiveCfg = Debug|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Debug|x64.Build.0 = Debug|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Debug|x86.ActiveCfg = Debug|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Debug|x86.Build.0 = Debug|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Release|Any CPU.Build.0 = Release|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Release|x64.ActiveCfg = Release|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Release|x64.Build.0 = Release|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Release|x86.ActiveCfg = Release|Any CPU + {CC3780CC-83CF-415C-A759-E5D0D4A64768}.Release|x86.Build.0 = Release|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Debug|x64.ActiveCfg = Debug|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Debug|x64.Build.0 = Debug|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Debug|x86.ActiveCfg = Debug|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Debug|x86.Build.0 = Debug|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Release|Any CPU.Build.0 = Release|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Release|x64.ActiveCfg = Release|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Release|x64.Build.0 = Release|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Release|x86.ActiveCfg = Release|Any CPU + {3F3F695B-537E-44DD-93FE-A1D1B88F4589}.Release|x86.Build.0 = Release|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Debug|x64.ActiveCfg = Debug|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Debug|x64.Build.0 = Debug|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Debug|x86.ActiveCfg = Debug|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Debug|x86.Build.0 = Debug|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Release|Any CPU.Build.0 = Release|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Release|x64.ActiveCfg = Release|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Release|x64.Build.0 = Release|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Release|x86.ActiveCfg = Release|Any CPU + {33D7A35E-A457-42FB-B849-3E752A372D23}.Release|x86.Build.0 = Release|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Debug|x64.ActiveCfg = Debug|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Debug|x64.Build.0 = Debug|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Debug|x86.ActiveCfg = Debug|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Debug|x86.Build.0 = Debug|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Release|Any CPU.Build.0 = Release|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Release|x64.ActiveCfg = Release|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Release|x64.Build.0 = Release|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Release|x86.ActiveCfg = Release|Any CPU + {3C9F1344-E773-42FF-B98D-BD7277608903}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {72213E25-DEA2-4A6F-9FA9-AC03F3DE7DCF} = {7DAC892E-ADAE-4CEB-8A0C-EDC452A5826A} + {0C92EDA7-492F-4CBA-9F36-61932CA5C1F4} = {7DAC892E-ADAE-4CEB-8A0C-EDC452A5826A} + {578CE255-E412-4CC7-9A03-1EDCA522DA27} = {7DAC892E-ADAE-4CEB-8A0C-EDC452A5826A} + {62BD1600-18A2-400E-94F5-C33BBD67CA97} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {E22B59BD-A658-4E32-9407-C8D9570B9FDF} = {7DAC892E-ADAE-4CEB-8A0C-EDC452A5826A} + {A86924AE-4B77-47A8-A690-EDA395F075A9} = {7DAC892E-ADAE-4CEB-8A0C-EDC452A5826A} + {9FCFAD2A-061E-4FC0-848E-F0E8AA03676D} = {7DAC892E-ADAE-4CEB-8A0C-EDC452A5826A} + {62BD1609-18A2-400E-94F5-C33BBD67CA97} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {865B29F0-BDBF-4CF6-A6F2-C41CD2F00A56} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {D9B16B88-4B85-4208-8F1C-39DFB248250C} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {FCB1B0F2-3067-4FE8-8A98-5EC80F38D534} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {D26D7FC1-3DCD-434C-8261-63FCCEF27278} = {7DAC892E-ADAE-4CEB-8A0C-EDC452A5826A} + {C416006F-FAE9-4263-8290-81B2AA81E024} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {4DF4A1B9-F31C-49D1-8FEB-8DB37AEBDC0B} = {7DAC892E-ADAE-4CEB-8A0C-EDC452A5826A} + {FBEBB725-F645-40DC-856C-D1BC7FB52CF3} = {B794BF86-4185-4DCE-AC86-C27D5D966B9B} + {FC7516FB-7F44-4786-ADF2-589EF06C2EDE} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {0A4EBB7D-E75B-4589-9FB3-0CD6A0B47C78} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {15304624-1774-4990-A0CC-7B9DA27A8FF6} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {F8C5BBAA-99E9-46BA-87C9-9FB7F388F403} = {B794BF86-4185-4DCE-AC86-C27D5D966B9B} + {ED5D6873-220B-4F9D-A30B-B99E950F8E26} = {B794BF86-4185-4DCE-AC86-C27D5D966B9B} + {858B2E28-1FF8-4ED2-A356-B576BD793B71} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {BB38681E-BBD1-4E77-BCD5-CDB6E532B2C5} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {B0330A2C-3F10-4C46-97DF-13D187564F70} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {BD758B10-A47F-4159-B9A1-997723AF7349} = {B794BF86-4185-4DCE-AC86-C27D5D966B9B} + {2FFDBB61-8AE8-468B-87D3-0D907D7C2FFE} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {12764D81-61A7-437A-90B6-9F245E43F457} = {B794BF86-4185-4DCE-AC86-C27D5D966B9B} + {B1DDA327-F55E-466A-AF3E-7F039B9B51A9} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {D478568D-CA20-4331-9019-F585B564425E} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {E8B9226E-879F-495A-BDAD-2607844D048C} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {D190251C-5649-4DD6-A158-16D119116352} = {C5186341-2064-49AA-B398-CDF4302D2823} + {43053BC4-32B4-4404-B62D-410F367CE0CE} = {B794BF86-4185-4DCE-AC86-C27D5D966B9B} + {EE8CD472-D8C4-4CD0-BC84-6C305F5971AE} = {43053BC4-32B4-4404-B62D-410F367CE0CE} + {449B9DDA-F18C-411E-9A74-3930652BB78A} = {B794BF86-4185-4DCE-AC86-C27D5D966B9B} + {DFB06A25-719F-4C8B-B84D-55D2D601BEF6} = {3A2D12C2-0455-4471-9EBB-91749BA3A60F} + {1F74F8F5-99E4-47AE-8608-6D470325E81C} = {FF433F83-22C2-46E0-99B3-4FFCA5190A94} + {8651DEEB-0291-4E49-92AA-3B097DAF9D37} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {AB8ED04A-5EBA-42AD-9AB9-8514617B27C8} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + {F7C1932C-49E3-4869-A123-8184534AD1E2} = {8B498D0C-F488-4B38-8A7D-B20BF9DB6F60} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6EA1A508-6033-4538-BF98-7F71B4E297AD} + EndGlobalSection +EndGlobal diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/ITemplatePackageInfo.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/ITemplatePackageInfo.cs new file mode 100644 index 000000000000..0b523ec8bce0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/ITemplatePackageInfo.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateSearch.Common.Abstractions +{ + /// + /// Represents information about template package. + /// + public interface ITemplatePackageInfo + { + /// + /// Gets template package name. + /// + string Name { get; } + + /// + /// Gets template package version. + /// + string? Version { get; } + + /// + /// Gets total number of downloads for the package. + /// Optional, might be 0 in case search provider cannot provide number of downloads. + /// + long TotalDownloads { get; } + + /// + /// Gets the list of template package owners. + /// + IReadOnlyList Owners { get; } + + /// + /// Gets the indication if the package is verified. + /// + /// + /// For NuGet.org 'verified' means that package ID is under reserved namespaces, see . + /// + bool Reserved { get; } + + /// + /// Gets the NuGet package description. + /// + string? Description { get; } + + /// + /// Gets the URL to the package icon. + /// + string? IconUrl { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/ITemplateSearchProvider.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/ITemplateSearchProvider.cs new file mode 100644 index 000000000000..94de7086bba1 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/ITemplateSearchProvider.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateSearch.Common.Abstractions +{ + /// + /// The template search provider searches for the templates at the source. + /// + public interface ITemplateSearchProvider + { + /// + /// Gets that created the provider. + /// + ITemplateSearchProviderFactory Factory { get; } + + /// + /// Searches the source for the available templates that matches the filters. + /// + /// The filter that defines if is a match. + /// The filter that list of templates that are the match inside given . + /// The cancellation token to cancel the operation. + /// The list of packages and matching templates for the source. + Task MatchedTemplates)>> SearchForTemplatePackagesAsync( + Func packFilters, + Func> matchingTemplatesFilter, + CancellationToken cancellationToken); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/ITemplateSearchProviderFactory.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/ITemplateSearchProviderFactory.cs new file mode 100644 index 000000000000..d22900297baa --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/ITemplateSearchProviderFactory.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateSearch.Common.Abstractions +{ + /// + /// The factory to create s. + /// This is a template engine component that can be installed for . + /// + public interface ITemplateSearchProviderFactory : IIdentifiedComponent + { + /// + /// Gets the display name of the component. + /// + string DisplayName { get; } + + /// + /// Creates . + /// + /// template engine environment settings. + /// additional readers to be used to read the information from the source (optional). + /// Created . + ITemplateSearchProvider CreateProvider(IEngineEnvironmentSettings environmentSettings, IReadOnlyDictionary> additionalDataReaders); + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/TemplatePackageSearchData.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/TemplatePackageSearchData.cs new file mode 100644 index 000000000000..20f9b991107f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/TemplatePackageSearchData.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.Common +{ + /// + /// Template package searchable data. + /// + [System.Diagnostics.DebuggerDisplay("{Name}@{Version}")] + public partial class TemplatePackageSearchData : ITemplatePackageInfo + { + public TemplatePackageSearchData(ITemplatePackageInfo packInfo, IEnumerable templates, IDictionary? data = null) + { + if (packInfo is null) + { + throw new ArgumentNullException(nameof(packInfo)); + } + + if (templates is null) + { + throw new ArgumentNullException(nameof(templates)); + } + + Name = !string.IsNullOrWhiteSpace(packInfo.Name) ? packInfo.Name + : throw new ArgumentException($"{nameof(packInfo.Name)} should not be null or empty", nameof(packInfo)); + Version = packInfo.Version; + TotalDownloads = packInfo.TotalDownloads; + Owners = packInfo.Owners; + Reserved = packInfo.Reserved; + Templates = templates.ToList(); + Description = packInfo.Description; + IconUrl = packInfo.IconUrl; + AdditionalData = data ?? new Dictionary(); + } + + /// + public string Name { get; } + + /// + public string? Version { get; } + + /// + public long TotalDownloads { get; } + + /// + public IReadOnlyList Owners { get; } + + /// + public bool Reserved { get; } + + /// + public string? Description { get; } + + /// + public string? IconUrl { get; } + + /// + /// Gets the list of templates in template package. + /// + public IReadOnlyList Templates { get; } + + /// + /// Gets the additional data available for template package. + /// + /// + /// Additional data may be read by additional readers provider to when creating the . + /// + public IDictionary AdditionalData { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/TemplateSearchData.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/TemplateSearchData.cs new file mode 100644 index 000000000000..dfc648358450 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Abstractions/TemplateSearchData.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.Common +{ + /// + /// Template searchable data. + /// + public partial class TemplateSearchData : ITemplateInfo + { + public TemplateSearchData(ITemplateInfo templateInfo, IDictionary? data = null) + { + if (templateInfo is null) + { + throw new ArgumentNullException(nameof(templateInfo)); + } + +#pragma warning disable CS0618 // Type or member is obsolete. The code will be moved to TemplateSearchData.Json when BlobStorageTemplateInfo is ready to be removed. + TemplateInfo = new BlobStorageTemplateInfo(templateInfo); +#pragma warning restore CS0618 // Type or member is obsolete + AdditionalData = data ?? new Dictionary(); + } + + /// + /// Gets the additional data available for template package. + /// + /// + /// Additional data may be read by additional readers provider to when creating the . + /// + public IDictionary AdditionalData { get; } + + /// + public string Identity => TemplateInfo.Identity; + + /// + public string? GroupIdentity => TemplateInfo.GroupIdentity; + + /// + public string Name => TemplateInfo.Name; + + /// + public IReadOnlyList ShortNameList => TemplateInfo.ShortNameList; + + /// + bool ITemplateMetadata.PreferDefaultName => TemplateInfo.PreferDefaultName; + + /// + public string? Author => TemplateInfo.Author; + + /// + public string? Description => TemplateInfo.Description; + + /// + public IReadOnlyList Classifications => TemplateInfo.Classifications; + + /// + public IReadOnlyDictionary TagsCollection => TemplateInfo.TagsCollection; + + /// + public IParameterDefinitionSet ParameterDefinitions => TemplateInfo.ParameterDefinitions; + + /// + [Obsolete("Use ParameterDefinitionSet instead.")] + public IReadOnlyList Parameters => ParameterDefinitions; + + /// + public int Precedence => TemplateInfo.Precedence; + + /// + public string? ThirdPartyNotices => TemplateInfo.ThirdPartyNotices; + + #region implicit ITemplateInfo implementation + string? ITemplateMetadata.DefaultName => TemplateInfo.DefaultName; + + Guid ITemplateLocator.GeneratorId => TemplateInfo.GeneratorId; + + [Obsolete] + string ITemplateInfo.ShortName => TemplateInfo.ShortName; + + [Obsolete] + IReadOnlyDictionary ITemplateInfo.Tags => TemplateInfo.Tags; + + [Obsolete] + IReadOnlyDictionary ITemplateInfo.CacheParameters => TemplateInfo.CacheParameters; + + string ITemplateLocator.MountPointUri => TemplateInfo.MountPointUri; + + string ITemplateLocator.ConfigPlace => TemplateInfo.ConfigPlace; + + string? IExtendedTemplateLocator.LocaleConfigPlace => TemplateInfo.LocaleConfigPlace; + + string? IExtendedTemplateLocator.HostConfigPlace => TemplateInfo.HostConfigPlace; + + IReadOnlyDictionary ITemplateMetadata.BaselineInfo => TemplateInfo.BaselineInfo; + + [Obsolete] + bool ITemplateInfo.HasScriptRunningPostActions { get => TemplateInfo.HasScriptRunningPostActions; set => throw new NotImplementedException(); } + + IReadOnlyList ITemplateMetadata.PostActions => TemplateInfo.PostActions; + + IReadOnlyList ITemplateMetadata.Constraints => TemplateInfo.Constraints; + #endregion + + private ITemplateInfo TemplateInfo { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/Components.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Components.cs new file mode 100644 index 000000000000..5967f04c4c3e --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Components.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateSearch.Common.Abstractions; +using Microsoft.TemplateSearch.Common.Providers; + +namespace Microsoft.TemplateSearch.Common +{ + public static class Components + { + public static IReadOnlyList<(Type Type, IIdentifiedComponent Instance)> AllComponents { get; } = + new (Type Type, IIdentifiedComponent Instance)[] + { + (typeof(ITemplateSearchProviderFactory), new NuGetMetadataSearchProviderFactory()) + }; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/LocalizableStrings.resx b/src/TemplateEngine/Microsoft.TemplateSearch.Common/LocalizableStrings.resx new file mode 100644 index 000000000000..4c8faf24729f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/LocalizableStrings.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Failed to update search cache. + + + Local search cache '{0}' does not exist. + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + + + The template search cache data is not supported. + + + The template search cache data is not valid. + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/Microsoft.TemplateSearch.Common.csproj b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Microsoft.TemplateSearch.Common.csproj new file mode 100644 index 000000000000..670d6633b999 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Microsoft.TemplateSearch.Common.csproj @@ -0,0 +1,47 @@ + + + + $(NetMinimum);$(NetCurrent);$(NetFrameworkMinimum) + Components used by the template discovery tool, and also used for related functionality in the CLI. + true + true + true + + true + + + + + + annotations + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/Providers/NuGetMetadataSearchProvider.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Providers/NuGetMetadataSearchProvider.cs new file mode 100644 index 000000000000..020c561249b0 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Providers/NuGetMetadataSearchProvider.cs @@ -0,0 +1,252 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +#if NETFRAMEWORK +using System.Net.Http; +#endif +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.Common.Providers +{ + internal class NuGetMetadataSearchProvider : ITemplateSearchProvider + { + private const string TemplateDiscoveryMetadataFile = "nugetTemplateSearchInfo.json"; + private const int CachedFileValidityInHours = 1; + private const string ETagFileSuffix = ".etag"; + private const string ETagHeaderName = "ETag"; + private const string IfNoneMatchHeaderName = "If-None-Match"; + private const string LocalSourceSearchFileOverrideEnvVar = "DOTNET_NEW_SEARCH_FILE_OVERRIDE"; + private const string UseLocalSearchFileIfPresentEnvVar = "DOTNET_NEW_LOCAL_SEARCH_FILE_ONLY"; + + private readonly IReadOnlyDictionary> _additionalDataReaders = new Dictionary>(); + private readonly ILogger _logger; + private readonly IEngineEnvironmentSettings _environmentSettings; + private readonly Uri[] _searchMetadataUris = + { + new Uri("https://go.microsoft.com/fwlink/?linkid=2168770&clcid=0x409"), //v2 search cache + new Uri("https://go.microsoft.com/fwlink/?linkid=2087906&clcid=0x409") //v1 search cache + }; + + private TemplateSearchCache? _searchCache; + + internal NuGetMetadataSearchProvider( + ITemplateSearchProviderFactory factory, + IEngineEnvironmentSettings environmentSettings, + IReadOnlyDictionary> additionalDataReaders) + { + Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + _environmentSettings = environmentSettings ?? throw new ArgumentNullException(nameof(environmentSettings)); + _additionalDataReaders = additionalDataReaders ?? throw new ArgumentNullException(nameof(additionalDataReaders)); + _logger = _environmentSettings.Host.LoggerFactory.CreateLogger(); + } + + /// + /// Test constructor allowing override search cache Uris. + /// + internal NuGetMetadataSearchProvider( + ITemplateSearchProviderFactory factory, + IEngineEnvironmentSettings environmentSettings, + IReadOnlyDictionary> additionalDataReaders, + IEnumerable searchCacheUri) : this(factory, environmentSettings, additionalDataReaders) + { + _searchMetadataUris = searchCacheUri.Select(s => new Uri(s)).ToArray(); + } + + public ITemplateSearchProviderFactory Factory { get; } + + public async Task MatchedTemplates)>> SearchForTemplatePackagesAsync( + Func packFilter, + Func> matchingTemplatesFilter, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_searchCache == null) + { + _logger.LogDebug("Initializing search cache..."); + string metadataLocation = await GetSearchFileAsync(cancellationToken).ConfigureAwait(false); + _searchCache = TemplateSearchCache.FromJObject(_environmentSettings.Host.FileSystem.ReadObject(metadataLocation), _logger, _additionalDataReaders); + _logger.LogDebug("Search cache was successfully setup."); + } + + IEnumerable filteredPackages = _searchCache.TemplatePackages.Where(package => packFilter(package)); + _logger.LogDebug("Retrieved {0} packages matching package search criteria.", filteredPackages.Count()); + + List<(ITemplatePackageInfo PackageInfo, IReadOnlyList MatchedTemplates)> matchingTemplates = filteredPackages + .Select MatchedTemplates)>(package => (package, matchingTemplatesFilter(package))) + .Where(result => result.MatchedTemplates.Any()) + .ToList(); + + _logger.LogDebug("Retrieved {0} packages matching template search criteria.", matchingTemplates.Count); + return matchingTemplates; + } + + internal async Task GetSearchFileAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + string? localOverridePath = _environmentSettings.Environment.GetEnvironmentVariable(LocalSourceSearchFileOverrideEnvVar); + if (!string.IsNullOrEmpty(localOverridePath)) + { + _logger.LogDebug("{0} is set to {1}, the search file will be loaded from this location instead.", LocalSourceSearchFileOverrideEnvVar, localOverridePath); + if (_environmentSettings.Host.FileSystem.FileExists(localOverridePath!)) + { + return localOverridePath!; + } + _logger.LogDebug("Failed to load search cache from defined location: file {0} does not exist.", localOverridePath); + throw new Exception(string.Format(LocalizableStrings.BlobStoreSourceFileProvider_Exception_LocalCacheDoesNotExist, localOverridePath)); + } + + string preferredMetadataLocation = Path.Combine(_environmentSettings.Paths.HostVersionSettingsDir, TemplateDiscoveryMetadataFile); + _logger.LogDebug("Search cache file location: {0}.", preferredMetadataLocation); + string? useLocalSearchFile = _environmentSettings.Environment.GetEnvironmentVariable(UseLocalSearchFileIfPresentEnvVar); + if (!string.IsNullOrEmpty(useLocalSearchFile)) + { + _logger.LogDebug("{0} is set to {1}, downloading of the search cache will be skipped.", UseLocalSearchFileIfPresentEnvVar, useLocalSearchFile); + // evn var is set, only use a local copy of the search file. Don't try to acquire one from blob storage. + if (_environmentSettings.Host.FileSystem.FileExists(preferredMetadataLocation)) + { + return preferredMetadataLocation; + } + else + { + _logger.LogDebug("Failed to load existing search cache: file {0} does not exist.", preferredMetadataLocation); + throw new Exception(string.Format(LocalizableStrings.BlobStoreSourceFileProvider_Exception_LocalCacheDoesNotExist, preferredMetadataLocation)); + } + } + else + { + _logger.LogDebug("Updating the search cache..."); + // prefer a search file from cloud storage. + // only download the file if it's been long enough since the last time it was downloaded. + if (ShouldDownloadFileFromCloud(preferredMetadataLocation)) + { + await AcquireFileFromCloudAsync(preferredMetadataLocation, cancellationToken).ConfigureAwait(false); + } + return preferredMetadataLocation; + } + } + + private bool ShouldDownloadFileFromCloud(string metadataFileTargetLocation) + { + _logger.LogDebug("Checking the age of search cache..."); + if (_environmentSettings.Host.FileSystem.FileExists(metadataFileTargetLocation)) + { + DateTime utcNow = DateTime.UtcNow; + DateTime lastWriteTimeUtc = _environmentSettings.Host.FileSystem.GetLastWriteTimeUtc(metadataFileTargetLocation); + _logger.LogDebug("The search cache was updated on {0}", lastWriteTimeUtc); + if (lastWriteTimeUtc.AddHours(CachedFileValidityInHours) > utcNow) + { + _logger.LogDebug("The search cache was updated less than {0} hours ago, the update will be skipped.", CachedFileValidityInHours); + return false; + } + _logger.LogDebug("The search cache was updated more than {0} hours ago, and needs to be updated.", CachedFileValidityInHours); + return true; + } + _logger.LogDebug("The search cache file {0} doesn't exist, and needs to be created.", metadataFileTargetLocation); + return true; + } + + /// + /// Attempt to get the search metadata file from cloud storage and place it in the expected search location. + /// Return true on success, false on failure. + /// Implement If-None-Match/ETag headers to avoid re-downloading the same content over and over again. + /// + /// + /// + /// + private async Task AcquireFileFromCloudAsync(string searchMetadataFileLocation, CancellationToken cancellationToken) + { + List exceptionsOccurred = new(); + foreach (Uri searchMetadataUri in _searchMetadataUris) + { + _logger.LogDebug("Retrieving cache file from {0} ...", searchMetadataUri); + cancellationToken.ThrowIfCancellationRequested(); + try + { + HttpClientHandler handler = new() + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + CheckCertificateRevocationList = true + }; + using HttpClient client = new(handler); + string etagFileLocation = searchMetadataFileLocation + ETagFileSuffix; + if (_environmentSettings.Host.FileSystem.FileExists(etagFileLocation)) + { + string etagValue = _environmentSettings.Host.FileSystem.ReadAllText(etagFileLocation); + client.DefaultRequestHeaders.Add(IfNoneMatchHeaderName, $"\"{etagValue}\""); + } + client.DefaultRequestHeaders.Add(IfNoneMatchHeaderName, string.Empty); + using HttpResponseMessage response = await client.GetAsync(searchMetadataUri, cancellationToken).ConfigureAwait(false); + _logger.LogDebug(GetResponseDetails(response)); + if (response.IsSuccessStatusCode) + { +#if NET + string resultText = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); +#else + string resultText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); +#endif + _environmentSettings.Host.FileSystem.WriteAllText(searchMetadataFileLocation, resultText); + _logger.LogDebug("Search cache file was successfully downloaded to {0}.", searchMetadataFileLocation); + if (response.Headers.TryGetValues(ETagHeaderName, out IEnumerable etagValues)) + { + if (etagValues.Count() == 1) + { + _environmentSettings.Host.FileSystem.WriteAllText(etagFileLocation, etagValues.First()); + } + _logger.LogDebug("ETag {0} was written to {1}.", etagValues.First(), etagFileLocation); + } + return; + } + else if (response.StatusCode == HttpStatusCode.NotModified) + { + _logger.LogDebug("Search cache file is not modified, updating the last modified date to now."); + _environmentSettings.Host.FileSystem.SetLastWriteTimeUtc(searchMetadataFileLocation, DateTime.UtcNow); + return; + } + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception e) + { + _logger.LogDebug("Failed to download {0}, details: {1}", searchMetadataUri, e); + exceptionsOccurred.Add(e); + } + } + + if (_environmentSettings.Host.FileSystem.FileExists(searchMetadataFileLocation)) + { + _logger.LogDebug("Failed to update search cache, {0} will be used instead.", searchMetadataFileLocation); + _environmentSettings.Host.Logger.LogWarning(LocalizableStrings.BlobStoreSourceFileProvider_Warning_LocalCacheWillBeUsed); + } + else + { + _logger.LogDebug("Failed to update search cache from all known locations."); + throw new AggregateException(LocalizableStrings.BlobStoreSourceFileProvider_Exception_FailedToUpdateCache, exceptionsOccurred); + } + } + + private string GetResponseDetails(HttpResponseMessage response) + { + StringBuilder message = new(); + + _ = message.AppendLine($"Status code: {response.StatusCode}").AppendLine("Headers:"); + foreach (KeyValuePair> header in response.Content.Headers) + { + _ = message.AppendLine($" {header.Key}: {string.Join(", ", header.Value)}"); + } + foreach (KeyValuePair> header in response.Headers) + { + _ = message.AppendLine($" {header.Key}: {string.Join(", ", header.Value)}"); + } + return message.ToString(); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/Providers/NuGetMetadataSearchProviderFactory.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Providers/NuGetMetadataSearchProviderFactory.cs new file mode 100644 index 000000000000..bf1a5398a4e2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/Providers/NuGetMetadataSearchProviderFactory.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.Common.Providers +{ + public sealed class NuGetMetadataSearchProviderFactory : ITemplateSearchProviderFactory + { + string ITemplateSearchProviderFactory.DisplayName => "NuGet.org"; + + Guid IIdentifiedComponent.Id => new Guid("6EA368C4-8A56-444C-91D1-55150B296BF2"); + + ITemplateSearchProvider ITemplateSearchProviderFactory.CreateProvider( + IEngineEnvironmentSettings environmentSettings, + IReadOnlyDictionary> additionalDataReaders) + { + if (environmentSettings is null) + { + throw new ArgumentNullException(nameof(environmentSettings)); + } + + if (additionalDataReaders is null) + { + throw new ArgumentNullException(nameof(additionalDataReaders)); + } + + return new NuGetMetadataSearchProvider(this, environmentSettings, additionalDataReaders); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/PublicAPI.Shipped.txt b/src/TemplateEngine/Microsoft.TemplateSearch.Common/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..4b7fd6249eaf --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/PublicAPI.Shipped.txt @@ -0,0 +1,51 @@ +#nullable enable +Microsoft.TemplateSearch.Common.Abstractions.ITemplatePackageInfo +Microsoft.TemplateSearch.Common.Abstractions.ITemplatePackageInfo.Name.get -> string! +Microsoft.TemplateSearch.Common.Abstractions.ITemplatePackageInfo.Owners.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateSearch.Common.Abstractions.ITemplatePackageInfo.TotalDownloads.get -> long +Microsoft.TemplateSearch.Common.Abstractions.ITemplatePackageInfo.Version.get -> string? +Microsoft.TemplateSearch.Common.Abstractions.ITemplateSearchProvider +Microsoft.TemplateSearch.Common.Abstractions.ITemplateSearchProvider.Factory.get -> Microsoft.TemplateSearch.Common.Abstractions.ITemplateSearchProviderFactory! +Microsoft.TemplateSearch.Common.Abstractions.ITemplateSearchProvider.SearchForTemplatePackagesAsync(System.Func! packFilters, System.Func!>! matchingTemplatesFilter, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! MatchedTemplates)>!>! +Microsoft.TemplateSearch.Common.Abstractions.ITemplateSearchProviderFactory +Microsoft.TemplateSearch.Common.Abstractions.ITemplateSearchProviderFactory.CreateProvider(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, System.Collections.Generic.IReadOnlyDictionary!>! additionalDataReaders) -> Microsoft.TemplateSearch.Common.Abstractions.ITemplateSearchProvider! +Microsoft.TemplateSearch.Common.Abstractions.ITemplateSearchProviderFactory.DisplayName.get -> string! +Microsoft.TemplateSearch.Common.Components +Microsoft.TemplateSearch.Common.SearchResult +Microsoft.TemplateSearch.Common.SearchResult.ErrorMessage.get -> string? +Microsoft.TemplateSearch.Common.SearchResult.Provider.get -> Microsoft.TemplateSearch.Common.Abstractions.ITemplateSearchProvider! +Microsoft.TemplateSearch.Common.SearchResult.SearchHits.get -> System.Collections.Generic.IReadOnlyList<(Microsoft.TemplateSearch.Common.Abstractions.ITemplatePackageInfo! PackageInfo, System.Collections.Generic.IReadOnlyList! MatchedTemplates)>! +Microsoft.TemplateSearch.Common.SearchResult.Success.get -> bool +Microsoft.TemplateSearch.Common.TemplatePackageSearchData +Microsoft.TemplateSearch.Common.TemplatePackageSearchData.AdditionalData.get -> System.Collections.Generic.IDictionary! +Microsoft.TemplateSearch.Common.TemplatePackageSearchData.Name.get -> string! +Microsoft.TemplateSearch.Common.TemplatePackageSearchData.Owners.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateSearch.Common.TemplatePackageSearchData.TemplatePackageSearchData(Microsoft.TemplateSearch.Common.Abstractions.ITemplatePackageInfo! packInfo, System.Collections.Generic.IEnumerable! templates, System.Collections.Generic.IDictionary? data = null) -> void +Microsoft.TemplateSearch.Common.TemplatePackageSearchData.Templates.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateSearch.Common.TemplatePackageSearchData.TotalDownloads.get -> long +Microsoft.TemplateSearch.Common.TemplatePackageSearchData.Version.get -> string? +Microsoft.TemplateSearch.Common.TemplateSearchCoordinator +Microsoft.TemplateSearch.Common.TemplateSearchCoordinator.SearchAsync(System.Func! packFilter, System.Func!>! matchingTemplatesFilter, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +Microsoft.TemplateSearch.Common.TemplateSearchCoordinator.TemplateSearchCoordinator(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, System.Collections.Generic.IReadOnlyDictionary!>? additionalDataReaders = null) -> void +Microsoft.TemplateSearch.Common.TemplateSearchData +Microsoft.TemplateSearch.Common.TemplateSearchData.AdditionalData.get -> System.Collections.Generic.IDictionary! +Microsoft.TemplateSearch.Common.TemplateSearchData.Author.get -> string? +Microsoft.TemplateSearch.Common.TemplateSearchData.Classifications.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateSearch.Common.TemplateSearchData.Description.get -> string? +Microsoft.TemplateSearch.Common.TemplateSearchData.GroupIdentity.get -> string? +Microsoft.TemplateSearch.Common.TemplateSearchData.Identity.get -> string! +Microsoft.TemplateSearch.Common.TemplateSearchData.Name.get -> string! +Microsoft.TemplateSearch.Common.TemplateSearchData.Parameters.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateSearch.Common.TemplateSearchData.Precedence.get -> int +Microsoft.TemplateSearch.Common.TemplateSearchData.ShortNameList.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateSearch.Common.TemplateSearchData.TagsCollection.get -> System.Collections.Generic.IReadOnlyDictionary! +Microsoft.TemplateSearch.Common.TemplateSearchData.TemplateSearchData(Microsoft.TemplateEngine.Abstractions.ITemplateInfo! templateInfo, System.Collections.Generic.IDictionary? data = null) -> void +Microsoft.TemplateSearch.Common.TemplateSearchData.ThirdPartyNotices.get -> string? +static Microsoft.TemplateSearch.Common.Components.AllComponents.get -> System.Collections.Generic.IReadOnlyList<(System.Type! Type, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent! Instance)>! +Microsoft.TemplateSearch.Common.Abstractions.ITemplatePackageInfo.Description.get -> string? +Microsoft.TemplateSearch.Common.Abstractions.ITemplatePackageInfo.IconUrl.get -> string? +Microsoft.TemplateSearch.Common.Providers.NuGetMetadataSearchProviderFactory +Microsoft.TemplateSearch.Common.Providers.NuGetMetadataSearchProviderFactory.NuGetMetadataSearchProviderFactory() -> void +Microsoft.TemplateSearch.Common.TemplatePackageSearchData.Description.get -> string? +Microsoft.TemplateSearch.Common.TemplatePackageSearchData.IconUrl.get -> string? +Microsoft.TemplateSearch.Common.TemplateSearchData.ParameterDefinitions.get -> Microsoft.TemplateEngine.Abstractions.Parameters.IParameterDefinitionSet! diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/PublicAPI.Unshipped.txt b/src/TemplateEngine/Microsoft.TemplateSearch.Common/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..8f796aa55e1d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +Microsoft.TemplateSearch.Common.Abstractions.ITemplatePackageInfo.Reserved.get -> bool +Microsoft.TemplateSearch.Common.TemplatePackageSearchData.Reserved.get -> bool \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/SearchResult.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/SearchResult.cs new file mode 100644 index 000000000000..110a3a0e60a1 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/SearchResult.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.Common +{ + /// + /// Results of the search performed by . + /// + public class SearchResult + { + internal SearchResult( + ITemplateSearchProvider provider, + bool success, + string? errorMessage = null, + IReadOnlyList<(ITemplatePackageInfo, IReadOnlyList)>? hits = null) + { + if (!success && string.IsNullOrWhiteSpace(errorMessage)) + { + throw new ArgumentException($"{nameof(errorMessage)} cannot be empty when {nameof(success)} is false", nameof(errorMessage)); + } + if (success && hits == null) + { + throw new ArgumentException($"{nameof(hits)} cannot be null when {nameof(success)} is true", nameof(hits)); + } + Provider = provider ?? throw new ArgumentNullException(nameof(provider)); + Success = success; + ErrorMessage = errorMessage; + SearchHits = success ? hits! : []; + } + + /// + /// Gets that performed the search. + /// + public ITemplateSearchProvider Provider { get; } + + /// + /// if search completed successfully. + /// + public bool Success { get; } + + /// + /// Gets localized error message if the search failed. + /// + public string? ErrorMessage { get; } + + /// + /// Gets search hits returned by . + /// + public IReadOnlyList<(ITemplatePackageInfo PackageInfo, IReadOnlyList MatchedTemplates)> SearchHits { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/BlobStorageTemplateInfo.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/BlobStorageTemplateInfo.cs new file mode 100644 index 000000000000..bdecfd54df46 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/BlobStorageTemplateInfo.cs @@ -0,0 +1,491 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.TemplateEngine; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.Parameters; + +namespace Microsoft.TemplateSearch.Common +{ + [Obsolete("The class is deprecated. Use TemplateSearchCache instead to create search cache data. Deserialization code to be moved to TemplateSearchData.Json.")] + internal class BlobStorageTemplateInfo : ITemplateInfo + { + public BlobStorageTemplateInfo(ITemplateInfo templateInfo) + { + if (templateInfo is null) + { + throw new ArgumentNullException(nameof(templateInfo)); + } + if (string.IsNullOrWhiteSpace(templateInfo.Identity)) + { + throw new ArgumentException($"'{nameof(templateInfo.Identity)}' cannot be null or whitespace.", nameof(templateInfo)); + } + + if (string.IsNullOrWhiteSpace(templateInfo.Name)) + { + throw new ArgumentException($"'{nameof(templateInfo.Name)}' cannot be null or whitespace.", nameof(templateInfo)); + } + + if (!templateInfo.ShortNameList.Any()) + { + throw new ArgumentException($"'{nameof(templateInfo.ShortNameList)}' should have at least one entry", nameof(templateInfo)); + } + + Identity = templateInfo.Identity; + Name = templateInfo.Name; + ShortNameList = templateInfo.ShortNameList; + ParameterDefinitions = new ParameterDefinitionSet(templateInfo.ParameterDefinitions?.Select(p => new BlobTemplateParameter(p))); + Author = templateInfo.Author; + Classifications = templateInfo.Classifications ?? []; + Description = templateInfo.Description; + GroupIdentity = templateInfo.GroupIdentity; + Precedence = templateInfo.Precedence; + ThirdPartyNotices = templateInfo.ThirdPartyNotices; + TagsCollection = templateInfo.TagsCollection ?? new Dictionary(); + BaselineInfo = templateInfo.BaselineInfo ?? new Dictionary(); + PostActions = templateInfo.PostActions; + } + + [System.Text.Json.Serialization.JsonConstructor] + private BlobStorageTemplateInfo(string identity, string name, IEnumerable shortNameList) + { + if (string.IsNullOrWhiteSpace(identity)) + { + throw new ArgumentException($"'{nameof(identity)}' cannot be null or whitespace.", nameof(identity)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or whitespace.", nameof(name)); + } + + if (!shortNameList.Any()) + { + throw new ArgumentException($"'{nameof(shortNameList)}' should have at least one entry", nameof(shortNameList)); + } + + Identity = identity; + Name = name; + ShortNameList = shortNameList.ToList(); + } + + [JsonPropertyName("Parameters")] + //reading manually now to support old format + public IParameterDefinitionSet ParameterDefinitions { get; private set; } = ParameterDefinitionSet.Empty; + + [JsonIgnore] + [Obsolete("Use ParameterDefinitionSet instead.")] + public IReadOnlyList Parameters => ParameterDefinitions; + + [JsonIgnore] + string ITemplateLocator.MountPointUri => string.Empty; + + public string? Author { get; private set; } + + public IReadOnlyList Classifications { get; private set; } = new List(); + + [JsonIgnore] + public string DefaultName => string.Empty; + + public string? Description { get; private set; } + + public string Identity { get; private set; } + + [JsonIgnore] + Guid ITemplateLocator.GeneratorId => Guid.Empty; + + public string? GroupIdentity { get; private set; } + + public int Precedence { get; private set; } + + public string Name { get; private set; } + + [JsonIgnore] + [Obsolete("Use ShortNameList instead.")] + string ITemplateInfo.ShortName => ShortNameList.Count > 0 ? ShortNameList[0] : string.Empty; + + public IReadOnlyList ShortNameList { get; private set; } + + [JsonIgnore] + public bool PreferDefaultName { get; private set; } + + [JsonIgnore] + [Obsolete] + public IReadOnlyDictionary Tags { get; private set; } = new Dictionary(); + + [JsonIgnore] + [Obsolete] + public IReadOnlyDictionary CacheParameters { get; private set; } = new Dictionary(); + + [JsonIgnore] + string ITemplateLocator.ConfigPlace => string.Empty; + + [JsonIgnore] + string IExtendedTemplateLocator.LocaleConfigPlace => string.Empty; + + [JsonIgnore] + string IExtendedTemplateLocator.HostConfigPlace => string.Empty; + + public string? ThirdPartyNotices { get; private set; } + + public IReadOnlyDictionary BaselineInfo { get; private set; } = new Dictionary(); + + [JsonIgnore] + [Obsolete("This property is deprecated")] + bool ITemplateInfo.HasScriptRunningPostActions { get; set; } + + public IReadOnlyDictionary TagsCollection { get; private set; } = new Dictionary(); + + public IReadOnlyList PostActions { get; private set; } = []; + + [JsonIgnore] + IReadOnlyList ITemplateMetadata.Constraints => []; + + public static BlobStorageTemplateInfo FromJObject(JsonObject entry) + { + string identity = entry.ToString(nameof(Identity)) + ?? throw new ArgumentException($"{nameof(entry)} doesn't have {nameof(Identity)} property.", nameof(entry)); + string name = entry.ToString(nameof(Name)) + ?? throw new ArgumentException($"{nameof(entry)} doesn't have {nameof(Name)} property.", nameof(entry)); + + JsonNode? shortNameToken = entry.Get(nameof(ShortNameList)); + IEnumerable shortNames = shortNameToken?.JTokenStringOrArrayToCollection([]) + ?? throw new ArgumentException($"{nameof(entry)} doesn't have {nameof(ShortNameList)} property.", nameof(entry)); + + BlobStorageTemplateInfo info = new BlobStorageTemplateInfo(identity, name, shortNames) + { + Author = entry.ToString(nameof(Author)) + }; + JsonArray? classificationsArray = entry.Get(nameof(Classifications)); + if (classificationsArray != null) + { + List classifications = new List(); + foreach (JsonNode? item in classificationsArray) + { + classifications.Add(item?.ToString() ?? string.Empty); + } + info.Classifications = classifications; + } + info.Description = entry.ToString(nameof(Description)); + info.GroupIdentity = entry.ToString(nameof(GroupIdentity)); + info.Precedence = entry.ToInt32(nameof(Precedence)); + info.ThirdPartyNotices = entry.ToString(nameof(ThirdPartyNotices)); + + JsonObject? baselineJObject = entry.Get(nameof(ITemplateInfo.BaselineInfo)); + Dictionary baselineInfo = new Dictionary(); + if (baselineJObject != null) + { + foreach (var item in baselineJObject) + { + IBaselineInfo baseline = new BaselineCacheInfo() + { + Description = item.Value.ToString(nameof(IBaselineInfo.Description)), + DefaultOverrides = item.Value?.ToStringDictionary(propertyName: nameof(IBaselineInfo.DefaultOverrides)) ?? new Dictionary() + }; + baselineInfo.Add(item.Key, baseline); + } + info.BaselineInfo = baselineInfo; + } + + JsonArray? postActionsArray = entry.Get(nameof(info.PostActions)); + if (postActionsArray != null) + { + List postActions = new List(); + foreach (JsonNode? item in postActionsArray) + { + if (Guid.TryParse(item?.ToString(), out Guid id)) + { + postActions.Add(id); + } + } + info.PostActions = postActions; + } + + //read parameters + bool readParameters = false; + List templateParameters = new List(); + JsonArray? parametersArray = entry.Get(nameof(Parameters)); + if (parametersArray != null) + { + foreach (JsonNode? item in parametersArray) + { + if (item is JsonObject jObj) + { + templateParameters.Add(new BlobTemplateParameter(jObj)); + } + } + readParameters = true; + } + + JsonObject? tagsObject = entry.Get(nameof(TagsCollection)); + Dictionary tags = new Dictionary(); + if (tagsObject != null) + { + foreach (var item in tagsObject) + { + tags.Add(item.Key, item.Value?.ToString() ?? string.Empty); + } + } + + //try read tags and parameters - for compatibility reason + tagsObject = entry.Get("tags"); + if (tagsObject != null) + { + Dictionary legacyTags = new Dictionary(); + foreach (var item in tagsObject) + { + if (item.Value is JsonValue jv && jv.GetValueKind() == JsonValueKind.String) + { + tags[item.Key] = item.Value.ToString(); + legacyTags[item.Key] = new BlobLegacyCacheTag( + description: null, + choicesAndDescriptions: new Dictionary() + { + { item.Value.ToString(), string.Empty } + }, + defaultValue: item.Value.ToString(), + defaultIfOptionWithoutValue: null); + } + else if (item.Value is JsonObject tagObj) + { + JsonObject? choicesObject = tagObj.Get("ChoicesAndDescriptions"); + if (choicesObject != null && !readParameters) + { + Dictionary choicesAndDescriptions = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary legacyChoices = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var cdPair in choicesObject) + { + choicesAndDescriptions[cdPair.Key] = new ParameterChoice(null, cdPair.Value?.ToString() ?? string.Empty); + legacyChoices[cdPair.Key] = cdPair.Value?.ToString() ?? string.Empty; + } + templateParameters.Add( + new BlobTemplateParameter(item.Key, "choice") + { + Choices = choicesAndDescriptions + }); + legacyTags[item.Key] = new BlobLegacyCacheTag( + description: tagObj.ToString("description"), + choicesAndDescriptions: legacyChoices, + defaultValue: tagObj.ToString("defaultValue"), + defaultIfOptionWithoutValue: tagObj.ToString("defaultIfOptionWithoutValue")); + } + tags[item.Key] = tagObj.ToString("defaultValue") ?? string.Empty; + } + } + info.Tags = legacyTags; + } + JsonObject? cacheParametersObject = entry.Get("cacheParameters"); + if (!readParameters && cacheParametersObject != null) + { + Dictionary legacyParams = new Dictionary(); + foreach (var item in cacheParametersObject) + { + if (item.Value is not JsonObject paramObj) + { + continue; + } + string dataType = paramObj.ToString(nameof(BlobTemplateParameter.DataType)) ?? "string"; + templateParameters.Add(new BlobTemplateParameter(item.Key, dataType)); + legacyParams[item.Key] = new BlobLegacyCacheParameter( + description: paramObj.ToString("description"), + dataType: paramObj.ToString(nameof(BlobTemplateParameter.DataType)) ?? "string", + defaultValue: paramObj.ToString("defaultValue"), + defaultIfOptionWithoutValue: paramObj.ToString("defaultIfOptionWithoutValue")); + } + info.CacheParameters = legacyParams; + } + + info.TagsCollection = tags; + info.ParameterDefinitions = new ParameterDefinitionSet(templateParameters); + return info; + + } + + private class BaselineCacheInfo : IBaselineInfo + { + public string? Description { get; set; } + + public IReadOnlyDictionary DefaultOverrides { get; set; } = new Dictionary(); + } + + private class BlobTemplateParameter : ITemplateParameter + { + internal BlobTemplateParameter(ITemplateParameter parameter) + { + if (parameter is null) + { + throw new ArgumentNullException(nameof(parameter)); + } + + if (string.IsNullOrWhiteSpace(parameter.Name)) + { + throw new ArgumentException($"{nameof(Name)} property should not be null or whitespace", nameof(parameter)); + } + Name = parameter.Name; + DataType = !string.IsNullOrWhiteSpace(parameter.DataType) ? parameter.DataType : "string"; + Choices = parameter.Choices; + + if (DataType.Equals("choice", StringComparison.OrdinalIgnoreCase) && Choices == null) + { + Choices = new Dictionary(); + } + + DefaultIfOptionWithoutValue = parameter.DefaultIfOptionWithoutValue; + Description = parameter.Description; + AllowMultipleValues = parameter.AllowMultipleValues; + Precedence = parameter.Precedence; + } + + internal BlobTemplateParameter(string name, string dataType) + { + Name = name; + DataType = dataType; + Precedence = TemplateParameterPrecedence.Default; + } + + internal BlobTemplateParameter(JsonObject jObject) + { + string? name = jObject.ToString(nameof(Name)); + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"{nameof(Name)} property should not be null or whitespace", nameof(jObject)); + } + + Name = name!; + DataType = jObject.ToString(nameof(DataType)) ?? "string"; + + if (DataType.Equals("choice", StringComparison.OrdinalIgnoreCase)) + { + Dictionary choices = new Dictionary(StringComparer.OrdinalIgnoreCase); + JsonObject? cdToken = jObject.Get(nameof(Choices)); + if (cdToken != null) + { + foreach (var cdPair in cdToken) + { + choices.Add( + cdPair.Key, + new ParameterChoice( + cdPair.Value.ToString(nameof(ParameterChoice.DisplayName)), + cdPair.Value.ToString(nameof(ParameterChoice.Description)))); + } + } + Choices = choices; + } + DefaultIfOptionWithoutValue = jObject.ToString(nameof(DefaultIfOptionWithoutValue)); + Description = jObject.ToString(nameof(Description)); + AllowMultipleValues = jObject.ToBool(nameof(AllowMultipleValues)); + + //We currently do not write the precedence to cache - so this code is redundant. + // However should we decide in future to populate it, this way the client code can consume it without the need to be updated + Precedence = jObject.ToTemplateParameterPrecedence(nameof(Precedence)); + } + + public string Name { get; internal set; } + + public string DataType { get; internal set; } + + public IReadOnlyDictionary? Choices { get; internal set; } + + [JsonIgnore] + [Obsolete("Use Precedence instead.")] + public TemplateParameterPriority Priority => Precedence.PrecedenceDefinition.ToTemplateParameterPriority(); + + [JsonIgnore] + public TemplateParameterPrecedence Precedence { get; } + + [JsonIgnore] + //ParameterDefinitionSet have only "parameter" symbols. + string ITemplateParameter.Type => "parameter"; + + [JsonIgnore] + bool ITemplateParameter.IsName => false; + + public string? DefaultValue { get; internal set; } + + [JsonIgnore] + string ITemplateParameter.DisplayName => string.Empty; + + public string? DefaultIfOptionWithoutValue { get; internal set; } + + public string? Description { get; internal set; } + + [Obsolete] + [JsonIgnore] + string ITemplateParameter.Documentation => string.Empty; + + public bool AllowMultipleValues { get; internal set; } + + public override bool Equals(object obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is ITemplateParameter parameter) + { + return Equals(parameter); + } + + return false; + } + + public override int GetHashCode() => Name != null ? Name.GetHashCode() : 0; + + public bool Equals(ITemplateParameter other) => !string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(other.Name) && Name == other.Name; + } + + private class BlobLegacyCacheTag : ICacheTag + { + public BlobLegacyCacheTag(string? description, IReadOnlyDictionary choicesAndDescriptions, string? defaultValue, string? defaultIfOptionWithoutValue) + { + Description = description; + ChoicesAndDescriptions = choicesAndDescriptions; + DefaultValue = defaultValue; + DefaultIfOptionWithoutValue = defaultIfOptionWithoutValue; + } + + public string? Description { get; } + + public IReadOnlyDictionary ChoicesAndDescriptions { get; } + + public string? DefaultValue { get; } + + public string? DefaultIfOptionWithoutValue { get; } + + [JsonIgnore] + public string DisplayName => string.Empty; + + [JsonIgnore] + public IReadOnlyDictionary Choices => new Dictionary(); + + } + + private class BlobLegacyCacheParameter : ICacheParameter + { + public BlobLegacyCacheParameter(string? description, string? dataType, string? defaultValue, string? defaultIfOptionWithoutValue) + { + Description = description; + DataType = dataType; + DefaultValue = defaultValue; + DefaultIfOptionWithoutValue = defaultIfOptionWithoutValue; + } + + public string? DataType { get; } + + public string? DefaultValue { get; } + + public string? Description { get; } + + public string? DefaultIfOptionWithoutValue { get; } + + [JsonIgnore] + public string DisplayName => string.Empty; + } + + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/LegacySearchCacheReader.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/LegacySearchCacheReader.cs new file mode 100644 index 000000000000..78b7f574cfd3 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/LegacySearchCacheReader.cs @@ -0,0 +1,254 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine; +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateSearch.Common +{ + [Obsolete("Use TemplateSearchCache instead.")] + internal static class LegacySearchCacheReader + { + internal static TemplateSearchCache ConvertTemplateDiscoveryMetadata(TemplateDiscoveryMetadata discoveryMetadata, IReadOnlyDictionary>? additionalDataReaders) + { + List packageData = new List(); + foreach (var package in discoveryMetadata.PackToTemplateMap) + { + List templateData = new List(); + foreach (var template in package.Value.TemplateIdentificationEntry) + { + var foundTemplate = discoveryMetadata.TemplateCache.FirstOrDefault(t => t.Identity.Equals(template.Identity, StringComparison.OrdinalIgnoreCase)); + if (foundTemplate == null) + { + continue; + } + if (additionalDataReaders != null) + { + Dictionary data = new Dictionary(); + foreach (var reader in additionalDataReaders) + { + if (discoveryMetadata.AdditionalData.TryGetValue(reader.Key, out object? legacyData)) + { + IDictionary? dict = legacyData as IDictionary; + if (dict?.Contains(template.Identity) ?? false) + { + data[reader.Key] = dict[template.Identity]; + } + } + } + templateData.Add(new TemplateSearchData(foundTemplate, data)); + } + else + { + templateData.Add(new TemplateSearchData(foundTemplate)); + } + } + packageData.Add(new TemplatePackageSearchData(new PackInfo(package.Key, package.Value.Version, package.Value.TotalDownloads, package.Value.Owners, package.Value.Reserved), templateData)); + } + return new TemplateSearchCache(packageData); + } + + internal static bool TryReadDiscoveryMetadata(IEngineEnvironmentSettings environmentSettings, string filePath, IReadOnlyDictionary>? additionalDataReaders, out TemplateDiscoveryMetadata? discoveryMetadata) + { + string pathToConfig = Path.Combine(environmentSettings.Paths.HostVersionSettingsDir, filePath); + environmentSettings.Host.Logger.LogDebug($"Reading cache file {pathToConfig}"); + string cacheText = environmentSettings.Host.FileSystem.ReadAllText(pathToConfig); + + JsonObject cacheObject = JExtensions.ParseJsonObject(cacheText); + + return TryReadDiscoveryMetadata(cacheObject, environmentSettings.Host.Logger, additionalDataReaders, out discoveryMetadata); + } + + internal static bool TryReadDiscoveryMetadata(JsonObject cacheObject, ILogger logger, IReadOnlyDictionary>? additionalDataReaders, out TemplateDiscoveryMetadata? discoveryMetadata) + { + // add the reader calls, build the model objects + if (TryReadVersion(logger, cacheObject, out string? version) + && TryReadTemplateList(logger, cacheObject, out IReadOnlyList? templateList) + && TryReadPackToTemplateMap(logger, cacheObject, out IReadOnlyDictionary? packToTemplateMap) + && TryReadAdditionalData(logger, cacheObject, additionalDataReaders, out IReadOnlyDictionary? additionalDta)) + { + discoveryMetadata = new TemplateDiscoveryMetadata(version!, templateList!, packToTemplateMap!, additionalDta!); + return true; + } + discoveryMetadata = null; + return false; + } + + private static bool TryReadVersion(ILogger logger, JsonObject cacheObject, out string? version) + { + logger.LogDebug($"Reading template metadata version"); + if (cacheObject.TryGetValueCaseInsensitive(nameof(TemplateDiscoveryMetadata.Version), out JsonNode? value)) + { + version = value?.ToString(); + logger.LogDebug($"Version: {version}."); + return true; + } + logger.LogDebug($"Failed to read template metadata version."); + version = null; + return false; + } + + private static bool TryReadTemplateList( + ILogger logger, + JsonObject cacheObject, + out IReadOnlyList? templateList) + { + logger.LogDebug($"Reading template list"); + try + { + // This is lifted from TemplateCache.ParseCacheContent - almost identical + if (cacheObject.TryGetValueCaseInsensitive(nameof(TemplateDiscoveryMetadata.TemplateCache), out JsonNode? templateInfoToken)) + { + List buildingTemplateList = new List(); + + if (templateInfoToken is JsonArray arr) + { + foreach (JsonNode? entry in arr) + { + if (entry is JsonObject entryObj) + { + try + { + buildingTemplateList.Add(BlobStorageTemplateInfo.FromJObject(entryObj)); + } + catch (ArgumentException ex) + { + logger.LogDebug($"Failed to read template info entry, missing mandatory fields. Details: {ex}"); + } + } + } + } + + logger.LogDebug($"Successfully read {buildingTemplateList.Count} templates."); + templateList = buildingTemplateList; + return true; + } + + logger.LogDebug($"Failed to read template info entries. Details: no TemplateCache property found."); + templateList = null; + return false; + } + catch (Exception ex) + { + logger.LogDebug($"Failed to read template info entries. Details: {ex}"); + templateList = null; + return false; + } + } + + private static bool TryReadPackToTemplateMap(ILogger logger, JsonObject cacheObject, out IReadOnlyDictionary? packToTemplateMap) + { + logger.LogDebug($"Reading package information."); + try + { + JsonNode? packToTemplateMapToken = JExtensions.GetPropertyCaseInsensitive(cacheObject, nameof(TemplateDiscoveryMetadata.PackToTemplateMap)); + if (packToTemplateMapToken is not JsonObject packToTemplateMapObject) + { + logger.LogDebug($"Failed to read package info entries. Details: no PackToTemplateMap property found."); + packToTemplateMap = null; + return false; + } + + Dictionary workingPackToTemplateMap = new(); + + foreach (var packEntry in packToTemplateMapObject) + { + if (packEntry.Value != null) + { + string packName = packEntry.Key; + JsonObject entryValue = (JsonObject)packEntry.Value; + + JsonNode? versionNode = JExtensions.GetPropertyCaseInsensitive(entryValue, nameof(PackToTemplateEntry.Version)); + JsonNode? identificationNode = JExtensions.GetPropertyCaseInsensitive(entryValue, nameof(PackToTemplateEntry.TemplateIdentificationEntry)); + if (versionNode is JsonValue versionVal && versionVal.GetValueKind() == JsonValueKind.String + && identificationNode is JsonArray identificationArray) + { + string? version = versionNode.ToString() ?? throw new Exception("Version value is null."); + List templatesInPack = new List(); + + foreach (JsonNode? templateIdentityInfo in identificationArray) + { + string? identity = templateIdentityInfo?.ToString(nameof(TemplateIdentificationEntry.Identity)); + string? groupIdentity = templateIdentityInfo?.ToString(nameof(TemplateIdentificationEntry.GroupIdentity)); + + if (identity == null) + { + throw new Exception("Identity value is null."); + } + TemplateIdentificationEntry deserializedEntry = new TemplateIdentificationEntry(identity, groupIdentity); + templatesInPack.Add(deserializedEntry); + } + + workingPackToTemplateMap[packName] = new PackToTemplateEntry(version, templatesInPack); + if (entryValue.TryGetValueCaseInsensitive(nameof(PackToTemplateEntry.TotalDownloads), out JsonNode? totalDownloadsNode) + && long.TryParse(totalDownloadsNode?.ToString(), out long totalDownloads)) + { + workingPackToTemplateMap[packName].TotalDownloads = totalDownloads; + } + } + } + } + + logger.LogDebug($"Successfully read {workingPackToTemplateMap.Count} packages."); + packToTemplateMap = workingPackToTemplateMap; + return true; + } + catch (Exception ex) + { + logger.LogDebug($"Failed to read package info entries. Details: {ex}"); + packToTemplateMap = null; + return false; + } + } + + private static bool TryReadAdditionalData(ILogger logger, JsonObject cacheObject, IReadOnlyDictionary>? additionalDataReaders, out IReadOnlyDictionary? additionalData) + { + if (additionalDataReaders == null) + { + additionalData = new Dictionary(StringComparer.OrdinalIgnoreCase); + return true; + } + logger.LogDebug($"Reading additional information."); + // get the additional data section + JsonNode? additionalDataToken = JExtensions.GetPropertyCaseInsensitive(cacheObject, nameof(TemplateDiscoveryMetadata.AdditionalData)); + if (additionalDataToken is not JsonObject additionalDataObject) + { + logger.LogDebug($"Failed to read package info entries. Details: no AdditionalData property found."); + additionalData = null; + return false; + } + + Dictionary workingAdditionalData = new(StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair> dataReadInfo in additionalDataReaders) + { + try + { + // get the entry for this piece of additional data + JsonNode? dataNode = JExtensions.GetPropertyCaseInsensitive(additionalDataObject, dataReadInfo.Key); + if (dataNode is not JsonObject dataObject) + { + // this piece of data wasn't found, or wasn't valid. Ignore it. + continue; + } + + workingAdditionalData[dataReadInfo.Key] = dataReadInfo.Value(dataObject); + } + catch (Exception ex) + { + logger.LogDebug($"Failed to read additional info entries. Details: {ex}"); + // Do nothing. + // This piece of data failed to read, but isn't strictly necessary. + } + } + + logger.LogDebug($"Successfully read {workingAdditionalData.Count} additional information entries."); + additionalData = workingAdditionalData; + return true; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/PackInfo.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/PackInfo.cs new file mode 100644 index 000000000000..deb6d88d9791 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/PackInfo.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.Common +{ + [Obsolete("The class is deprecated. Use TemplateSearchCache instead to create search cache data.")] + internal class PackInfo : ITemplatePackageInfo + { + internal PackInfo(string name, string version) + { + Name = name; + Version = version; + } + + internal PackInfo(string name, string version, long totalDownloads, IEnumerable owners, bool reserved = false) + { + Name = name; + Version = version; + TotalDownloads = totalDownloads; + Owners = owners.ToList(); + Reserved = reserved; + } + + public string Name { get; } + + public string Version { get; } + + public long TotalDownloads { get; } + + public IReadOnlyList Owners { get; } = []; + + public bool Reserved { get; } + + //not supported for v1 + public string? Description => null; + + //not supported for v1 + public string? IconUrl => null; + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/PackToTemplateEntry.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/PackToTemplateEntry.cs new file mode 100644 index 000000000000..1bc240a8957c --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/PackToTemplateEntry.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Microsoft.TemplateSearch.Common +{ + [Obsolete("The class is deprecated. Use TemplateSearchCache instead to create search cache data.")] + internal class PackToTemplateEntry + { + internal PackToTemplateEntry(string version, List templateIdentificationEntry) + { + Version = version; + TemplateIdentificationEntry = templateIdentificationEntry; + } + + [JsonInclude] + internal string Version { get; } + + [JsonInclude] + internal long TotalDownloads { get; set; } + + [JsonInclude] + internal IReadOnlyList Owners { get; set; } = []; + + [JsonInclude] + internal bool Reserved { get; set; } + + [JsonInclude] + internal IReadOnlyList TemplateIdentificationEntry { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/TemplateDiscoveryMetadata.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/TemplateDiscoveryMetadata.cs new file mode 100644 index 000000000000..2b66b2e48879 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/TemplateDiscoveryMetadata.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET7_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.TemplateEngine; +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateSearch.Common +{ + [Obsolete("The class is deprecated. Use TemplateSearchCache instead to create search cache data.")] + internal class TemplateDiscoveryMetadata + { + internal TemplateDiscoveryMetadata(string version, IReadOnlyList templateCache, IReadOnlyDictionary packToTemplateMap, IReadOnlyDictionary additionalData) + { + Version = version; + TemplateCache = templateCache; + PackToTemplateMap = packToTemplateMap; + AdditionalData = additionalData; + } + + [JsonInclude] + internal string Version { get; } + + [JsonInclude] + internal IReadOnlyList TemplateCache { get; } + + [JsonInclude] + internal IReadOnlyDictionary PackToTemplateMap { get; } + + [JsonInclude] + internal IReadOnlyDictionary AdditionalData { get; } + +#if NET7_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "Serializes a known internal type (TemplateDiscoveryMetadata).")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "Serializes a known internal type (TemplateDiscoveryMetadata).")] +#endif + internal JsonObject ToJObject() + { + return JExtensions.FromObject(this); + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/TemplateIdentificationEntry.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/TemplateIdentificationEntry.cs new file mode 100644 index 000000000000..8a8ccdb46615 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateDiscoveryMetadata/TemplateIdentificationEntry.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Microsoft.TemplateSearch.Common +{ + [Obsolete("The class is deprecated. Use TemplateSearchCache instead to create search cache data.")] + internal class TemplateIdentificationEntry + { + internal TemplateIdentificationEntry(string identity, string? groupIdentity) + { + Identity = identity; + GroupIdentity = groupIdentity; + } + + [JsonInclude] + internal string Identity { get; } + + [JsonInclude] + internal string? GroupIdentity { get; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplatePackageSearchData.Json.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplatePackageSearchData.Json.cs new file mode 100644 index 000000000000..8c9aac05b66d --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplatePackageSearchData.Json.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET7_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine; + +namespace Microsoft.TemplateSearch.Common +{ + [System.Text.Json.Serialization.JsonConverter(typeof(TemplatePackageSearchDataJsonConverter))] + public partial class TemplatePackageSearchData + { + internal TemplatePackageSearchData(JsonObject jObject, ILogger logger, IReadOnlyDictionary>? additionalDataReaders = null) + { + if (jObject is null) + { + throw new ArgumentNullException(nameof(jObject)); + } + + if (logger is null) + { + throw new ArgumentNullException(nameof(logger)); + } + string? name = jObject.ToString(nameof(Name)); + Name = !string.IsNullOrWhiteSpace(name) ? name! + : throw new ArgumentException($"{nameof(jObject)} doesn't have {nameof(Name)} property or it is not a string.", nameof(jObject)); + Version = jObject.ToString(nameof(Version)); + TotalDownloads = jObject.ToInt32(nameof(TotalDownloads)); + Owners = jObject.Get(nameof(Owners)).JTokenStringOrArrayToCollection([]); + Reserved = jObject.ToBool(nameof(Reserved)); + + Description = jObject.ToString(nameof(Description)); + IconUrl = jObject.ToString(nameof(IconUrl)); + + JsonArray? templatesData = jObject.Get(nameof(Templates)) + ?? throw new ArgumentException($"{nameof(jObject)} doesn't have {nameof(Templates)} property or it is not an array.", nameof(jObject)); + List templates = new List(); + foreach (JsonNode? template in templatesData) + { + try + { + if (template is JsonObject templateObj) + { + templates.Add(new TemplateSearchData(templateObj, logger, additionalDataReaders)); + } + else + { + throw new Exception($"Unexpected data in template package cache data, property: {nameof(Templates)}."); + } + } + catch (Exception ex) + { + logger.LogDebug($"Template package {Name}: Failed to read template data {template}, details: {ex}."); + } + } + Templates = templates; + //read additional data + AdditionalData = additionalDataReaders != null + ? TemplateSearchCache.ReadAdditionalData(jObject, additionalDataReaders, logger) + : new Dictionary(); + } + + #region JsonConverter + private class TemplatePackageSearchDataJsonConverter : System.Text.Json.Serialization.JsonConverter + { + public override TemplatePackageSearchData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => throw new NotImplementedException(); + +#if NET7_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "Templates and AdditionalData are serialized with known types.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "Templates and AdditionalData are serialized with known types.")] +#endif + public override void Write(Utf8JsonWriter writer, TemplatePackageSearchData value, JsonSerializerOptions options) + { + if (value == null) + { + return; + } + writer.WriteStartObject(); + writer.WritePropertyName(nameof(Name)); + writer.WriteStringValue(value.Name); + if (!string.IsNullOrWhiteSpace(value.Version)) + { + writer.WritePropertyName(nameof(Version)); + writer.WriteStringValue(value.Version); + } + if (value.TotalDownloads != 0) + { + writer.WritePropertyName(nameof(TotalDownloads)); + writer.WriteNumberValue(value.TotalDownloads); + } + if (value.Owners.Any()) + { + writer.WritePropertyName(nameof(Owners)); + if (value.Owners.Count == 1) + { + writer.WriteStringValue(value.Owners[0]); + } + else + { + writer.WriteStartArray(); + foreach (string owner in value.Owners) + { + writer.WriteStringValue(owner); + } + writer.WriteEndArray(); + } + } + + if (value.Reserved) + { + writer.WritePropertyName(nameof(Reserved)); + writer.WriteBooleanValue(value.Reserved); + } + if (!string.IsNullOrWhiteSpace(value.Description)) + { + writer.WritePropertyName(nameof(Description)); + writer.WriteStringValue(value.Description); + } + if (!string.IsNullOrWhiteSpace(value.IconUrl)) + { + writer.WritePropertyName(nameof(IconUrl)); + writer.WriteStringValue(value.IconUrl); + } + + writer.WritePropertyName(nameof(Templates)); + JsonSerializer.Serialize(writer, value.Templates, options); + + if (value.AdditionalData.Any()) + { + foreach (var item in value.AdditionalData) + { + writer.WritePropertyName(item.Key); + JsonSerializer.Serialize(writer, item.Value, options); + } + } + writer.WriteEndObject(); + } + } + + #endregion + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplateSearchCache.Json.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplateSearchCache.Json.cs new file mode 100644 index 000000000000..aef541af28b2 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplateSearchCache.Json.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET7_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine; + +namespace Microsoft.TemplateSearch.Common +{ + internal partial class TemplateSearchCache + { + private static readonly string[] SupportedVersions = new[] { "1.0.0.0", "1.0.0.3", "2.0" }; + + internal static TemplateSearchCache FromJObject( + JsonObject cacheObject, + ILogger logger, + IReadOnlyDictionary>? additionalDataReaders = null) + { + if (cacheObject is null) + { + throw new ArgumentNullException(nameof(cacheObject)); + } + + if (logger is null) + { + throw new ArgumentNullException(nameof(logger)); + } + + if (!TryReadVersion(logger, cacheObject, out string? version) || string.IsNullOrWhiteSpace(version)) + { + throw new NotSupportedException(LocalizableStrings.TemplateSearchCache_Exception_NotSupported); + } + if (version!.StartsWith("1")) + { +#pragma warning disable CS0612, CS0618 // Type or member is obsolete + if (LegacySearchCacheReader.TryReadDiscoveryMetadata(cacheObject, logger, additionalDataReaders, out TemplateDiscoveryMetadata? discoveryMetadata)) + { + return LegacySearchCacheReader.ConvertTemplateDiscoveryMetadata(discoveryMetadata!, additionalDataReaders); + } + else + { + logger.LogDebug($"Failed to read template search cache, version: {version}."); + throw new Exception(LocalizableStrings.TemplateSearchCache_Exception_NotValid); + } +#pragma warning restore CS0612, CS0618 // Type or member is obsolete + + } + + JsonArray? data = cacheObject.Get(nameof(TemplatePackages)) + ?? throw new Exception(LocalizableStrings.TemplateSearchCache_Exception_NotValid); + List templatePackages = new(); + foreach (JsonNode? templatePackage in data) + { + try + { + if (templatePackage is not JsonObject templatePackageObj) + { + throw new Exception($"Unexpected data in template search cache data, property: {nameof(TemplatePackages)}, value: {templatePackage}"); + } + templatePackages.Add(new TemplatePackageSearchData(templatePackageObj, logger, additionalDataReaders)); + } + catch (Exception ex) + { + logger.LogDebug($"Failed to read template package data {templatePackage}, details: {ex}"); + } + } + return new TemplateSearchCache(templatePackages, version!); + } + + internal static IDictionary ReadAdditionalData( + JsonObject cacheObject, + IReadOnlyDictionary> additionalDataReaders, + ILogger logger) + { + Dictionary additionalData = new(StringComparer.OrdinalIgnoreCase); + foreach (KeyValuePair> dataReadInfo in additionalDataReaders) + { + JsonNode? dataNode = JExtensions.GetPropertyCaseInsensitive(cacheObject, dataReadInfo.Key); + if (dataNode is not JsonObject dataObject) + { + // this piece of data wasn't found, or wasn't valid. Ignore it. + continue; + } + try + { + // get the entry for this piece of additional data + additionalData[dataReadInfo.Key] = dataReadInfo.Value(dataObject); + } + catch (Exception ex) + { + logger.LogDebug($"Failed to read additional info entries. Details: {ex}"); + // Do nothing. + // This piece of data failed to read, but isn't strictly necessary. + } + } + return additionalData; + } + +#if NET7_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "Serializes a known internal type (TemplateSearchCache).")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "Serializes a known internal type (TemplateSearchCache).")] +#endif + internal JsonObject ToJObject() + { + return JExtensions.FromObject(this); + } + + private static bool TryReadVersion(ILogger logger, JsonObject cacheObject, out string? version) + { + logger.LogDebug($"Reading template search cache version"); + version = cacheObject.ToString(nameof(Version)); + if (!string.IsNullOrWhiteSpace(version)) + { + logger.LogDebug($"Version: {version}."); + if (SupportedVersions.Contains(version)) + { + return true; + } + else + { + logger.LogDebug($"Unsupported template search cache version."); + version = null; + return false; + } + } + logger.LogDebug($"Failed to read template search cache version."); + version = null; + return false; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplateSearchCache.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplateSearchCache.cs new file mode 100644 index 000000000000..1c71edb54c39 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplateSearchCache.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Microsoft.TemplateSearch.Common +{ + internal partial class TemplateSearchCache + { + private const string CurrentVersion = "2.0"; + + internal TemplateSearchCache(IReadOnlyList data) + { + // when creating from freshly-generated data, order the results for clarity + TemplatePackages = data.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase).ToArray(); + Version = CurrentVersion; + } + + private TemplateSearchCache(IReadOnlyList data, string version) + { + // don't order results when creating from a read file + TemplatePackages = data; + Version = version; + } + + [JsonInclude] + internal IReadOnlyList TemplatePackages { get; } + + [JsonInclude] + internal string Version { get; private set; } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplateSearchData.Json.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplateSearchData.Json.cs new file mode 100644 index 000000000000..6d7bec690df6 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCache/TemplateSearchData.Json.cs @@ -0,0 +1,208 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET7_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateSearch.Common +{ + [System.Text.Json.Serialization.JsonConverter(typeof(TemplateSearchData.TemplateSearchDataJsonConverter))] + public partial class TemplateSearchData : ITemplateInfo + { + internal TemplateSearchData(JsonObject jObject, ILogger logger, IReadOnlyDictionary>? additionalDataReaders = null) + { + if (jObject is null) + { + throw new ArgumentNullException(nameof(jObject)); + } + + if (logger is null) + { + throw new ArgumentNullException(nameof(logger)); + } + +#pragma warning disable CS0618 // Type or member is obsolete. The code will be moved to TemplateSearchData.Json when BlobStorageTemplateInfo is ready to be removed. + TemplateInfo = BlobStorageTemplateInfo.FromJObject(jObject); +#pragma warning restore CS0618 // Type or member is obsolete + + //read additional data + AdditionalData = additionalDataReaders != null + ? TemplateSearchCache.ReadAdditionalData(jObject, additionalDataReaders, logger) + : new Dictionary(); + } + + #region JsonConverter + private class TemplateSearchDataJsonConverter : System.Text.Json.Serialization.JsonConverter + { + //falls back to default de-serializer if not implemented + public override TemplateSearchData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => throw new NotImplementedException(); + +#if NET7_0_OR_GREATER + [UnconditionalSuppressMessage("AOT", "IL2026:RequiresUnreferencedCode", Justification = "BaselineInfo and AdditionalData are serialized with known types.")] + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "BaselineInfo and AdditionalData are serialized with known types.")] +#endif + public override void Write(Utf8JsonWriter writer, TemplateSearchData value, JsonSerializerOptions options) + { + if (value == null) + { + return; + } + writer.WriteStartObject(); + writer.WritePropertyName(nameof(ITemplateInfo.Identity)); + writer.WriteStringValue(value.TemplateInfo.Identity); + if (!string.IsNullOrWhiteSpace(value.TemplateInfo.GroupIdentity)) + { + writer.WritePropertyName(nameof(ITemplateInfo.GroupIdentity)); + writer.WriteStringValue(value.TemplateInfo.GroupIdentity); + } + if (value.TemplateInfo.Precedence != 0) + { + writer.WritePropertyName(nameof(ITemplateInfo.Precedence)); + writer.WriteNumberValue(value.TemplateInfo.Precedence); + } + writer.WritePropertyName(nameof(ITemplateInfo.Name)); + writer.WriteStringValue(value.TemplateInfo.Name); + writer.WritePropertyName(nameof(ITemplateInfo.ShortNameList)); + writer.WriteStartArray(); + foreach (string shortName in value.TemplateInfo.ShortNameList) + { + if (!string.IsNullOrWhiteSpace(shortName)) + { + writer.WriteStringValue(shortName); + } + } + writer.WriteEndArray(); + if (!string.IsNullOrWhiteSpace(value.TemplateInfo.Author)) + { + writer.WritePropertyName(nameof(ITemplateInfo.Author)); + writer.WriteStringValue(value.TemplateInfo.Author); + } + if (!string.IsNullOrWhiteSpace(value.TemplateInfo.Description)) + { + writer.WritePropertyName(nameof(ITemplateInfo.Description)); + writer.WriteStringValue(value.TemplateInfo.Description); + } + if (!string.IsNullOrWhiteSpace(value.TemplateInfo.ThirdPartyNotices)) + { + writer.WritePropertyName(nameof(ITemplateInfo.ThirdPartyNotices)); + writer.WriteStringValue(value.TemplateInfo.ThirdPartyNotices); + } + + if (value.TemplateInfo.Classifications.Any()) + { + writer.WritePropertyName(nameof(ITemplateInfo.Classifications)); + writer.WriteStartArray(); + foreach (string classification in value.TemplateInfo.Classifications) + { + if (!string.IsNullOrWhiteSpace(classification)) + { + writer.WriteStringValue(classification); + } + } + writer.WriteEndArray(); + } + + if (value.TemplateInfo.TagsCollection.Any()) + { + writer.WritePropertyName(nameof(ITemplateInfo.TagsCollection)); + writer.WriteStartObject(); + foreach (var tag in value.TemplateInfo.TagsCollection) + { + writer.WritePropertyName(tag.Key); + writer.WriteStringValue(tag.Value); + } + writer.WriteEndObject(); + } + + if (value.TemplateInfo.ParameterDefinitions.Any()) + { +#pragma warning disable CS0618 // Type or member is obsolete + writer.WritePropertyName(nameof(ITemplateInfo.Parameters)); +#pragma warning restore CS0618 // Type or member is obsolete + writer.WriteStartArray(); + foreach (ITemplateParameter param in value.TemplateInfo.ParameterDefinitions) + { + writer.WriteStartObject(); + writer.WritePropertyName(nameof(ITemplateParameter.Name)); + writer.WriteStringValue(param.Name); + if (!string.IsNullOrWhiteSpace(param.DataType)) + { + writer.WritePropertyName(nameof(ITemplateParameter.DataType)); + writer.WriteStringValue(param.DataType); + } + if (!string.IsNullOrWhiteSpace(param.Description)) + { + writer.WritePropertyName(nameof(ITemplateParameter.Description)); + writer.WriteStringValue(param.Description); + } + if (!string.IsNullOrWhiteSpace(param.DefaultIfOptionWithoutValue)) + { + writer.WritePropertyName(nameof(ITemplateParameter.DefaultIfOptionWithoutValue)); + writer.WriteStringValue(param.DefaultIfOptionWithoutValue); + } + + if (param.Choices != null && param.Choices.Any()) + { + writer.WritePropertyName(nameof(ITemplateParameter.Choices)); + writer.WriteStartObject(); + foreach (var choice in param.Choices) + { + writer.WritePropertyName(choice.Key); + writer.WriteStartObject(); + if (!string.IsNullOrWhiteSpace(choice.Value.Description)) + { + writer.WritePropertyName(nameof(ParameterChoice.Description)); + writer.WriteStringValue(choice.Value.Description); + } + if (!string.IsNullOrWhiteSpace(choice.Value.DisplayName)) + { + writer.WritePropertyName(nameof(ParameterChoice.DisplayName)); + writer.WriteStringValue(choice.Value.DisplayName); + } + writer.WriteEndObject(); + } + writer.WriteEndObject(); + } + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + + if (value.TemplateInfo.BaselineInfo.Any()) + { + writer.WritePropertyName(nameof(ITemplateInfo.BaselineInfo)); + JsonSerializer.Serialize(writer, value.TemplateInfo.BaselineInfo, options); + } + + if (value.TemplateInfo.PostActions.Any()) + { + writer.WritePropertyName(nameof(ITemplateInfo.PostActions)); + writer.WriteStartArray(); + foreach (Guid guid in value.TemplateInfo.PostActions) + { + writer.WriteStringValue(guid.ToString()); + } + writer.WriteEndArray(); + } + + if (value.AdditionalData.Any()) + { + foreach (var item in value.AdditionalData) + { + writer.WritePropertyName(item.Key); + JsonSerializer.Serialize(writer, item.Value, options); + } + } + + writer.WriteEndObject(); + } + } + #endregion + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCoordinator.cs b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCoordinator.cs new file mode 100644 index 000000000000..e35a76b2575f --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/TemplateSearchCoordinator.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.Common +{ + /// + /// performs the search for the templates in registered search providers. + /// + public sealed class TemplateSearchCoordinator + { + private readonly IEngineEnvironmentSettings _environmentSettings; + private readonly IReadOnlyDictionary _providers; + + public TemplateSearchCoordinator( + IEngineEnvironmentSettings environmentSettings, + IReadOnlyDictionary>? additionalDataReaders = null) + { + _environmentSettings = environmentSettings ?? throw new ArgumentNullException(nameof(environmentSettings)); + IReadOnlyDictionary> additionalDataReaders1 = additionalDataReaders ?? new Dictionary>(); + Dictionary configuredProviders = new Dictionary(); + foreach (ITemplateSearchProviderFactory factory in _environmentSettings.Components.OfType()) + { + configuredProviders.Add(factory.DisplayName, factory.CreateProvider(_environmentSettings, additionalDataReaders1)); + } + _providers = configuredProviders; + } + + /// + /// Searches for the templates matching the filters in registered search providers. + /// + /// The filter that defines if is a match. + /// The filter that list of templates that are the match inside given . + /// The cancellation token to cancel the operation. + /// The list of representing the result of the search for given . + public async Task> SearchAsync( + Func packFilter, + Func> matchingTemplatesFilter, + CancellationToken cancellationToken) + { + if (packFilter is null) + { + throw new ArgumentNullException(nameof(packFilter)); + } + + if (matchingTemplatesFilter is null) + { + throw new ArgumentNullException(nameof(matchingTemplatesFilter)); + } + cancellationToken.ThrowIfCancellationRequested(); + + List results = new List(); + + foreach (ITemplateSearchProvider provider in _providers.Values) + { + try + { + var providerResults = await provider.SearchForTemplatePackagesAsync(packFilter, matchingTemplatesFilter, cancellationToken).ConfigureAwait(false); + results.Add(new SearchResult(provider, true, hits: providerResults)); + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception ex) + { + _environmentSettings.Host.Logger.LogDebug("Search by provider {0} failed, details: {1}", provider.Factory.DisplayName, ex); + results.Add(new SearchResult(provider, false, ex.Message)); + } + } + return results; + } + } +} diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.cs.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.cs.xlf new file mode 100644 index 000000000000..b3ec89ac39fe --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.cs.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + Nepovedlo se aktualizovat mezipaměť vyhledávání. + + + + Local search cache '{0}' does not exist. + Místní mezipaměť vyhledávání {0} neexistuje. + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + Nepovedlo se aktualizovat mezipaměť vyhledávání. Použije se místo ní místní mezipaměť vyhledávání. + + + + The template search cache data is not supported. + Data mezipaměti vyhledávání šablon se nepodporují. + + + + The template search cache data is not valid. + Data mezipaměti vyhledávání šablon nejsou platná. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.de.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.de.xlf new file mode 100644 index 000000000000..c2c32e5f8237 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.de.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + Fehler beim Aktualisieren des Suchcaches. + + + + Local search cache '{0}' does not exist. + Der lokale Suchcache \"{0}\" ist nicht vorhanden. + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + Fehler beim Aktualisieren des Suchcaches. Stattdessen wird der lokale Suchcache verwendet. + + + + The template search cache data is not supported. + Die Vorlagen-Suchcachedaten werden nicht unterstützt. + + + + The template search cache data is not valid. + Die Vorlagen-Suchcachedaten sind nicht gültig. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.es.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.es.xlf new file mode 100644 index 000000000000..dbf995d47b60 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.es.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + No se pudo actualizar la caché de búsqueda. + + + + Local search cache '{0}' does not exist. + La caché de búsqueda local \"{0}\" no existe. + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + No se pudo actualizar la caché de búsqueda. Se usará la caché de búsqueda local en su lugar. + + + + The template search cache data is not supported. + No se admiten los datos de la caché de búsqueda de plantillas. + + + + The template search cache data is not valid. + Los datos de la caché de búsqueda de plantillas no son válidos. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.fr.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.fr.xlf new file mode 100644 index 000000000000..5818299a0e0b --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.fr.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + Échec de la mise à jour du cache de recherche. + + + + Local search cache '{0}' does not exist. + Le cache de recherche local « {0} » n’existe pas. + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + Échec de la mise à jour du cache de recherche. Le cache de recherche local sera utilisé à la place. + + + + The template search cache data is not supported. + Les données du cache de recherche de modèle ne sont pas prises en charge. + + + + The template search cache data is not valid. + Les données du cache de recherche de modèle ne sont pas valides. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.it.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.it.xlf new file mode 100644 index 000000000000..23529b719024 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.it.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + Non è stato possibile aggiornare la cache di ricerca. + + + + Local search cache '{0}' does not exist. + La cache di ricerca locale '{0}' non esiste. + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + Non è stato possibile aggiornare la cache di ricerca. Verrà invece usata la cache di ricerca locale. + + + + The template search cache data is not supported. + I dati della cache di ricerca del modello non sono supportati. + + + + The template search cache data is not valid. + I dati della cache di ricerca del modello non sono validi. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.ja.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.ja.xlf new file mode 100644 index 000000000000..19c0f63f1025 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.ja.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + 検索キャッシュを更新できませんでした。 + + + + Local search cache '{0}' does not exist. + ローカル検索キャッシュ '{0}' は存在しません。 + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + 検索キャッシュを更新できませんでした。代わりにローカル検索キャッシュが使用されます。 + + + + The template search cache data is not supported. + このテンプレート検索キャッシュ データはサポートされていません。 + + + + The template search cache data is not valid. + このテンプレート検索キャッシュ データは有効ではありません。 + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.ko.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.ko.xlf new file mode 100644 index 000000000000..b80b4a0f2af4 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.ko.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + 검색 캐시를 업데이트하지 못했습니다. + + + + Local search cache '{0}' does not exist. + ‘{0}’ 로컬 검색 캐시가 없습니다. + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + 검색 캐시를 업데이트하지 못했습니다. 대신 로컬 검색 캐시를 사용 합니다. + + + + The template search cache data is not supported. + 템플릿 검색 캐시 데이터가 지원되지 않습니다. + + + + The template search cache data is not valid. + 템플릿 검색 캐시 데이터가 유효하지 않습니다. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.pl.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.pl.xlf new file mode 100644 index 000000000000..8319c43f50fa --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.pl.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + Nie można zaktualizować pamięci podręcznej wyszukiwania. + + + + Local search cache '{0}' does not exist. + Lokalna pamięć podręczna wyszukiwania \"{0}\" nie istnieje. + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + Nie można zaktualizować pamięci podręcznej wyszukiwania. Zamiast tego zostanie użyta lokalna pamięć podręczna wyszukiwania. + + + + The template search cache data is not supported. + Dane pamięci podręcznej wyszukiwania szablonów nie są obsługiwane. + + + + The template search cache data is not valid. + Dane pamięci podręcznej wyszukiwania szablonów nie są prawidłowe. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.pt-BR.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.pt-BR.xlf new file mode 100644 index 000000000000..0f2822e9eabd --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.pt-BR.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + Falha ao atualizar o cache de pesquisa. + + + + Local search cache '{0}' does not exist. + O cache de pesquisa local '{0}' não existe. + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + Falha ao atualizar o cache de pesquisa. Em vez disso, o cache de pesquisa local será usado. + + + + The template search cache data is not supported. + Os dados do cache de pesquisa do modelo não são suportados. + + + + The template search cache data is not valid. + O modelo de cache de busca de dados não é válido. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.ru.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.ru.xlf new file mode 100644 index 000000000000..8ead1ea67e93 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.ru.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + Не удалось обновить кэш поиска. + + + + Local search cache '{0}' does not exist. + Кэш локального поиска \"{0}\" не существует. + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + Не удалось обновить кэш поиска. Вместо него будет использоваться кэш локального поиска. + + + + The template search cache data is not supported. + Данные кэша поиска шаблонов не поддерживаются. + + + + The template search cache data is not valid. + Данные кэша поиска шаблонов недопустимы. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.tr.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.tr.xlf new file mode 100644 index 000000000000..bea94fdb8865 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.tr.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + Arama önbelleği güncelleştirilemedi. + + + + Local search cache '{0}' does not exist. + Yerel arama önbelleği '{0}' mevcut değil. + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + Arama önbelleği güncelleştirilemedi. Bunun yerine yerel arama önbelleği kullanılacak. + + + + The template search cache data is not supported. + Şablon arama önbellek verileri desteklenmiyor. + + + + The template search cache data is not valid. + Şablon arama önbellek verileri geçerli değil. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.zh-Hans.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.zh-Hans.xlf new file mode 100644 index 000000000000..ab38e3e98828 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.zh-Hans.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + 更新搜索缓存失败。 + + + + Local search cache '{0}' does not exist. + 本地搜索缓存“{0}”不存在。 + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + 更新搜索缓存失败。将改为使用本地搜索缓存。 + + + + The template search cache data is not supported. + 不支持该模板搜索缓存数据。 + + + + The template search cache data is not valid. + 该模板搜索缓存数据无效。 + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.zh-Hant.xlf b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.zh-Hant.xlf new file mode 100644 index 000000000000..22858be43430 --- /dev/null +++ b/src/TemplateEngine/Microsoft.TemplateSearch.Common/xlf/LocalizableStrings.zh-Hant.xlf @@ -0,0 +1,32 @@ + + + + + + Failed to update search cache. + 無法更新搜尋快取。 + + + + Local search cache '{0}' does not exist. + 本機搜尋快取 '{0}' 不存在。 + {0} - file path to search cache + + + Failed to update search cache. Local search cache will be used instead. + 無法更新搜尋快取。會改用本機搜尋快取。 + + + + The template search cache data is not supported. + 不支援範本搜尋快取資料。 + + + + The template search cache data is not valid. + 範本搜尋快取資料無效。 + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Shared/IsExternalInit.cs b/src/TemplateEngine/Shared/IsExternalInit.cs new file mode 100644 index 000000000000..5fb3b8678341 --- /dev/null +++ b/src/TemplateEngine/Shared/IsExternalInit.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETSTANDARD || NETFRAMEWORK + +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit; +} + +#endif diff --git a/src/TemplateEngine/Shared/JExtensions.cs b/src/TemplateEngine/Shared/JExtensions.cs new file mode 100644 index 000000000000..819812544f72 --- /dev/null +++ b/src/TemplateEngine/Shared/JExtensions.cs @@ -0,0 +1,556 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET7_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; + +namespace Microsoft.TemplateEngine +{ + internal static class JExtensions + { + private static readonly JsonDocumentOptions DocOptions = new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; + private static readonly JsonSerializerOptions SerializerOptions = new() + { + PropertyNameCaseInsensitive = true, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + internal static string? ToString(this JsonNode? token, string? key) + { + if (key == null) + { + if (token == null) + { + return null; + } + + if (token is JsonValue val && val.GetValueKind() == JsonValueKind.String) + { + return val.GetValue(); + } + + return null; + } + + if (token is not JsonObject obj) + { + return null; + } + + JsonNode? element = GetPropertyCaseInsensitive(obj, key); + if (element == null || element.GetValueKind() == JsonValueKind.Null) + { + return null; + } + + if (element is JsonValue strVal && strVal.GetValueKind() == JsonValueKind.String) + { + return strVal.GetValue(); + } + + return element.ToJsonString(); + } + + internal static bool TryGetValue(this JsonNode? token, string? key, out JsonNode? result) + { + result = null; + + // determine which token to bool-ify + if (token == null) + { + return false; + } + else if (key == null) + { + result = token; + } + else + { + result = GetPropertyCaseInsensitive(token.AsObject(), key); + if (result == null) + { + return false; + } + } + + return true; + } + + internal static bool TryParseBool(this JsonNode token, out bool result) + { + result = false; + var kind = token.GetValueKind(); + if (kind == JsonValueKind.True) + { + result = true; + return true; + } + if (kind == JsonValueKind.False) + { + result = false; + return true; + } + if (kind == JsonValueKind.String) + { + return bool.TryParse(token.GetValue(), out result); + } + return false; + } + + internal static bool ToBool(this JsonNode? token, string? key = null, bool defaultValue = false) + { + if (!token.TryGetValue(key, out JsonNode? checkToken)) + { + return defaultValue; + } + + if (!checkToken!.TryParseBool(out bool result)) + { + result = defaultValue; + } + + return result; + } + + internal static bool TryParseInt(this JsonNode token, out int result) + { + result = default; + var kind = token.GetValueKind(); + if (kind == JsonValueKind.Number) + { + if (token is JsonValue jv && jv.TryGetValue(out int intVal)) + { + result = intVal; + return true; + } + return int.TryParse(token.ToJsonString(), out result); + } + if (kind == JsonValueKind.String) + { + return int.TryParse(token.GetValue(), out result); + } + return false; + } + + internal static int ToInt32(this JsonNode? token, string? key = null, int defaultValue = 0) + { + if (key == null) + { + if (token == null || !token.TryParseInt(out int value)) + { + return defaultValue; + } + + return value; + } + + if (token is not JsonObject obj) + { + return defaultValue; + } + + JsonNode? element = GetPropertyCaseInsensitive(obj, key); + if (element == null || !element.TryParseInt(out int result)) + { + return defaultValue; + } + + return result; + } + + internal static T ToEnum(this JsonNode token, string? key = null, T defaultValue = default, bool ignoreCase = false) + where T : struct + { + string? val = token.ToString(key); + if (val == null || !Enum.TryParse(val, ignoreCase, out T result)) + { + return defaultValue; + } + + return result; + } + + internal static Guid ToGuid(this JsonNode token, string? key = null, Guid defaultValue = default) + { + string? val = token.ToString(key); + if (val == null || !Guid.TryParse(val, out Guid result)) + { + return defaultValue; + } + + return result; + } + + /// + /// Reads as read only string list/>. + /// Property value may be string or array. + /// + internal static IReadOnlyList ToStringReadOnlyList(this JsonObject jObject, string propertyName, IReadOnlyList? defaultValue = null) + { + defaultValue ??= []; + JsonNode? token = jObject.Get(propertyName); + if (token == null) + { + return defaultValue; + } + return token.JTokenStringOrArrayToCollection(defaultValue) ?? defaultValue; + } + + internal static IEnumerable> PropertiesOf(this JsonNode? token, string? key = null) + { + if (token is not JsonObject obj) + { + return []; + } + + if (key != null) + { + JsonNode? element = GetPropertyCaseInsensitive(obj, key); + if (element == null) + { + return []; + } + return element is not JsonObject jObj ? [] : jObj.ToList(); + } + return obj.ToList(); + } + + internal static T? Get(this JsonNode? token, string? key) + where T : JsonNode + { + if (token is not JsonObject obj || key == null) + { + return default; + } + + JsonNode? res = GetPropertyCaseInsensitive(obj, key); + return res as T; + } + + internal static IReadOnlyDictionary ToStringDictionary(this JsonNode token, StringComparer? comparer = null, string? propertyName = null) + { + Dictionary result = new(comparer ?? StringComparer.Ordinal); + + foreach (var property in token.PropertiesOf(propertyName)) + { + if (property.Value == null || property.Value.GetValueKind() != JsonValueKind.String) + { + continue; + } + + result[property.Key] = property.Value.GetValue(); + } + + return result; + } + + /// + /// Converts properties of to dictionary. + /// Leaves the values as JsonNode. + /// + internal static IReadOnlyDictionary ToJsonNodeDictionary(this JsonNode token, StringComparer? comparer = null, string? propertyName = null) + { + Dictionary result = new(comparer ?? StringComparer.Ordinal); + + foreach (var property in token.PropertiesOf(propertyName)) + { + if (property.Value != null) + { + result[property.Key] = property.Value; + } + } + + return result; + } + + /// + /// Converts properties of to dictionary. + /// Values are serialized to string (as JsonNode). Strings are serialized as JSON, i.e. needs to be parsed prior to be used. + /// + internal static IReadOnlyDictionary ToJsonNodeStringDictionary(this JsonNode token, StringComparer? comparer = null, string? propertyName = null) + { + Dictionary result = new(comparer ?? StringComparer.Ordinal); + + foreach (var property in token.PropertiesOf(propertyName)) + { + if (property.Value != null) + { + result[property.Key] = property.Value.ToJsonString(); + } + } + + return result; + } + + internal static TemplateParameterPrecedence ToTemplateParameterPrecedence(this JsonNode jObject, string? key) + { + if (!jObject.TryGetValue(key, out JsonNode? checkToken)) + { + return TemplateParameterPrecedence.Default; + } + + PrecedenceDefinition precedenceDefinition = (PrecedenceDefinition)checkToken.ToInt32(nameof(PrecedenceDefinition)); + string? isRequiredCondition = checkToken.ToString(nameof(TemplateParameterPrecedence.IsRequiredCondition)); + string? isEnabledCondition = checkToken.ToString(nameof(TemplateParameterPrecedence.IsEnabledCondition)); + bool isRequired = checkToken.ToBool(nameof(TemplateParameterPrecedence.IsRequired)); + + return new TemplateParameterPrecedence(precedenceDefinition, isRequiredCondition, isEnabledCondition, isRequired); + } + + internal static IReadOnlyList ArrayAsStrings(this JsonNode? token, string? propertyName = null) + { + if (propertyName != null) + { + token = token.Get(propertyName); + } + + if (token is not JsonArray arr) + { + return []; + } + + List values = new(); + + foreach (JsonNode? item in arr) + { + if (item != null && item.GetValueKind() == JsonValueKind.String) + { + values.Add(item.GetValue()); + } + } + + return values; + } + + internal static IReadOnlyList ArrayAsGuids(this JsonNode? token, string? propertyName = null) + { + if (propertyName != null) + { + token = token.Get(propertyName); + } + + if (token is not JsonArray arr) + { + return []; + } + + List values = new(); + + foreach (JsonNode? item in arr) + { + if (item != null && item.GetValueKind() == JsonValueKind.String) + { + if (Guid.TryParse(item.GetValue(), out Guid val)) + { + values.Add(val); + } + } + } + + return values; + } + + internal static IEnumerable Items(this JsonNode? token, string? propertyName = null) + where T : JsonNode + { + if (propertyName != null) + { + token = token.Get(propertyName); + } + + if (token is not JsonArray arr) + { + yield break; + } + + foreach (JsonNode? item in arr) + { + if (item is T res) + { + yield return res; + } + } + } + + internal static JsonObject ReadJObjectFromIFile(this IFile file) + { + using Stream s = file.OpenRead(); + using TextReader tr = new StreamReader(s, System.Text.Encoding.UTF8, true); + string json = tr.ReadToEnd(); + return (JsonObject?)JsonNode.Parse(json, null, DocOptions) + ?? throw new InvalidOperationException("Failed to parse JSON from file."); + } + + internal static JsonObject ReadObject(this IPhysicalFileSystem fileSystem, string path) + { + using Stream fileStream = fileSystem.OpenRead(path); + using var textReader = new StreamReader(fileStream, System.Text.Encoding.UTF8, true); + string json = textReader.ReadToEnd(); + return (JsonObject?)JsonNode.Parse(json, null, DocOptions) + ?? throw new InvalidOperationException($"Failed to parse JSON from '{path}'."); + } + + internal static void WriteObject(this IPhysicalFileSystem fileSystem, string path, T obj, JsonTypeInfo jsonTypeInfo) + { + using Stream fileStream = fileSystem.CreateFile(path); + JsonSerializer.Serialize(fileStream, obj, jsonTypeInfo); + } + + internal static IReadOnlyList JTokenStringOrArrayToCollection(this JsonNode? token, IReadOnlyList defaultSet) + { + if (token == null) + { + return defaultSet; + } + + if (token.GetValueKind() == JsonValueKind.String) + { + string tokenValue = token.GetValue(); + return new List() { tokenValue }; + } + + return token.ArrayAsStrings(); + } + + /// + /// Converts to valid JSON string. + /// +#if NET7_0_OR_GREATER + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + [RequiresDynamicCode("JSON serialization and deserialization might require runtime code generation.")] +#endif + internal static string ToJsonString(object obj) + { + return JsonSerializer.Serialize(obj, SerializerOptions); + } + + internal static string ToCamelCase(this string str) + { + return str switch + { + "" => str, + _ => str.First().ToString().ToLower() + str.Substring(1), + }; + } + + /// + /// Tries to get a property value from a using case-insensitive key matching. + /// + internal static bool TryGetValueCaseInsensitive(this JsonObject obj, string key, out JsonNode? result) + { + result = GetPropertyCaseInsensitive(obj, key); + return result != null; + } + + /// + /// Gets a property from a JsonObject with case-insensitive key matching. + /// + internal static JsonNode? GetPropertyCaseInsensitive(JsonObject obj, string key) + { + // Try exact match first (fast path). + if (obj.TryGetPropertyValue(key, out JsonNode? result)) + { + return result; + } + + // Fall back to case-insensitive search. + foreach (var kvp in obj) + { + if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)) + { + return kvp.Value; + } + } + + return null; + } + + /// + /// Parses a JSON string into a JsonObject (with comment/trailing comma support). + /// + internal static JsonObject ParseJsonObject(string json) + { + return (JsonObject?)JsonNode.Parse(json, null, DocOptions) + ?? throw new InvalidOperationException("Failed to parse JSON string as JsonObject."); + } + + /// + /// Parses a JSON string into a JsonNode (with comment/trailing comma support). + /// + internal static JsonNode? ParseJsonNode(string json) + { + return JsonNode.Parse(json, null, DocOptions); + } + + /// + /// Serializes an object to a JsonObject via JSON round-trip. + /// Equivalent to Newtonsoft's JObject.FromObject(). + /// +#if NET7_0_OR_GREATER + [RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")] + [RequiresDynamicCode("JSON serialization and deserialization might require runtime code generation.")] +#endif + internal static JsonObject FromObject(object obj) + { + string json = JsonSerializer.Serialize(obj, SerializerOptions); + return (JsonObject?)JsonNode.Parse(json, null, DocOptions) + ?? throw new InvalidOperationException("Failed to round-trip object to JsonObject."); + } + + /// + /// Creates a deep clone of a by round-tripping through JSON text. + /// + internal static JsonObject DeepCloneObject(this JsonObject source) + { + return (JsonObject?)JsonNode.Parse(source.ToJsonString(), null, DocOptions) + ?? throw new InvalidOperationException("Failed to deep clone JsonObject."); + } + + /// + /// Merges properties from into . + /// Equivalent to Newtonsoft's JObject.Merge() with default settings: + /// - Objects are recursively merged + /// - Arrays are concatenated + /// - Other values (including null) from source overwrite target. + /// + internal static void Merge(this JsonObject target, JsonObject source) + { + foreach (var property in source) + { + if (property.Value is JsonObject sourceObj + && target.TryGetPropertyValue(property.Key, out JsonNode? targetNode) + && targetNode is JsonObject targetObj) + { + // Recursively merge nested objects + targetObj.Merge(sourceObj); + } + else if (property.Value is JsonArray sourceArr + && target.TryGetPropertyValue(property.Key, out targetNode) + && targetNode is JsonArray targetArr) + { + // Concatenate arrays + foreach (var item in sourceArr) + { + targetArr.Add(item != null ? JsonNode.Parse(item.ToJsonString()) : null); + } + } + else + { + // Overwrite (or add) the property; clone value to detach from source tree + target[property.Key] = property.Value != null + ? JsonNode.Parse(property.Value.ToJsonString()) + : null; + } + } + } + } +} diff --git a/src/TemplateEngine/Tools/Directory.Build.props b/src/TemplateEngine/Tools/Directory.Build.props new file mode 100644 index 000000000000..e4fe79a83479 --- /dev/null +++ b/src/TemplateEngine/Tools/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + true + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/ExecutableCommand.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/ExecutableCommand.cs new file mode 100644 index 000000000000..e7dbf2e18f4b --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/ExecutableCommand.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using System.CommandLine.Invocation; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; + +namespace Microsoft.TemplateEngine.Authoring.CLI.Commands +{ + /// + /// Represents a together with its action. + /// + internal abstract class ExecutableCommand : Command where TModel : class + { + internal ExecutableCommand(string name, string? description = null) + : base(name, description) + { + Action = new CommandAction(this); + } + + /// + /// Parses the context from . + /// + protected internal abstract TModel ParseContext(ParseResult parseResult); + + /// + /// Executes the command on the parsed context. + /// + protected abstract Task ExecuteAsync(TModel args, ILoggerFactory loggerFactory, CancellationToken cancellationToken); + + private sealed class CommandAction : AsynchronousCommandLineAction + { + private readonly ExecutableCommand _command; + + public CommandAction(ExecutableCommand command) => _command = command; + + public int Invoke(ParseResult parseResult) => InvokeAsync(parseResult).GetAwaiter().GetResult(); + + public override async Task InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default) + { + int result; + using (ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddSimpleConsole(c => c.ColorBehavior = LoggerColorBehavior.Disabled))) + { + TModel arguments = _command.ParseContext(parseResult); + + //exceptions are handled by parser itself + result = await _command.ExecuteAsync(arguments, loggerFactory, cancellationToken).ConfigureAwait(false); + } + // Ensure all console output is flushed after the logger factory + // (and its background queue thread) has been disposed. + Console.Out.Flush(); + Console.Error.Flush(); + return result; + } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/Verify/VerifyCommand.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/Verify/VerifyCommand.cs new file mode 100644 index 000000000000..f6a5862f924a --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/Verify/VerifyCommand.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using System.CommandLine.Parsing; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier; + +namespace Microsoft.TemplateEngine.Authoring.CLI.Commands.Verify +{ + internal class VerifyCommand : ExecutableCommand + { + private const string CommandName = "verify"; + + private readonly Argument _templateNameArgument = new("template-short-name") + { + Description = LocalizableStrings.command_verify_help_templateName_description, + // 0 for case where only path is specified + Arity = new ArgumentArity(1, 1) + }; + + private readonly Option _remainingArguments = new("--template-args") + { + Description = "Template specific arguments - all joined into single enquoted string. Any needed quotations of actual arguments has to be escaped.", + Arity = new ArgumentArity(0, 1) + }; + + private readonly Option _templatePathOption = new("--template-path", "-p") + { + Description = LocalizableStrings.command_verify_help_templatePath_description, + }; + + private readonly Option _templateOutputPathOption = new("--output", "-o") + { + Description = LocalizableStrings.command_verify_help_outputPath_description, + }; + + private readonly Option _snapshotsDirectoryOption = new("--snapshots-directory", "-d") + { + Description = LocalizableStrings.command_verify_help_snapshotsDirPath_description, + }; + + private readonly Option _scenarioNameOption = new("--scenario-name") + { + Description = LocalizableStrings.command_verify_help_scenarioName_description, + }; + + private readonly Option _disableDiffToolOption = new("--disable-diff-tool") + { + Description = LocalizableStrings.command_verify_help_disableDiffTool_description, + }; + + private readonly Option _disableDefaultExcludePatternsOption = new("--disable-default-exclude-patterns") + { + Description = LocalizableStrings.command_verify_help_disableDefaultExcludes_description, + }; + + private readonly Option> _excludePatternOption = new("--exclude-pattern") + { + Description = LocalizableStrings.command_verify_help_customExcludes_description, + Arity = new ArgumentArity(0, 999) + }; + + private readonly Option> _includePatternOption = new("--include-pattern") + { + Description = LocalizableStrings.command_verify_help_customIncludes_description, + Arity = new ArgumentArity(0, 999) + }; + + private readonly Option _verifyCommandOutputOption = new("--verify-std") + { + Description = LocalizableStrings.command_verify_help_verifyOutputs_description, + }; + + private readonly Option _isCommandExpectedToFailOption = new("--fail-expected") + { + Description = LocalizableStrings.command_verify_help_expectFailure_description, + }; + + private readonly Option> _uniqueForOption = new("--unique-for") + { + Description = LocalizableStrings.command_verify_help_uniqueFor_description, + Arity = new ArgumentArity(0, 999), + AllowMultipleArgumentsPerToken = true, + }; + + public VerifyCommand() + : base(CommandName, LocalizableStrings.command_verify_help_description) + { + Arguments.Add(_templateNameArgument); + Options.Add(_remainingArguments); + Options.Add(_templatePathOption); + Options.Add(_templateOutputPathOption); + Options.Add(_snapshotsDirectoryOption); + Options.Add(_scenarioNameOption); + Options.Add(_disableDiffToolOption); + Options.Add(_disableDefaultExcludePatternsOption); + Options.Add(_excludePatternOption); + Options.Add(_includePatternOption); + Options.Add(_verifyCommandOutputOption); + Options.Add(_isCommandExpectedToFailOption); + FromAmongCaseInsensitive( + _uniqueForOption, + Enum.GetNames(typeof(UniqueForOption)) + .Where(v => !v.Equals(UniqueForOption.None.ToString(), StringComparison.OrdinalIgnoreCase)) + .ToArray()); + Options.Add(_uniqueForOption); + } + + protected internal override VerifyCommandArgs ParseContext(ParseResult parseResult) + { + return new VerifyCommandArgs( + templateName: parseResult.GetValue(_templateNameArgument), + templateSpecificArgs: parseResult.GetValue(_remainingArguments), + templatePath: parseResult.GetValue(_templatePathOption), + snapshotsDirectory: parseResult.GetValue(_snapshotsDirectoryOption), + scenarioDistinguisher: parseResult.GetValue(_scenarioNameOption), + outputDirectory: parseResult.GetValue(_templateOutputPathOption), + disableDiffTool: parseResult.GetValue(_disableDiffToolOption), + disableDefaultVerificationExcludePatterns: parseResult.GetValue(_disableDefaultExcludePatternsOption), + verificationExcludePatterns: parseResult.GetValue(_excludePatternOption), + verificationIncludePatterns: parseResult.GetValue(_includePatternOption), + verifyCommandOutput: parseResult.GetValue(_verifyCommandOutputOption), + isCommandExpectedToFail: parseResult.GetValue(_isCommandExpectedToFailOption), + uniqueForOptions: parseResult.GetValue(_uniqueForOption)); + } + + protected override async Task ExecuteAsync(VerifyCommandArgs args, ILoggerFactory loggerFactory, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ILogger logger = loggerFactory.CreateLogger(); + logger.LogInformation("Running the verification of {templateName}.", args.TemplateName); + + try + { + VerificationEngine engine = new VerificationEngine(loggerFactory); + TemplateVerifierOptions options = new(templateName: args.TemplateName) + { + TemplatePath = args.TemplatePath, + TemplateSpecificArgs = args.TemplateSpecificArgs, + DisableDiffTool = args.DisableDiffTool, + DisableDefaultVerificationExcludePatterns = args.DisableDefaultVerificationExcludePatterns, + VerificationExcludePatterns = args.VerificationExcludePatterns, + VerificationIncludePatterns = args.VerificationIncludePatterns, + SnapshotsDirectory = args.SnapshotsDirectory, + ScenarioName = args.ScenarioDistinguisher, + OutputDirectory = args.OutputDirectory, + VerifyCommandOutput = args.VerifyCommandOutput, + IsCommandExpectedToFail = args.IsCommandExpectedToFail, + UniqueFor = args.UniqueFor, + DoNotPrependCallerMethodNameToScenarioName = true, + EnsureEmptyOutputDirectory = true + }; + await engine.Execute( + options, + cancellationToken, + // We explicitly pass a path - so that the engine then process it and gets the current executing dir + // and treats it as a code base of caller of API (as in case of CLI usage we do not want to store + // expectation files in CLI sources dir) + Path.Combine(Environment.CurrentDirectory, "_")) + .ConfigureAwait(false); + return 0; + } + catch (Exception e) + { + logger.LogError(LocalizableStrings.command_verify_error_failed); + logger.LogError(e.Message); + TemplateVerificationException? ex = e as TemplateVerificationException; + return (int)(ex?.TemplateVerificationErrorCode ?? TemplateVerificationErrorCode.InternalError); + } + } + + /// + /// Case insensitive version for . + /// + private static void FromAmongCaseInsensitive(Option> option, string[]? allowedValues = null, string? allowedHiddenValue = null) + { + allowedValues ??= []; + option.Validators.Add(optionResult => ValidateAllowedValues(optionResult, allowedValues, allowedHiddenValue)); + option.CompletionSources.Add(allowedValues); + } + + private static void ValidateAllowedValues(OptionResult optionResult, string[] allowedValues, string? allowedHiddenValue = null) + { + var invalidArguments = optionResult.Tokens.Where(token => !allowedValues.Append(allowedHiddenValue).Contains(token.Value, StringComparer.OrdinalIgnoreCase)).ToList(); + if (invalidArguments.Any()) + { + optionResult.AddError(string.Format( + LocalizableStrings.command_verify_error_unrecognizedArguments, + string.Join(", ", invalidArguments.Select(arg => $"'{arg.Value}'")), + string.Join(", ", allowedValues.Select(allowedValue => $"'{allowedValue}'")))); + } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/Verify/VerifyCommandArgs.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/Verify/VerifyCommandArgs.cs new file mode 100644 index 000000000000..8bb882e04b73 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/Verify/VerifyCommandArgs.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier; + +namespace Microsoft.TemplateEngine.Authoring.CLI.Commands.Verify +{ + /// + /// Model class representing the arguments of . + /// + internal sealed class VerifyCommandArgs + { + public VerifyCommandArgs(string templateName, string? templateSpecificArgs) + { + TemplateName = templateName; + TemplateSpecificArgs = TokenizeJoinedArgs(templateSpecificArgs); + } + + public VerifyCommandArgs( + string? templateName, + string? templatePath, + string? templateSpecificArgs, + string? snapshotsDirectory, + string? scenarioDistinguisher, + string? outputDirectory, + bool disableDiffTool, + bool disableDefaultVerificationExcludePatterns, + IEnumerable? verificationExcludePatterns, + IEnumerable? verificationIncludePatterns, + bool verifyCommandOutput, + bool isCommandExpectedToFail, + IEnumerable? uniqueForOptions) + : this(templateName ?? string.Empty, templateSpecificArgs) + { + TemplatePath = templatePath; + SnapshotsDirectory = snapshotsDirectory; + ScenarioDistinguisher = scenarioDistinguisher; + OutputDirectory = outputDirectory; + DisableDiffTool = disableDiffTool; + DisableDefaultVerificationExcludePatterns = disableDefaultVerificationExcludePatterns; + VerificationExcludePatterns = verificationExcludePatterns; + VerificationIncludePatterns = verificationIncludePatterns; + VerifyCommandOutput = verifyCommandOutput; + IsCommandExpectedToFail = isCommandExpectedToFail; + UniqueFor = ToUniqueForOptionFlags(uniqueForOptions); + } + + /// + /// Gets the name of locally installed template. + /// + public string TemplateName { get; init; } + + /// + /// Gets the path to template.json file or containing directory. + /// + public string? TemplatePath { get; init; } + + /// + /// Gets the template specific arguments. + /// + public IEnumerable TemplateSpecificArgs { get; init; } + + /// + /// Gets the directory with snapshot files. + /// + public string? SnapshotsDirectory { get; init; } + + /// + /// Gets a custom prefix prepended in front of generated scenario name - result used for naming verification subdirectories. + /// + public string? ScenarioDistinguisher { get; init; } + + /// + /// Gets the target directory to output the generated template. + /// + public string? OutputDirectory { get; init; } + + /// + /// If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + /// + public bool DisableDiffTool { get; init; } + + /// + /// If set to true - all template output files will be verified, unless are specified. + /// Otherwise a default exclusions (to be documented - mostly binaries etc.). + /// + public bool DisableDefaultVerificationExcludePatterns { get; init; } + + /// + /// Set of patterns defining files to be excluded from verification. + /// + public IEnumerable? VerificationExcludePatterns { get; init; } + + /// + /// Set of patterns defining files to be included into verification (unless excluded by ). + /// By default all files are included (unless excluded). + /// + public IEnumerable? VerificationIncludePatterns { get; init; } + + /// + /// If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + /// + public bool VerifyCommandOutput { get; init; } + + /// + /// If set to true - 'dotnet new' command is expected to return nonzero return code. + /// Otherwise a zero error code and no error output is expected. + /// + public bool IsCommandExpectedToFail { get; init; } + + /// + /// Gets the Verifier expectations directory naming convention - by indicating which scenarios should be differentiated. + /// + public UniqueForOption? UniqueFor { get; init; } + + public static IEnumerable TokenizeJoinedArgs(string? joinedArgs) + { + if (string.IsNullOrEmpty(joinedArgs)) + { + return Enumerable.Empty(); + } + + if (!joinedArgs.Contains('"') && !joinedArgs.Contains('\'')) + { + return joinedArgs.Split().Where(s => !string.IsNullOrWhiteSpace(s)); + } + + return Regex.Matches(joinedArgs, @"[\""'].+?[\""']|[^ ]+") + .Cast() + .Select(m => m.Value) + .Select(RemoveEnclosingQuotation) + .Where(s => !string.IsNullOrWhiteSpace(s)); + } + + public static UniqueForOption? ToUniqueForOptionFlags(IEnumerable? uniqueForOptions) + { + return uniqueForOptions?.Aggregate(UniqueForOption.None, (a, b) => a | b); + } + + private static string RemoveEnclosingQuotation(string input) + { + int indexOfLast = input.Length - 1; + + if ( + indexOfLast >= 1 && + ((input[0] == '"' && input[indexOfLast] == '"') || (input[0] == '\'' && input[indexOfLast] == '\''))) + { + return input.Substring(1, indexOfLast - 1); + } + else + { + return input; + } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/localize/LocalizeCommand.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/localize/LocalizeCommand.cs new file mode 100644 index 000000000000..786410b7a156 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/localize/LocalizeCommand.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; + +namespace Microsoft.TemplateEngine.Authoring.CLI.Commands +{ + internal class LocalizeCommand : Command + { + internal LocalizeCommand() + : base("localize") + { + Subcommands.Add(new ExportCommand()); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/localize/export/ExportCommand.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/localize/export/ExportCommand.cs new file mode 100644 index 000000000000..e2fa5a9c5834 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/localize/export/ExportCommand.cs @@ -0,0 +1,223 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.TemplateLocalizer.Core; + +namespace Microsoft.TemplateEngine.Authoring.CLI.Commands +{ + internal sealed class ExportCommand : ExecutableCommand + { + private const string CommandName = "export"; + + private readonly Argument> _templatePathArgument = new("template-path") + { + Arity = ArgumentArity.OneOrMore, + Description = LocalizableStrings.command_export_help_templatePath_description, + }; + + private readonly Option> _languageOption = new("--language", "-l") + { + Description = LocalizableStrings.command_export_help_language_description, + Arity = ArgumentArity.OneOrMore, + AllowMultipleArgumentsPerToken = true, + }; + + private readonly Option _recursiveOption = new("recursive", new[] { "--recursive", "-r" }) + { + Description = LocalizableStrings.command_export_help_recursive_description, + }; + + private readonly Option _dryRunOption = new("--dry-run", "-d") + { + Description = LocalizableStrings.command_export_help_dryrun_description, + }; + + public ExportCommand() + : base(CommandName, LocalizableStrings.command_export_help_description) + { + Arguments.Add(_templatePathArgument); + Options.Add(_recursiveOption); + Options.Add(_languageOption); + Options.Add(_dryRunOption); + } + + protected internal override ExportCommandArgs ParseContext(ParseResult parseResult) + { + return new ExportCommandArgs( + templatePath: parseResult.GetValue(_templatePathArgument), + language: parseResult.GetValue(_languageOption), + recursive: parseResult.GetValue(_recursiveOption), + dryRun: parseResult.GetValue(_dryRunOption)); + } + + protected override async Task ExecuteAsync(ExportCommandArgs args, ILoggerFactory loggerFactory, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ILogger logger = loggerFactory.CreateLogger(); + + bool failed = false; + List templateJsonFiles = new(); + + if (args.TemplatePaths == null || !args.TemplatePaths.Any()) + { + // This shouldn't happen since command line parser will ensure that there is at least one path. + logger.LogError(LocalizableStrings.generic_log_commandExecutionFailed, CommandName); + return 1; + } + + foreach (string templatePath in args.TemplatePaths) + { + int filesBeforeAdd = templateJsonFiles.Count; + templateJsonFiles.AddRange(GetTemplateJsonFiles(templatePath, args.SearchSubdirectories)); + + if (filesBeforeAdd == templateJsonFiles.Count) + { + // No new files has been added by this path. This is an indication of a bad input. + logger.LogError(LocalizableStrings.command_export_log_templateJsonNotFound, templatePath); + failed = true; + } + } + + if (failed) + { + logger.LogError(LocalizableStrings.generic_log_commandExecutionFailed, CommandName); + return 1; + } + + List exportResults = new(); + List<(string TemplateJsonPath, Task Task)> runningExportTasks = new(templateJsonFiles.Count); + + foreach (string templateJsonPath in templateJsonFiles) + { + ExportOptions exportOptions = new(args.DryRun, targetDirectory: null, args.Languages); + runningExportTasks.Add( + (templateJsonPath, + new TemplateLocalizer.Core.TemplateLocalizer(loggerFactory).ExportLocalizationFilesAsync(templateJsonPath, exportOptions, cancellationToken))); + } + + try + { + _ = await Task.WhenAll(runningExportTasks.Select(t => t.Task)).ConfigureAwait(false); + } + catch (Exception e) when (e is not TaskCanceledException) + { + // Task.WhenAll will only throw one of the exceptions. We need to log them all. Handle this outside of catch block. + } + cancellationToken.ThrowIfCancellationRequested(); + + foreach ((string TemplateJsonPath, Task Task) pathTaskPair in runningExportTasks) + { + if (pathTaskPair.Task.IsCanceled) + { + logger.LogWarning(LocalizableStrings.command_export_log_cancelled, pathTaskPair.TemplateJsonPath); + continue; + } + else if (pathTaskPair.Task.IsFaulted) + { + failed = true; + logger.LogError(pathTaskPair.Task.Exception, LocalizableStrings.command_export_log_templateExportFailedWithException, pathTaskPair.TemplateJsonPath); + } + else + { + // Tasks is known to have already completed. We can get the result without await. + ExportResult result = pathTaskPair.Task.Result; + exportResults.Add(result); + failed |= !result.Succeeded; + } + } + + PrintResults(exportResults, logger); + return (failed || cancellationToken.IsCancellationRequested) ? 1 : 0; + } + + /// + /// Given a , finds and returns all the template.json files. The search rules are executed in the following order: + /// + /// If path points to a template.json file, it is directly returned. + /// If path points to a template directory, path to the "<directory>/.template.config/template.json" file is returned. + /// If path points to a "template.config" directory, path to the "<directory>/template.json" file is returned. + /// If path points to any other directory and is , path to all the + /// ".template.config/template.json" files under the given directory is returned. + /// + /// + /// + /// Indicates weather the subdirectories should be searched + /// in the case that points to a directory. This parameter has no effect + /// if points to a file. + /// A path for each of the found "template.json" files. + private static IEnumerable GetTemplateJsonFiles(string path, bool searchSubdirectories) + { + if (string.IsNullOrEmpty(path)) + { + yield break; + } + + if (File.Exists(path)) + { + yield return path; + yield break; + } + + if (!Directory.Exists(path)) + { + // This path neither points to a file nor to a directory. + yield break; + } + + if (!searchSubdirectories) + { + string filePath = Path.Combine(path, ".template.config", "template.json"); + if (File.Exists(filePath)) + { + yield return filePath; + } + else + { + filePath = Path.Combine(path, "template.json"); + if (File.Exists(filePath)) + { + yield return filePath; + } + } + + yield break; + } + + foreach (string filePath in Directory.EnumerateFiles(path, "template.json", SearchOption.AllDirectories)) + { + string? directoryName = Path.GetFileName(Path.GetDirectoryName(filePath)); + if (directoryName == ".template.config") + { + yield return filePath; + } + } + } + + private void PrintResults(IReadOnlyList results, ILogger logger) + { + using IDisposable? scope = logger.BeginScope("Results"); + logger.LogInformation(LocalizableStrings.command_export_log_executionEnded, results.Count); + + foreach (ExportResult result in results) + { + if (result.Succeeded) + { + logger.LogInformation(LocalizableStrings.command_export_log_templateExportSucceeded, result.TemplateJsonPath); + } + else + { + if (result.InnerException != null) + { + logger.LogError(result.InnerException, LocalizableStrings.command_export_log_templateExportFailedWithException, result.TemplateJsonPath); + } + else + { + logger.LogError(LocalizableStrings.command_export_log_templateExportFailedWithError, result.ErrorMessage, result.TemplateJsonPath); + } + } + } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/localize/export/ExportCommandArgs.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/localize/export/ExportCommandArgs.cs new file mode 100644 index 000000000000..6256ac134948 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/localize/export/ExportCommandArgs.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Authoring.CLI.Commands +{ + /// + /// Model class representing the arguments of . + /// + internal sealed class ExportCommandArgs + { + public ExportCommandArgs(IEnumerable? templatePath, IEnumerable? language, bool recursive, bool dryRun) + { + TemplatePaths = templatePath; + Languages = language; + SearchSubdirectories = recursive; + DryRun = dryRun; + } + + /// + /// Gets the paths to template.json files or containing directories. + /// + public IEnumerable? TemplatePaths { get; init; } + + /// + /// Gets the languages for which the localization files should be created. + /// If null, the default language set supported by dotnet will be used. + /// + public IEnumerable? Languages { get; init; } + + /// + /// Gets if subdirectories should be searched by the providers. + /// + public bool SearchSubdirectories { get; init; } + + /// + /// Gets the value indicating whether the export process should skip + /// flushing the file changes to file system. + /// + public bool DryRun { get; init; } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/validate/ValidateCommand.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/validate/ValidateCommand.cs new file mode 100644 index 000000000000..0fdc8abef0f1 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/validate/ValidateCommand.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Edge; +using Microsoft.TemplateEngine.Edge.Settings; + +namespace Microsoft.TemplateEngine.Authoring.CLI.Commands +{ + internal class ValidateCommand : ExecutableCommand + { + private const string CommandName = "validate"; + + private readonly Argument _templateLocationArg = new("template-location") + { + Description = LocalizableStrings.command_validate_help_description, + Arity = new ArgumentArity(1, 1) + }; + + public ValidateCommand() : base(CommandName, LocalizableStrings.command_validate_help_locationArg_description) + { + Arguments.Add(_templateLocationArg); + } + + protected internal override ValidateCommandArgs ParseContext(ParseResult parseResult) + { + return new ValidateCommandArgs(parseResult.GetValue(_templateLocationArg) ?? throw new InvalidOperationException("The command should be run with one argument.")); + } + + protected override async Task ExecuteAsync(ValidateCommandArgs args, ILoggerFactory loggerFactory, CancellationToken cancellationToken) + { + ILogger logger = loggerFactory.CreateLogger(CommandName); + cancellationToken.ThrowIfCancellationRequested(); + + using IEngineEnvironmentSettings settings = SetupSettings(loggerFactory); + Scanner scanner = new(settings); + + logger.LogInformation(LocalizableStrings.command_validate_info_scanning_in_progress, args.TemplateLocation); + + ScanResult scanResult = await scanner.ScanAsync( + args.TemplateLocation, + logValidationResults: false, + returnInvalidTemplates: true, + cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + logger.LogInformation("Scanning completed"); + PrintResults(logger, scanResult); + return scanResult.Templates.Any(t => !t.IsValid) ? 1 : 0; + } + + private IEngineEnvironmentSettings SetupSettings(ILoggerFactory loggerFactory) + { + var builtIns = new List<(Type InterfaceType, IIdentifiedComponent Instance)>(); + builtIns.AddRange(Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Components.AllComponents); + builtIns.AddRange(Microsoft.TemplateEngine.Edge.Components.AllComponents); + + ITemplateEngineHost host = new DefaultTemplateEngineHost("template-validator", "1.0", builtIns: builtIns, loggerFactory: loggerFactory); + IEngineEnvironmentSettings engineEnvironmentSettings = new EngineEnvironmentSettings(host, virtualizeSettings: true); + + return engineEnvironmentSettings; + } + + private void PrintResults(ILogger logger, ScanResult scanResult) + { + using var scope = logger.BeginScope("Results"); + logger.LogInformation(LocalizableStrings.command_validate_info_scanning_completed, scanResult.MountPoint.MountPointUri, scanResult.Templates.Count); + foreach (IScanTemplateInfo template in scanResult.Templates.OrderBy(t => t.Identity, StringComparer.Ordinal)) + { + string templateDisplayName = GetTemplateDisplayName(template); + StringBuilder sb = new(); + + LogValidationEntries( + sb, + string.Format(LocalizableStrings.command_validate_info_template_header, templateDisplayName), + template.ValidationErrors); + foreach (KeyValuePair locator in template.Localizations.OrderBy(l => l.Value.Locale, StringComparer.Ordinal)) + { + ILocalizationLocator localizationInfo = locator.Value; + + LogValidationEntries( + sb, + string.Format(LocalizableStrings.command_validate_info_template_loc_header, localizationInfo.Locale, templateDisplayName), + localizationInfo.ValidationErrors); + } + + if (!template.IsValid) + { + sb.AppendFormat(LocalizableStrings.command_validate_info_summary_invalid, templateDisplayName); + } + else + { + sb.AppendFormat(LocalizableStrings.command_validate_info_summary_valid, templateDisplayName); + } + sb.AppendLine(); + foreach (ILocalizationLocator loc in template.Localizations.Values.OrderBy(l => l.Locale, StringComparer.Ordinal)) + { + if (loc.IsValid) + { + sb.AppendFormat(LocalizableStrings.command_validate_info_summary_loc_valid, loc.Locale, templateDisplayName); + } + else + { + sb.AppendFormat(LocalizableStrings.command_validate_info_summary_loc_invalid, loc.Locale, templateDisplayName); + } + sb.AppendLine(); + } + logger.LogInformation(sb.ToString()); + } + + static string GetTemplateDisplayName(IScanTemplateInfo template) + { + string templateName = string.IsNullOrEmpty(template.Name) ? "" : template.Name; + return $"'{templateName}' ({template.Identity})"; + } + + static string PrintError(IValidationEntry error) => $" [{error.Severity}][{error.Code}] {error.ErrorMessage}"; + + static void LogValidationEntries(StringBuilder sb, string header, IReadOnlyList errors) + { + sb.AppendLine(header); + if (!errors.Any()) + { + sb.AppendLine(" " + LocalizableStrings.command_validate_info_no_entries); + } + else + { + foreach (IValidationEntry error in errors.OrderByDescending(e => e.Severity)) + { + sb.AppendLine(PrintError(error)); + } + } + } + + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/validate/ValidateCommandArgs.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/validate/ValidateCommandArgs.cs new file mode 100644 index 000000000000..c9b480b3e27a --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Commands/validate/ValidateCommandArgs.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Authoring.CLI.Commands +{ + internal class ValidateCommandArgs + { + public ValidateCommandArgs(string templateLocation) + { + TemplateLocation = templateLocation; + } + + public string TemplateLocation { get; } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/LocalizableStrings.resx b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/LocalizableStrings.resx new file mode 100644 index 000000000000..267b129cec66 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/LocalizableStrings.resx @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + + + When specified, subdirectories are also searched for template.json files. + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + + + Localization files were successfully generated for the template.json file at path "{0}". + + + Failed to find "template.json" file under the path "{0}". + Do not localize: "template.json" + + + Validates the templates at given location. + + + The location of template(s) to validate. + + + <no entries> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + {0} - template name and identity in format name (identity) + + + Found template {0}: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + + + Argument(s) {0} are not recognized. Must be one of: {1}. + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + + + Runs the template with specified arguments and compares the result with expectations files. + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + + + Specifies optional scenario name to be used in the snapshot folder name. + + + Specifies path to the directory with snapshot files. + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Microsoft.TemplateEngine.Authoring.CLI.csproj b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Microsoft.TemplateEngine.Authoring.CLI.csproj new file mode 100644 index 000000000000..58ed25d6d231 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Microsoft.TemplateEngine.Authoring.CLI.csproj @@ -0,0 +1,39 @@ + + + + Exe + $(NetMinimum);$(NetCurrent) + A dotnet CLI tool with commands for template authoring. + true + true + dotnet-template-authoring + Major + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Program.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Program.cs new file mode 100644 index 000000000000..8656226e120e --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/Program.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using Microsoft.TemplateEngine.Authoring.CLI.Commands; +using Microsoft.TemplateEngine.Authoring.CLI.Commands.Verify; + +namespace Microsoft.TemplateEngine.Authoring.CLI +{ + internal sealed class Program + { + internal static Task Main(string[] args) + { + RootCommand rootCommand = new("dotnet-template-authoring"); + rootCommand.Subcommands.Add(new LocalizeCommand()); + rootCommand.Subcommands.Add(new VerifyCommand()); + rootCommand.Subcommands.Add(new ValidateCommand()); + + return rootCommand.Parse(args, new() { EnablePosixBundling = false }).InvokeAsync(); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.cs.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.cs.xlf new file mode 100644 index 000000000000..ba34a4a4a38a --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.cs.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + Vedle souboru template.config se vytvoří „lokalizační“ adresář a lokalizační soubory se exportují do vytvořeného adresáře. Pokud lokalizační soubory už existují, existující překlady se zachovají. + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + Pokud je tato možnost specifikovaná, upravené soubory se neuloží do systému souborů a změny se pouze vytisknou ve výstupu konzole. + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + Seznam jazyků, které tato šablona podporuje. Pokud se tato možnost vynechá, následující seznam jazyků se použije jako výchozí: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + + + + When specified, subdirectories are also searched for template.json files. + Pokud je specifikováno, soubory template.json se budou hledat i v podadresářích. + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + Specifikuje cestu k souboru template.json. Pokud je uveden adresář, soubor template.json se vyhledá v tomto adresáři. Pokud jsou specifikovány rekurzivní možnosti, všechny soubory template.json v uvedeném adresáři a jeho podadresářích se budou považovat za vstupní. + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + Příkaz export se pro následující soubor zrušil: {0} + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + Provedení příkazu export se dokončilo. Počet zpracovaných souborů: {0} + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + Generování souborů lokalizace pro template.json se nezdařilo. +Důvod: {0}. +Cesta k souboru: {1}. + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + Generování lokalizačních souborů pro následující soubor template.json selhalo: „{0}“. + + + + Localization files were successfully generated for the template.json file at path "{0}". + Lokalizační soubory se úspěšně vygenerovaly pro soubor template.json s cestou „{0}“. + + + + Failed to find "template.json" file under the path "{0}". + Soubor „template.json“ se v cestě „{0}“ nepodařilo najít. + Do not localize: "template.json" + + + Validates the templates at given location. + Ověří šablony v daném umístění. + + + + The location of template(s) to validate. + Umístění šablon(y) k ověření. + + + + <no entries> + <no entries> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + Umístění „{0}“: nalezen tento počet šablon: {1}. + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + Probíhá kontrola umístění „{0}“ pro šablony. + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + Šablona {0}: Šablona není platná. + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + Lokalizace „{0}“ pro šablonu {1}: Lokalizační soubor není platný. Lokalizace se přeskočí. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + Lokalizace „{0}“ pro šablonu {1}: lokalizační soubor je platný. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + Šablona {0}: Šablona je platná. + {0} - template name and identity in format name (identity) + + + Found template {0}: + Byla nalezena šablona {0}: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + Byla nalezena lokalizace „{0}“ pro šablonu {1}: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + Ověření se nezdařilo. + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + Argument(y) {0} nebyl(y) rozpoznán(y). Musí se jednat o jeden z těchto argumentů: {1}. + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + Určuje vzory definující soubory, které se mají vyloučit z ověřování. + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + Určuje vzory definující soubory, které mají být zahrnuty do ověřování (pokud nejsou zadané, zahrnou se všechny soubory). + + + + Runs the template with specified arguments and compares the result with expectations files. + Spustí šablonu se zadanými argumenty a porovná výsledek s očekávanými soubory. + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + Pokud je nastaveno na true – všechny výstupní soubory šablony se ověří, pokud se nepoužije možnost --exclude-pattern. + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + Pokud je nastavená hodnota true, nástroj rozdílu se při neúspěšných ověřeních automaticky nespustí. + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + Pokud je nastaveno na true – očekává se, že příkaz dotnet new vrátí nenulový návratový kód. + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + Určuje cestu k cílovému adresáři pro výstup vygenerované šablony. + + + + Specifies optional scenario name to be used in the snapshot folder name. + Určuje volitelný název scénáře, který se má použít v názvu složky snímku. + + + + Specifies path to the directory with snapshot files. + Určuje cestu k adresáři se soubory snímků. + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + Název šablony, která se má ověřit. Může jít o již nainstalovanou šablonu, nebo o šablonu v místní cestě zadané s parametrem -p/--template-path. + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + Určuje cestu k adresáři se šablonou, která se má ověřit. + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + Nastaví zásady vytváření názvů adresářů podle očekávání ověřovatele tím, že určí, které scénáře by se měly odlišit. + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + Pokud je nastaveno na true – standardní výstup příkazu dotnet new a obsah chyby se ověří spolu s vytvořenými soubory šablony. + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + V příkazu {0} došlo k chybě. Další podrobnosti najdete v protokolech. + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + Při spouštění příkazu „--{0}“ došlo k chybě. Chybová zpráva: „{1}“. Podrobnosti jsou uvedeny v protokolech. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.de.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.de.xlf new file mode 100644 index 000000000000..f311dbc4c06b --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.de.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + Wenn eine template.config-Datei angegeben ist, wird daneben ein "localize"-Verzeichnis erstellt und die Lokalisierungsdateien werden in das erstellte Verzeichnis exportiert. Wenn die Lokalisierungsdateien bereits vorhanden sind, werden die vorhandenen Übersetzungen beibehalten. + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + Wenn diese Option angegeben wird, werden geänderte Dateien nicht im Dateisystem gespeichert und die Änderungen werden nur in der Konsolenausgabe gedruckt. + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + Die Liste der Sprachen, die von dieser Vorlage unterstützt werden sollen. Die folgende Sprachliste wird als Standard verwendet, wenn diese Option weggelassen wird: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + + + + When specified, subdirectories are also searched for template.json files. + Bei Angabe dieser Option werden Unterverzeichnisse auch nach template.json-Dateien durchsucht. + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + Gibt den Pfad zur Datei template.json an. Wenn ein Verzeichnis angegeben wird, wird die template.json-Datei in diesem Verzeichnis gesucht. Wenn die Option \"--recursive\" angegeben ist, werden alle template.json-Dateien unter dem angegebenen Verzeichnis und seinen Unterverzeichnissen als Eingabe betrachtet. + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + Der Befehl "export" für die folgende Datei wurde abgebrochen: "{0}". + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + Die Ausführung des "export"-Befehls wurde abgeschlossen. {0} Dateien wurden verarbeitet. + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + Fehler beim Generieren von Lokalisierungsdateien für eine Datei \"template.json\". +Ursache: {0}. +Dateipfad: {1}. + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + Fehler beim Generieren von Lokalisierungsdateien für die folgende template.json-Datei: "{0}". + + + + Localization files were successfully generated for the template.json file at path "{0}". + Lokalisierungsdateien wurden erfolgreich für die template.json-Datei im Pfad "{0}" generiert. + + + + Failed to find "template.json" file under the path "{0}". + Die Datei "template.json" konnte unter dem Pfad "{0}" nicht gefunden werden. + Do not localize: "template.json" + + + Validates the templates at given location. + Überprüft die Vorlagen am angegebenen Speicherort. + + + + The location of template(s) to validate. + Der Speicherort der zu überprüfenden Vorlage(n). + + + + <no entries> + <no entries> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + Speicherort „{0}“: {1} Vorlagen gefunden. + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + Der Speicherort „{0}“ für die Vorlagen wird überprüft. + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + Vorlage {0}: Die Vorlage ist ungültig. + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + Lokalisierung „{0}“ für die Vorlage {1}: Die Lokalisierungsdatei ist ungültig. Die Lokalisierung wird übersprungen. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + Lokalisierung „{0}“ für die Vorlage {1}: Die Lokalisierungsdatei ist gültig. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + Vorlage {0}: Die Vorlage ist gültig. + {0} - template name and identity in format name (identity) + + + Found template {0}: + Vorlage {0} gefunden: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + Lokalisierung „{0}“ für Vorlage „{1}“ gefunden: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + Fehler bei der Überprüfung. + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + Das oder die Argumente {0} wurden nicht erkannt. Folgendes ist erforderlich: {1}. + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + Gibt Muster an, die Dateien definieren, die von der Überprüfung ausgeschlossen werden sollen. + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + Gibt Muster an, die Dateien definieren, die in die Überprüfung einbezogen werden sollen (wenn nicht anders angegeben, werden alle Dateien einbezogen). + + + + Runs the template with specified arguments and compares the result with expectations files. + Führt die Vorlage mit angegebenen Argumenten aus und vergleicht das Ergebnis mit den Erwartungsdateien. + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + Bei Festlegung auf WAHR werden alle Vorlagenausgabedateien überprüft, es sei denn, die Option „--exclude-pattern“ wird verwendet. + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + Bei Festlegung auf WAHR wird das Diff-Tool bei Überprüfungsfehlern nicht automatisch vom Überprüfer gestartet. + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + Bei Festlegung auf WAHR wird erwartet, dass der Befehl „dotnet new“ einen Rückgabecode ungleich 0 (Null) zurückgibt. + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + Gibt den Pfad zum Zielverzeichnis an, in das die generierte Vorlage ausgegeben werden soll. + + + + Specifies optional scenario name to be used in the snapshot folder name. + Gibt den optionalen Szenarionamen an, der im Ordnernamen der Momentaufnahme verwendet werden soll. + + + + Specifies path to the directory with snapshot files. + Gibt den Pfad zum Verzeichnis mit Momentaufnahmedateien an. + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + Name der zu überprüfenden Vorlage. Kann eine bereits installierte Vorlage oder eine Vorlage innerhalb des lokalen Pfads sein, der mit der Option „-p/--template-path“ angegeben ist. + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + Gibt den Pfad zum Verzeichnis mit der zu überprüfenden Vorlage an. + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + Legt die Verzeichnisnamenskonvention für die Überprüfer-Erwartungen fest, indem angegeben wird, welche Szenarien unterschieden werden sollen. + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + Bei Festlegung auf WAHR werden die Standardausgabe und der Fehlerinhalt des Befehls „dotnet new“ zusammen mit den erstellten Vorlagendateien überprüft. + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + Fehler beim Ausführen des Befehls "{0}". Weitere Informationen finden Sie in den Protokollen. + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + Fehler beim Ausführen des "--{0}"-Befehls. Fehlermeldung: "{1}". Weitere Informationen finden Sie in den Protokollen. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.es.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.es.xlf new file mode 100644 index 000000000000..f0385603c7df --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.es.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + Dado un archivo template.config, crea un directorio de "localización" junto a él y exporta los archivos de localización en el directorio creado. Si los archivos de localización ya existen, se conservarán las traducciones existentes. + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + Si se especifica esta opción, los archivos modificados no se guardarán en el sistema de archivos y los cambios solo se imprimirán en los resultados de la consola. + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + La lista de idiomas que va a ser compatible con esta plantilla. Si omite esta opción, se usará la siguiente lista de idiomas como predeterminada: cs, de, en, es, fr, it, ja, dc, pl, pt-BR, ru, zh-hans, zh-hant + + + + When specified, subdirectories are also searched for template.json files. + Al especificar esto, los subdirectorios también se buscan los archivos template.json. + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + Especifica la ruta de acceso al archivo template.json. Si se da un directorio, se buscará el archivo template.json dentro del directorio. Si se especifican opciones --recursive, todos los archivos template.json del directorio especificado y sus valores se tomarán como entrada. + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + Comando "export" para el archivo siguiente se ha cancelado: "{0}". + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + Se ha completado la ejecución del comando "export". Se procesaron {0} archivos. + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + Error al generar archivos de localización para una template.json. +Motivo: {0}. +Ruta de acceso del archivo: {1}. + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + No se pudo generar archivos de localización para el siguiente archivo template.json: "{0}". + + + + Localization files were successfully generated for the template.json file at path "{0}". + Los archivos de localización se generaron correctamente para el archivo template.json en la ruta de acceso "{0}". + + + + Failed to find "template.json" file under the path "{0}". + No se pudo encontrar el archivo "template.json" bajo la ruta "{0}". + Do not localize: "template.json" + + + Validates the templates at given location. + Valida las plantillas en la ubicación especificada. + + + + The location of template(s) to validate. + Ubicación de las plantillas que se van a validar. + + + + <no entries> + <ninguna entrada> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + Ubicación "{0}": se encontraron {1} plantillas. + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + Examinando la ubicación "{0}" de las plantillas... + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + Plantilla {0}: la plantilla no es válida. + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + Localización "{0}" de la plantilla {1}: el archivo de localización no es válido. Se omitirá la localización. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + Localización "{0}" de la plantilla {1}: el archivo de localización es válido. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + Plantilla {0}: la plantilla es válida. + {0} - template name and identity in format name (identity) + + + Found template {0}: + Plantilla {0} encontrada: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + Se encontró la localización "{0}" para la plantilla {1}: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + Error de comprobación. + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + No se reconoce el argumento {0}. Debe ser uno de: {1}. + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + Especifica los patrones que definen los archivos que se excluirán de la comprobación. + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + Especifica patrones que definen los archivos que se incluirán en la comprobación (todos los archivos se incluyen si no se especifican). + + + + Runs the template with specified arguments and compares the result with expectations files. + Ejecuta la plantilla con los argumentos especificados y compara el resultado con los archivos de expectativas. + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + Si se establece en true, se comprobarán todos los archivos de salida de plantilla, a menos que se use la opción --exclude-pattern. + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + Si se establece en true, el comprobador no iniciará automáticamente la herramienta de comparación en caso de errores de comprobación. + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + Si se establece en true, se espera que el comando "dotnet new" devuelva código de retorno distinto de cero. + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + Especifica la ruta de acceso al directorio de destino en el que se generará la plantilla generada. + + + + Specifies optional scenario name to be used in the snapshot folder name. + Especifica el nombre de escenario opcional que se usará en el nombre de la carpeta de instantáneas. + + + + Specifies path to the directory with snapshot files. + Especifica la ruta de acceso al directorio con archivos de instantánea. + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + Nombre de la plantilla por verificar. Puede ser una plantilla ya instalada o una plantilla dentro de la ruta local especificada con la opción -p/--template-path. + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + Especifica la ruta de acceso al directorio con la plantilla que se va a comprobar. + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + Establece la convención de nomenclatura de directorios de expectativas del comprobador, indicando qué escenarios se deben diferenciar. + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + Si se establece en true: la salida estándar del comando "dotnet new" y el contenido del error se comprobarán junto con los archivos de plantilla generados. + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + El comando "{0}" ha encontrado un error. Vea los registros para obtener más información. + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + Ha ocurrido un error al ejecutar el comando "{0}". Mensaje de error: "{1}". Vea los registros para obtener más información. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.fr.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.fr.xlf new file mode 100644 index 000000000000..4996716340f7 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.fr.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + Étant donné un fichier template.config, crée un répertoire « localiser » en regard de celui-ci et exporte les fichiers de localisation dans le répertoire créé. Si les fichiers de localisation existent déjà, les traductions existantes sont conservées. + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + Si cette option est spécifiée, les fichiers modifiés ne seront pas enregistrés dans le système de fichiers et les modifications seront uniquement imprimées dans la sortie de la console. + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + Liste des langues que ce modèle doit prendre en charge. La liste de langages suivante sera utilisée par défaut si cette option est omise : cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + + + + When specified, subdirectories are also searched for template.json files. + Lorsqu'il est spécifié, les sous-répertoires sont également recherchés pour les fichiers template.json. + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + Spécifie le chemin d’accès au fichier template.json. Si un répertoire est donné, le fichier template.json est recherché dans le répertoire. Si l’option--Options récursives est spécifiée, tous les fichiers template.json du répertoire donné et de ses sous-répertoires sont pris comme entrée. + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + la commande « export » pour le fichier suivant a été annulée : « {0} ». + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + L'exécution de la commande « export » est terminée. {0} les fichiers ont été traités. + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + Échec de la génération de fichiers de localisation pour le template.json. +Raison{0}. +Chemin d’accès au fichier :{1}. + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + La génération de fichiers de localisation pour le template.jssuivant a échoué : « {0} ». + + + + Localization files were successfully generated for the template.json file at path "{0}". + Les fichiers de localisation ont été générés avec succès pour le fichier template.json au chemin d’accès « {0} ». + + + + Failed to find "template.json" file under the path "{0}". + Échec de la recherche du fichier « template.jssur » sous le chemin d’accès «0{0} ». + Do not localize: "template.json" + + + Validates the templates at given location. + Valide les modèles à un emplacement donné. + + + + The location of template(s) to validate. + L'emplacement du ou des modèles à valider. + + + + <no entries> + <no entries> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + Emplacement '{0}' : {1} modèles trouvés. + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + Emplacement de numérisation '{0}' pour les modèles. + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + Modèle {0} : le modèle n'est pas valide. + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + '{0}' localisation du modèle {1} : le fichier de localisation n'est pas valide. La localisation sera ignorée. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + '{0}' localisation du modèle {1} : le fichier de localisation est valide. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + Modèle {0} : le modèle est valide. + {0} - template name and identity in format name (identity) + + + Found template {0}: + Modèle trouvé {0}: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + Localisation '{0}' trouvée pour le modèle {1}: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + Échec de la vérification. + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + L'argument ou les arguments {0} ne sont pas reconnus. Doit être l'un de : {1}. + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + Spécifie le ou les modèles définissant les fichiers à exclure de la vérification. + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + Spécifie le ou les modèles définissant les fichiers à inclure dans la vérification (tous les fichiers sont inclus s’ils ne sont pas spécifiés). + + + + Runs the template with specified arguments and compares the result with expectations files. + Exécute le modèle avec les arguments spécifiés et compare le résultat aux fichiers attendus. + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + Si la valeur est définies sur true, tous les fichiers de sortie de modèle sont vérifiés, sauf si l’option --exclude-pattern est utilisée. + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + Si la valeur est définie sur true, l’outil Diff n’est pas automatiquement démarré par le vérificateur en cas d’échecs de vérification. + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + Si la valeur est définie sur true, la commande 'dotnet new' doit retourner un code de retour différent de zéro. + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + Spécifie le chemin d’accès au répertoire cible vers lequel générer la sortie du modèle généré. + + + + Specifies optional scenario name to be used in the snapshot folder name. + Spécifie le nom de scénario facultatif à utiliser dans le nom du dossier d’instantanés. + + + + Specifies path to the directory with snapshot files. + Spécifie le chemin d’accès au répertoire avec les fichiers de capture instantanée. + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + Nom du modèle à vérifier. Peut déjà un modèle être installé ou un modèle dans le chemin d’accès local spécifié avec l’option -p/--template-path. + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + Spécifie le chemin d’accès au répertoire avec le modèle à vérifier. + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + Définit la convention de nom d’annuaire des attentes du vérificateur, en indiquant les scénarios qui doivent être différenciés. + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + Si la valeur est définie sur true, la sortie standard et le contenu des erreurs de la commande 'dotnet new' sont vérifiés avec les fichiers de modèle produits. + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + La commande « {0} » a rencontré une erreur. Pour plus d’informations, consultez les journaux. + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + Une erreur s’est produite lors de l’exécution de la commande « --{0} ». Message d’erreur : « {1} ». Pour plus d’informations, consultez les journaux. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.it.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.it.xlf new file mode 100644 index 000000000000..bc95b3d32485 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.it.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + Quando viene specificato un file template.config, crea una directory "localize" accanto a esso ed esporta i file di localizzazione nella directory creata. Se i file di localizzazione esistono già, verranno mantenute le traduzioni esistenti. + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + Se questa opzione è specificata, i file modificati non verranno salvati nel file system e le modifiche verranno stampante unicamente nell'output della console. + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + L'elenco delle lingue supportate da questo modello. Il seguente elenco di lingue verrà usato per impostazione predefinito se questa opzione viene omessa: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + + + + When specified, subdirectories are also searched for template.json files. + Quando specificato, vengono ricercate anche le sottodirectory per il file template.json. + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + Specifica il percorso al file template.json file. Se viene specificata una directory, verrà eseguita una ricerca per il file template.json nella directory. Se l'opzione --recursive viene specificata, tutti i file template.json nella directory e le relative sottodirectory verranno considerati come input. + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + Il comando "export" per il file seguente è stato annullato: "{0}". + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + L'esecuzione del comando "export" è stata completata. {0} file sono stati elaborati. + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + La generazione dei file di localizzazione per un modello.json non è riuscita. +Motivo: {0}. +Percorso file: {1}. + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + Non è stato possibile generare i file di localizzazione per il file seguente template.json: "{0}". + + + + Localization files were successfully generated for the template.json file at path "{0}". + I file di localizzazione sono stati generati correttamente per il file template.json nel percorso "{0}". + + + + Failed to find "template.json" file under the path "{0}". + Non è stato possibile trovare il file "template.json" nel percorso "{0}". + Do not localize: "template.json" + + + Validates the templates at given location. + Convalida i modelli nella posizione specificata. + + + + The location of template(s) to validate. + Percorso dei modelli da convalidare. + + + + <no entries> + <no entries> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + Percorso '{0}': {1} modelli trovati. + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + Analisi del percorso '{0}' per i modelli. + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + Modello {0}: il modello non è valido. + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + Localizzazione '{0}' per il modello {1}: il file di localizzazione non è valido. La localizzazione verrà ignorata. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + Localizzazione '{0}' per il modello {1}: il file di localizzazione è valido. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + Modello {0}: il modello è valido. + {0} - template name and identity in format name (identity) + + + Found template {0}: + Trovato modello {0}: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + È stata trovata la localizzazione '{0}' per il modello {1}: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + La verifica non è riuscita. + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + L'argomento {0} non è riconosciuto. Deve essere uno dei seguenti: {1} + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + Specifica i criteri che definiscono i file da escludere dalla verifica. + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + Specifica i criteri che definiscono i file da includere nella verifica (se non specificati, vengono inclusi tutti i file). + + + + Runs the template with specified arguments and compares the result with expectations files. + Esegue il modello con gli argomenti specificati e confronta il risultato con i file delle aspettative. + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + Se impostato su true, verranno verificati tutti i file di output del modello, a meno che non venga usata l'opzione --exclude-pattern. + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + Se impostato su true, lo strumento diff non verrà avviato automaticamente da Verifier in eventuali errori di verifica. + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + Se impostato su true, è previsto che il comando 'dotnet new' restituisca codice restituito diverso da zero. + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + Specifica il percorso della directory di destinazione in cui restituire il modello generato. + + + + Specifies optional scenario name to be used in the snapshot folder name. + Specifica il nome facoltativo dello scenario da usare nel nome della cartella snapshot. + + + + Specifies path to the directory with snapshot files. + Specifica il percorso della directory con i file di snapshot. + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + Nome del modello da verificare. Può essere già installato un modello o un modello all'interno del percorso locale specificato con l'opzione -p/--template-path. + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + Specifica il percorso della directory con il modello da verificare. + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + Imposta la convenzione di denominazione delle directory delle aspettative di Verifier, indicando quali scenari devono essere differenziati. + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + Se impostato su true, verranno verificati l'output standard del comando 'dotnet new' e il contenuto degli errori insieme ai file modello prodotti. + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + Si è verificato un errore durante il comando "{0}". Per altri dettagli, vedere i log. + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + Si è verificato un errore durante l'esecuzione del comando "--{0}". Messaggio di errore: "{1}". Per altri dettagli, vedere i log. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.ja.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.ja.xlf new file mode 100644 index 000000000000..d4c9607bb988 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.ja.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + template.config ファイルが指定されている場合、そのファイルの横に "localize" ディレクトリが作成され、作成したディレクトリにローカライズ ファイルがエクスポートされます。ローカライズ ファイルが既に存在する場合、既存の翻訳は保存されます。 + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + このオプションを指定されている場合、変更されたファイルはファイル システムに保存されず、変更はコンソール出力にのみ出力されます。 + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + このテンプレートでサポートされる言語の一覧です。このオプションを省略すると、次の言語の一覧が既定で使用されます: cs、de、en、es、fr、it、ja、ko、pl、pt-BR、ru、tr、zh-Hans、zh-Hant + + + + When specified, subdirectories are also searched for template.json files. + 指定されている場合、template.json ファイルのサブディレクトリも検索されます。 + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + template.json ファイルへのパスを指定します。ディレクトリが指定されている場合、template.json ファイルはディレクトリ内で検索されます。--recursive オプションが指定されている場合、指定されたディレクトリとそのサブディレクトリの下にあるすべての template.json ファイルが入力として取得されます。 + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + "export" コマンドは、次のファイルに対してキャンセルされました: "{0}"。 + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + "export" コマンドの実行が完了しました。{0} 個のファイルが処理されました。 + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + template.json のローカライズ ファイルを生成できませんでした。 +理由: {0}。 +ファイルのパス: {1}。 + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + 次の template.json ファイルのローカライズ ファイルを生成できませんでした: "{0}"。 + + + + Localization files were successfully generated for the template.json file at path "{0}". + パス "{0}" にある template.json ファイルのローカライズ ファイルが正常に生成されました。 + + + + Failed to find "template.json" file under the path "{0}". + パス "{0}" の下に "template.json" ファイルが見つかりません。 + Do not localize: "template.json" + + + Validates the templates at given location. + 指定した場所にあるテンプレートを検証します。 + + + + The location of template(s) to validate. + 検証するテンプレートの場所。 + + + + <no entries> + <no entries> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + 場所 '{0}': {1} テンプレートが見つかりました。 + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + テンプレートの場所 '{0}' をスキャンしています... + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + テンプレート {0}: テンプレートが無効です。 + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + テンプレート{1}の ' {0}' ローカリゼーション: ローカリゼーション ファイルが無効です。ローカライズはスキップされます。 + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + テンプレート{1}の ' {0}' ローカリゼーション: ローカリゼーション ファイルが有効です。 + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + テンプレート {0}: テンプレートが有効です。 + {0} - template name and identity in format name (identity) + + + Found template {0}: + 見つかったテンプレート {0}: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + テンプレート{1}のローカライズ '{0}' が見つかりました: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + 検証に失敗しました。 + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + 引数 {0} は認識されません。次のいずれかである必要があります: {1}。 + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + 検証から除外するファイルを定義するパターンを指定します。 + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + 検証に含めるファイルを定義するパターンを指定します (指定しない場合は、すべてのファイルが含まれます)。 + + + + Runs the template with specified arguments and compares the result with expectations files. + 指定された引数を使用してテンプレートを実行し、結果を期待値ファイルと比較します。 + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + true に設定すると、--exclude-pattern オプションを使用しない限り、すべてのテンプレート出力ファイルが検証されます。 + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + true に設定すると、検証エラー時に検証ツールによって差分ツールが自動的に開始されません。 + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + true に設定すると、'dotnet new' コマンドは 0 以外のリターン コードを返す必要があります。 + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + 生成されたテンプレートを出力するターゲット ディレクトリへのパスを指定します。 + + + + Specifies optional scenario name to be used in the snapshot folder name. + スナップショット フォルダー名で使用するオプションのシナリオ名を指定します。 + + + + Specifies path to the directory with snapshot files. + スナップショットファイルを含むディレクトリへのパスを指定します。 + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + 検証するテンプレートの名前。既にインストールされているテンプレート、または -p/--template-path オプションで指定されたローカル パス内のテンプレート。 + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + 検証するテンプレートを含むディレクトリへのパスを指定します。 + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + どのシナリオを区別するかを示すことで、検証ツールの予測ディレクトリの名前指定規則を設定します。 + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + true に設定すると、'dotnet new' コマンドの標準出力とエラーの内容が、生成されたテンプレート ファイルと共に検証されます。 + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + "{0}" コマンドでエラーが発生しました。詳細については、ログを参照してください。 + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + "--{0}" コマンドの実行中にエラーが発生しました。エラー メッセージ: "{1}"。詳細については、ログをご覧ください。 + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.ko.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.ko.xlf new file mode 100644 index 000000000000..f5f43129e8de --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.ko.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + template.config 파일이 지정된 경우 옆에 "localize" 디렉터리를 만들고 지역화 파일을 생성된 디렉터리로 내보냅니다. 지역화 파일이 이미 있는 경우 기존 번역이 유지됩니다. + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + 이 옵션을 지정하면 수정된 파일이 파일 시스템에 저장되지 않고 변경 내용이 콘솔 출력에만 출력됩니다. + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + 이 템플릿에서 지원되는 언어 목록입니다. 이 옵션을 생략하면 다음 언어 목록이 기본값으로 사용됩니다. cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + + + + When specified, subdirectories are also searched for template.json files. + 지정된 경우 하위 디렉터리에 대해서도 template.json 파일을 검색합니다. + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + template.json 파일의 경로를 지정합니다. 디렉터리가 지정되면 디렉터리 내에서 template.json 파일이 검색됩니다. --recursive 옵션을 지정하면 지정된 디렉터리 아래의 모든 template.json 파일과 해당 하위 디렉터리가 입력으로 간주됩니다. + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + 다음 파일에 대한 "export" 명령이 취소되었습니다. "{0}". + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + "export" 명령 실행이 완료되었습니다. {0} 파일이 처리되었습니다. + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + template.js에 대한 지역화 파일을 생성하지 못했습니다. +이유: {0}. +파일 경로: {1}. + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + template.json 파일에 대한 지역화 파일을 생성하지 못했습니다. "{0}". + + + + Localization files were successfully generated for the template.json file at path "{0}". + 경로 "{0}"에서 template.json 파일에 대한 지역화 파일이 생성되었습니다. + + + + Failed to find "template.json" file under the path "{0}". + "{0}” 경로에서 "template.json" 파일을 찾지 못했습니다. + Do not localize: "template.json" + + + Validates the templates at given location. + 지정된 위치에서 템플릿의 유효성을 검사합니다. + + + + The location of template(s) to validate. + 유효성을 검사할 템플릿의 위치입니다. + + + + <no entries> + <no entries> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + 위치 '{0}': {1} 템플릿을 찾았습니다. + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + 템플릿의 위치 '{0}'을(를) 검사하는 중입니다. + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + 템플릿 {0}: 템플릿이 잘못되었습니다. + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + 템플릿 {1}에 대한 지역화 '{0}': 지역화 파일이 잘못되었습니다. 지역화를 건너뜁니다. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + 템플릿 {1}에 대한 '{0}' 지역화: 지역화 파일이 유효합니다. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + 템플릿 {0}: 템플릿이 유효합니다. + {0} - template name and identity in format name (identity) + + + Found template {0}: + 찾은 템플릿 {0}: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + 템플릿 {1}에 대한 지역화 '{0}'을(를) 찾았습니다. + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + 확인에 실패했습니다. + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + {0} 인수를 인식할 수 없습니다. {1} 중 하나여야 합니다. + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + 확인에서 제외할 파일을 정의하는 패턴을 지정합니다. + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + 확인에 포함할 파일을 정의하는 패턴을 지정합니다(지정하지 않은 경우 모든 파일이 포함됨). + + + + Runs the template with specified arguments and compares the result with expectations files. + 지정된 인수로 템플릿을 실행하고 결과를 예상 파일과 비교합니다. + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + true로 설정하면 --exclude-pattern 옵션이 사용되지 않는 한 모든 템플릿 출력 파일이 확인됩니다. + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + true로 설정하면 확인 실패 시 검증자가 Diff 도구를 자동으로 시작하지 않습니다. + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + true로 설정하면 'dotnet new' 명령이 0이 아닌 반환 코드를 반환할 것으로 예상됩니다. + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + 생성된 템플릿을 출력할 대상 디렉터리의 경로를 지정합니다. + + + + Specifies optional scenario name to be used in the snapshot folder name. + 스냅샷 폴더 이름에 사용할 선택적 시나리오 이름을 지정합니다. + + + + Specifies path to the directory with snapshot files. + 스냅샷 파일이 있는 디렉터리의 경로를 지정합니다. + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + 확인할 템플릿의 이름입니다. 이미 설치된 템플릿 또는 -p/--template-path 옵션으로 지정된 로컬 경로 내의 템플릿일 수 있습니다. + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + 확인할 템플릿이 있는 디렉터리의 경로를 지정합니다. + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + 구별해야 하는 시나리오를 표시하여 검증자 기대 디렉터리 명명 규칙을 설정합니다. + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + true로 설정하면 생성된 템플릿 파일과 함께 'dotnet new' 명령 표준 출력 및 오류 내용이 확인됩니다. + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + "{0}" 명령에 오류가 발생했습니다. 자세한 내용은 로그를 참조하세요. + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + "--{0}" 명령을 실행하는 동안 오류가 발생했습니다. 오류 메시지: "{1}". 자세한 내용은 로그를 참조하세요. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.pl.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.pl.xlf new file mode 100644 index 000000000000..14487b37a319 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.pl.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + W przypadku pliku template.config tworzony jest katalog „lokalizować”, który znajduje się obok niego i eksportuje pliki lokalizacji do utworzonego katalogu. Jeśli pliki lokalizacji już istnieją, istniejące tłumaczenia zostaną zachowane. + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + Jeśli ta opcja jest określona, zmodyfikowane pliki nie zostaną zapisane w systemie plików, a zmiany zostaną wydrukowane tylko na danych wyjściowych konsoli. + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + Lista języków, które będą obsługiwane przez ten szablon. Następująca lista języków będzie używana domyślnie, jeśli ta opcja zostanie pominięta: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru,tr, zh-Hans, zh-Hant + + + + When specified, subdirectories are also searched for template.json files. + Jeśli zostanie określone, w podkatalogach są również wyszukiwane pliki template.json. + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + Określa ścieżkę do pliku template.json. Jeśli podano katalog, plik template.json będzie przeszukiwany w katalogu. Jeśli określono opcje --rekursywne, wszystkie pliki template.json w danym katalogu i jego podkatalogach będą traktowane jako dane wejściowe. + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + Polecenie „eksportuj” dla następującego pliku zostało anulowane: „{0}”. + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + Ukończono wykonywanie polecenia „eksportuj”. Przetworzone pliki: {0}. + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + Generowanie plików lokalizacji dla pliku template.json nie powiodło się. +Przyczyna: {0}. +Ścieżka pliku: {1}. + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + Generowanie plików lokalizacji dla następującego pliku template.json nie powiodło się: „{0}”. + + + + Localization files were successfully generated for the template.json file at path "{0}". + Pomyślnie wygenerowano pliki lokalizacji dla pliku template.json w ścieżce „{0}”. + + + + Failed to find "template.json" file under the path "{0}". + Nie można odnaleźć pliku „template.json” pod ścieżką „{0}”. + Do not localize: "template.json" + + + Validates the templates at given location. + Weryfikuje szablony w danej lokalizacji. + + + + The location of template(s) to validate. + Lokalizacja szablonów do zweryfikowania. + + + + <no entries> + <no entries> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + Lokalizacja „{0}”: znalezione szablony: {1}. + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + Skanowanie lokalizacji „{0}” dla szablonów. + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + Szablon {0}: szablon jest nieprawidłowy. + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + Lokalizacja „{0}” dla szablonu {1}: plik lokalizacji jest nieprawidłowy. Lokalizacja zostanie pominięta. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + Lokalizacja „{0}” dla szablonu {1}: plik lokalizacji jest prawidłowy. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + Szablon {0}: szablon jest prawidłowy. + {0} - template name and identity in format name (identity) + + + Found template {0}: + Znaleziono szablon {0}: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + Znaleziono lokalizację „{0}” dla szablonu {1}: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + Weryfikacja nie powiodła się. + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + Nie rozpoznano argumentu {0}. Musi on być jednym z {1}. + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + Określa wzorce definiujące pliki, które mają zostać wykluczone z weryfikacji. + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + Określa wzorce definiujące pliki, które mają być dołączone do weryfikacji (wszystkie pliki są uwzględniane, jeśli nie zostały określone). + + + + Runs the template with specified arguments and compares the result with expectations files. + Uruchamia szablon z określonymi argumentami i porównuje wynik z plikami oczekiwań. + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + W przypadku ustawienia wartości true — wszystkie pliki wyjściowe szablonu zostaną zweryfikowane, chyba że zostanie użyta opcja --exclude-pattern. + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + W przypadku ustawienia wartości true narzędzie różnicowe nie zostanie automatycznie uruchomione przez weryfikator w przypadku niepowodzeń weryfikacji. + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + W przypadku ustawienia wartości true polecenie „dotnet new” zwraca niezerowy kod powrotny. + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + Określa ścieżkę do katalogu docelowego, do którą ma zostać wygenerowany szablon. + + + + Specifies optional scenario name to be used in the snapshot folder name. + Określa opcjonalną nazwę scenariusza, która ma być używana w nazwie folderu migawek. + + + + Specifies path to the directory with snapshot files. + Określa ścieżkę do katalogu z plikami migawki. + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + Nazwa szablonu do zweryfikowania. Można już zainstalować szablon lub szablon w ścieżce lokalnej określonej za pomocą opcji -p/--template-path. + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + Określa ścieżkę do katalogu z szablonem do zweryfikowania. + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + Ustawia konwencję nazewnictwa katalogów oczekiwań weryfikatora, wskazując, które scenariusze powinny być zróżnicowane. + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + W przypadku ustawienia wartości true — standardowe dane wyjściowe polecenia „dotnet new” i zawartość błędów zostaną zweryfikowane wraz z utworzonymi plikami szablonów. + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + Wystąpił błąd przy poleceniu „{0}”. Zobacz dzienniki, aby uzyskać więcej szczegółów. + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + Wystąpił błąd podczas uruchamiania polecenia „--{0}”. Komunikat o błędzie: „{1}”. Zobacz dzienniki, aby uzyskać więcej szczegółów. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.pt-BR.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.pt-BR.xlf new file mode 100644 index 000000000000..5fa5c251093b --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.pt-BR.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + Dado um arquivo template.config, cria um diretório "localizar" próximo a ele e exporta os arquivos de localização para o diretório criado. Se os arquivos de localização já existirem, as traduções existentes serão preservadas. + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + Se esta opção for especificada, os arquivos modificados não serão salvos no sistema de arquivos e as alterações serão impressas apenas na saída do console. + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + A lista de idiomas suportados por este modelo. A lista de idiomas a seguinte será usada como padrão se esta opção for omitida: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + + + + When specified, subdirectories are also searched for template.json files. + Quando especificado, os subdiretórios também são pesquisados por arquivos template.json. + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + Especifica o caminho para o arquivo template.json. Se um diretório for fornecido, o arquivo template.json será pesquisado dentro do diretório. Se opções --recursivas forem especificadas, todos os arquivos template.json no diretório fornecido e seus subdiretórios serão considerados como entrada. + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + O comando "export" para o seguinte arquivo foi cancelado: "{0}". + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + A execução do comando "export" foi concluída. {0} arquivos foram processados. + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + A geração de arquivos de localização para um template.json falhou. +Motivo: {0}. +Caminho do arquivo: {1}. + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + A geração de arquivos de localização para o arquivo template.json a seguir falhou: "{0}". + + + + Localization files were successfully generated for the template.json file at path "{0}". + Os arquivos de localização foram gerados com sucesso para o arquivo template.json no caminho "{0}". + + + + Failed to find "template.json" file under the path "{0}". + Falhou em localizar o arquivo "template.json" no o caminho "{0}". + Do not localize: "template.json" + + + Validates the templates at given location. + Valida os modelos em uma determinada localização. + + + + The location of template(s) to validate. + A localização dos modelos a serem validados. + + + + <no entries> + <no entries> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + Localização ''{0}'': {1} modelos encontrados. + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + Verificando a localização ''{0}'' dos modelos.. + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + Modelo {0}: o modelo não é válido. + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + Localização ''{0}'' do modelo {1}: o arquivo de localização não é válido. A localização será ignorada. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + Localização ''{0}'' para o modelo {1}: o arquivo de localização é válido. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + Modelo {0}: o modelo é válido. + {0} - template name and identity in format name (identity) + + + Found template {0}: + Modelo encontrado {0}: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + Localização ''{0}'' encontrada para o modelo {1}: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + Falha na Verificação. + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + O(s) argumento(s) {0} não é(são) reconhecido(s). Deve ser um destes: {1}. + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + Especifica padrões que definem arquivos a serem excluídos da verificação. + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + Especifica os arquivos de definição de padrão(s) a serem incluídos na verificação (todos os arquivos são incluídos se não forem especificados). + + + + Runs the template with specified arguments and compares the result with expectations files. + Executa o modelo com argumentos especificados e compara o resultado com arquivos de expectativas. + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + Se definido como verdadeiro - todos os arquivos de saída do modelo serão verificados, a menos que a opção --exclude-pattern seja usada. + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + Se definido como verdadeiro - a ferramenta de comparação não será iniciada automaticamente pelo Verificador em caso de falhas de verificação. + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + Se definido como verdadeiro - esperava-se que o comando 'dotnet new' retornasse um código de retorno diferente de zero. + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + Especifica o caminho para o diretório de destino para a saída do modelo gerado. + + + + Specifies optional scenario name to be used in the snapshot folder name. + Especifica o nome do cenário opcional a ser usado no nome da pasta de instantâneo. + + + + Specifies path to the directory with snapshot files. + Especifica o caminho para o diretório com arquivos de instantâneo. + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + Nome do modelo a ser verificado. Pode ser um modelo já instalado ou um modelo dentro do caminho local especificado com a opção -p/--template-path. + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + Especifica o caminho para o diretório com o modelo a ser verificado. + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + Define a convenção de nomenclatura do diretório de expectativas do Verificador, indicando quais cenários devem ser diferenciados. + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + Se definido como verdadeiro - a saída padrão do comando 'dotnet new' e o conteúdo do erro serão verificados juntamente com os arquivos de modelo produzidos. + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + O comando "{0}" encontrou um erro. Veja os logs para mais detalhes. + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + Ocorreu um erro ao executar o comando "--{0}". Mensagem de erro: "{1}". Veja os logs para mais detalhes. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.ru.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.ru.xlf new file mode 100644 index 000000000000..b05fa86be293 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.ru.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + В файле template.config создается каталог "локализации" рядом с файлом и экспортируются файлы локализации в созданный каталог. Если файлы локализации уже существуют, существующие переводы будут сохранены. + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + Если задан этот параметр, измененные файлы не будут сохранены в файловой системе, и изменения будут напечатаны только в выходных данных консоли. + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + Список языков, поддерживаемых этим шаблоном. Если этот параметр пропущен, по умолчанию будет использоваться следующий список языков: английский, испанский, итальянский, китайский (традиционное письмо), китайский (упрощенный), корейский, немецкий, польский, португальский (Бразилия), русский, турецкий, французский, чешский, японский. + + + + When specified, subdirectories are also searched for template.json files. + Если указано, в подкаталогах также выполняется поиск файлов template.json. + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + Указывает путь к файлу template.json. Если задан каталог, файл template.json можно будет найти в каталоге. Если указан параметр --recursive, все файлы template.json в указанном каталоге и его подкаталогах будут считаться входными данными. + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + Команда "export" для следующего файла отменена: "{0}". + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + Выполнение команды "export" завершено. Обработано файлов: {0}. + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + Произошел сбой при создании файлов локализации для template.json. +Причина: {0}. +Путь к файлу: {1}. + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + Не удалось создать файлы локализации для следующего файла template.json: "{0}". + + + + Localization files were successfully generated for the template.json file at path "{0}". + Файлы локализации успешно созданы для файла template.json в пути "{0}". + + + + Failed to find "template.json" file under the path "{0}". + Не удалось найти файл template.json в пути "{0}". + Do not localize: "template.json" + + + Validates the templates at given location. + Проверка шаблонов в заданном расположении. + + + + The location of template(s) to validate. + Проверяемое расположение шаблонов. + + + + <no entries> + <нет записей> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + Расположение "{0}": найдены шаблоны ({1}). + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + Поиск шаблонов в расположении "{0}". + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + Шаблон {0}: недопустимый шаблон. + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + Локализация "{0}" для шаблона {1}: недопустимый файл локализации. Локализация будет пропущена. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + Локализация "{0}" для шаблона {1}: допустимый файл локализации. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + Шаблон {0}: допустимый шаблон. + {0} - template name and identity in format name (identity) + + + Found template {0}: + Найден шаблон {0}: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + Найдена локализация "{0}" для шаблона {1}: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + Проверка не пройдена. + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + Аргументы {0} не распознаны. Требуется один из следующих: {1}. + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + Задает шаблон(ы), определяя файлы, которые следует исключить из проверки. + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + Указывает шаблоны, определяющие файлы, которые включаются в проверку (если не указано, включаются все файлы). + + + + Runs the template with specified arguments and compares the result with expectations files. + Запускает шаблон с указанными аргументами и сравнивает результат с файлами ожиданий. + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + Если задано значение "true", все выходные файлы шаблона будут проходить проверку, если не используется параметр --exclude-pattern. + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + Если задано значение "true", инструмент сравнения не будет автоматически запускаться средством проверки при сбоях проверки. + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + Если задано значение "true", ожидается, что команда "dotnet new" вернет ненулевой код возврата. + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + Указывает путь к целевому каталогу для вывода созданного шаблона. + + + + Specifies optional scenario name to be used in the snapshot folder name. + Указывает необязательное имя сценария, используемое в имени папки моментальных снимков. + + + + Specifies path to the directory with snapshot files. + Указывает путь к каталогу с файлами моментальных снимков. + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + Имя шаблона, который необходимо проверить. Это может быть уже установленный шаблон или шаблон в локальном пути, указанном через параметр -p/--template-path. + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + Указывает путь к каталогу с шаблоном, который следует проверить. + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + Задает соглашение о наименовании каталогов ожиданий средства проверки, указывая, какие сценарии следует различать. + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + Если задано значение "true", стандартный вывод команды "dotnet new" и содержимое ошибок будут проверены вместе с созданными файлами шаблонов. + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + Команда "{0}" обнаружила ошибку. Дополнительные сведения см. в журналах. + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + Произошла ошибка при запуске команды "--{0}". Сообщение об ошибке: "{1}". Дополнительные сведения см. в журналах. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.tr.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.tr.xlf new file mode 100644 index 000000000000..bfad63bb9c40 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.tr.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + template.config dosyası belirtildiğinde, yanında bir "localize" dizini oluşturulur ve yerelleştirme dosyaları oluşturulan dizine aktarılır. Yerelleştirme dosyaları zaten varsa mevcut çeviriler saklanır. + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + Bu seçenek belirtilmişse, değiştirilen dosyalar dosya sistemine kaydedilmez ve değişiklikler yalnızca konsol çıkışına yazdırılır. + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + Bu şablon tarafından desteklenen dillerin listesi. Bu seçenek atlanırsa, şu dil listesi varsayılan olarak kullanılır: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + + + + When specified, subdirectories are also searched for template.json files. + Belirtildiğinde, template.json dosyalarına yönelik alt dizinler de aranır. + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + template.json dosyasının yolunu belirtir. Dizin belirtildiğinde, dizinde template.json dosyası aranır. --recursive options belirtilirse, sağlanan dizin ve alt dizinleri altındaki tüm template.json dosyaları giriş olarak alınır. + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + Şu dosya için "dışarı aktarma" komutu iptal edildi: "{0}". + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + “Dışarı aktarma” komutunun yürütme işlemi tamamlandı. {0} dosyalar işlendi. + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + template.json için yerelleştirme dosyaları oluşturulamadı. +Neden: {0}. +Dosya Yolu: {1}. + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + Şu template.json dosyası için yerelleştirme dosyaları oluşturulamadı: "{0}". + + + + Localization files were successfully generated for the template.json file at path "{0}". + "{0}" yolundaki template.json dosyası için yerelleştirme dosyaları başarıyla oluşturuldu. + + + + Failed to find "template.json" file under the path "{0}". + "{0}" yolunda "template.json" dosyası bulunamadı. + Do not localize: "template.json" + + + Validates the templates at given location. + Belirtilen konumdaki şablonları doğrular. + + + + The location of template(s) to validate. + Doğrulanacak şablonların konumu. + + + + <no entries> + <girdi yok> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + '{0}' konumu: {1} şablon bulundu. + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + Şablonlar için '{0}' konumu taranıyor.. + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + {0} şablonu : şablon geçerli değil. + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + {1} şablonu için '{0}' yerelleştirmesi: yerelleştirme dosyası geçerli değil. Yerelleştirme atlanacak. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + "{1}" şablonu için '{0}' yerelleştirmesi : yerelleştirme dosyası geçerli. + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + {0} şablonu : şablon geçerli. + {0} - template name and identity in format name (identity) + + + Found template {0}: + {0} şablonu bulundu: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + {1} şablonu için '{0}' yerelleştirmesi bulundu: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + Doğrulama Başarısız Oldu. + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + Bağımsız değişken {0} tanınmıyor. Şunlardan biri olmalı: {1} + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + Doğrulamanın dışında tutulacak dosyaları tanımlayan desenleri belirtir. + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + Doğrulamaya dahil edilecek dosyaları tanımlayan kalıpları belirtir (belirtilmemişse tüm dosyalar dahil edilir). + + + + Runs the template with specified arguments and compares the result with expectations files. + Şablonu, belirtilen bağımsız değişkenlerle çalıştırıp sonucu beklenti dosyalarıyla karşılaştırır. + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + True olarak ayarlandığında, -- exclude-pattern seçeneği kullanılmadıkça tüm şablon çıkış dosyaları doğrulanır. + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + True olarak ayarlandığında, Doğrulayıcı, doğrulama hatalarında fark aracını otomatik olarak başlatmaz. + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + True olarak ayarlandığında, - 'dotnet new' komutunun sıfır olmayan dönüş kodu döndürmesi beklenir. + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + Oluşturulan şablonun çıkış yapılacağı hedef dizini yolunu belirtir. + + + + Specifies optional scenario name to be used in the snapshot folder name. + Anlık görüntü klasörü adında kullanılacak isteğe bağlı senaryo adını belirtir. + + + + Specifies path to the directory with snapshot files. + Anlık görüntü dosyaları içeren dizinin yolunu belirtir. + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + Doğrulanacak şablonun adı. Zaten yüklü bir şablon veya -p/--template-path seçeneğiyle belirtilen yerel yolda bir şablon olabilir. + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + Doğrulanacak şablonu içeren dizinin yolunu belirtir. + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + Hangi senaryoların ayrıştırılması gerektiğini belirterek Doğrulayıcı beklentileri dizin adlandırma kuralını ayarlar. + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + True olarak ayarlandığında, - 'dotnet new' komutu standart çıktısı ve hata içerikleri, oluşturulan şablon dosyalarıyla birlikte doğrulanır. + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + "{0}" komutu bir hatayla karşılaştı. Daha fazla ayrıntı için günlüklere bakın. + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + "--{0}" komutu çalıştırılırken bir hata oluştu. Hata iletisi: "{1}". Daha fazla ayrıntı için günlüklere bakın. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.zh-Hans.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.zh-Hans.xlf new file mode 100644 index 000000000000..643dd779bf9b --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.zh-Hans.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + 给定一个 template.config 文件,在其旁边创建一个“本地化”目录,并将本地化文件导出到已创建的目录中。如果本地化文件已存在,将保留现有翻译。 + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + 如果已指定此选项,则不会将修改的文件保存到文件系统,并且更改将仅打印到控制台输出。 + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + 此模板支持的语言列表。如果省略此选项,则使用以下语言列表作为默认值: cs、de、en、es、fr、it、ja、ko、pl、pt-BR、ru、tr、zh-Hans、zh-Hant + + + + When specified, subdirectories are also searched for template.json files. + 指定时,系统还会搜索子目录中的 template.json 文件。 + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + 指定到 template.json 文件的路径。如果给定了目录,系统将在目录中搜索文件 template.json。如果指定了“--recursive”选项,则系统将会提取给定目录及其子目录下的所有 template.json 作为输入。 + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + 已取消以下文件的 “export” 命令:“{0}”。 + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + 已完成执行 “export” 命令。已处理 {0} 个文件。 + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + 为 template.json 生成本地化文件已失败。 +原因: {0}。 +文件路径: {1}。 + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + 为以下 template.json 文件生成本地化文件失败:“{0}”。 + + + + Localization files were successfully generated for the template.json file at path "{0}". + 已成功为位于路径“{0}”的 template.json 文件生成本地化文件。 + + + + Failed to find "template.json" file under the path "{0}". + 在路径“{0}”下未找到“template.json”文件。 + Do not localize: "template.json" + + + Validates the templates at given location. + 在给定位置验证模板。 + + + + The location of template(s) to validate. + 要验证的模板的位置。 + + + + <no entries> + <no entries> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + 位置“{0}”: 找到 {1} 模板。 + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + 正在扫描模板的位置“{0}”。 + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + 模板 {0}: 模板无效。 + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + “{0}”{1}模板的本地化: 本地化文件无效。将跳过本地化。 + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + “{0}”,模板的本地化 {1}: 本地化文件有效。 + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + 模板 {0}: 模板有效。 + {0} - template name and identity in format name (identity) + + + Found template {0}: + 找到的 {0} 模板: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + 已找到模板 {1} 的本地化“{0}”: + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + 验证失败。 + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + 未识别参数 {0}。必须为一个: {1}。 + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + 指定定义要从验证中排除的文件的模式。 + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + 指定用于定义要在验证中包含的文件的模式(如果未指定,则包含所有文件)。 + + + + Runs the template with specified arguments and compares the result with expectations files. + 使用指定的参数运行模板,并将结果与期望文件进行比较。 + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + 如果设置为 true–将验证所有模板输出文件,除非使用了 --exclude-pattern 选项。 + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + 如果设置为 true - 验证程序不会在验证失败时自动启动差异工具。 + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + 如果设置为 true–"dotnet new" 命令应返回非零返回代码。 + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + 指定要将生成的模板输出到的目标目录的路径。 + + + + Specifies optional scenario name to be used in the snapshot folder name. + 指定要在快照文件夹名称中使用的可选方案名称。 + + + + Specifies path to the directory with snapshot files. + 指定包含快照文件的目录的路径。 + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + 要验证的模板的名称。可以是已安装的模板或在使用 -p/--template-path 选项指定的本地路径内的模板。 + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + 指定具有要验证的模板的目录的路径。 + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + 通过指示应区分哪些方案来设置验证程序期望目录命名约定。 + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + 如果设置为 true–"dotnet new" 命令标准输出和错误内容将与生成的模板文件一起验证。 + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + “{0}”命令遇到错误。如需了解详细信息,请参阅日志。 + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + 运行“--{0}”命令时出错。错误消息:“{1}”。如需了解详细信息,请参阅日志。 + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.zh-Hant.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.zh-Hant.xlf new file mode 100644 index 000000000000..fbee80dd39e4 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.CLI/xlf/LocalizableStrings.zh-Hant.xlf @@ -0,0 +1,208 @@ + + + + + + Given a template.config file, creates a "localize" directory next to it and exports the localization files into the created directory. If the localization files already exist, the existing translations will be preserved. + 給定的 template.config 檔案會在檔案旁邊建立 [當地語系化] 目錄,並將當地語系化檔案匯出到建立的目錄中。如果當地語系化檔案已存在,將會保留現有的翻譯。 + The phrase "localization files" here mean the files that will be given to the translators to be localized. + + + If this option is specified, modified files will not be saved to file system and the changes will only be printed to console output. + 如果指定此選項,修改後的檔案將不會儲存至檔案系統,而且只會將變更列印到主控台輸出。 + + + + The list of languages to be supported by this template. The following language list will be used as default if this option is omitted: cs, de, en, es, fr, it, ja, ko, pl, pt-BR, ru, tr, zh-Hans, zh-Hant + 此範本將支援的語言清單。若省略此選項,將會使用下列語言清單作為預設值: cs、de、en、fr、zh、ja、ko、pl、pt-BR、ru、-Hans、zh-Hant + + + + When specified, subdirectories are also searched for template.json files. + 指定時,也會在子目錄中搜尋 template.json 檔案。 + Do not localize: "template.json" + + + Specifies the path to the template.json file. If a directory is given, template.json file will be searched within the directory. If --recursive options is specified, all the template.json files under the given directory and its subdirectories will be taken as input. + 指定 template.json 檔案的路徑。如果指定目錄,將會在目錄內搜尋 template.json 檔案。如果指定--recursive 選項,則會將指定目錄及其子目錄下的所有 template.json 檔案視為輸入。 + Do not localize: "template.json", "--recursive" + + + "export" command for the following file was cancelled: "{0}". + 已取消下列檔案的 "export" 命令: "{0}"。 + Do not localize: "export" + + + Execution of "export" command has completed. {0} files were processed. + 已完成 "export" 命令的執行。已處理 {0} 個檔案。 + {0} is the number of files. Do not localize "export" + + + Generating localization files for a template.json has failed. +Reason: {0}. +File Path: {1}. + 為 template.json 產生當地語系化檔案失敗。 +原因: {0}。 +檔案路徑: {1}。 + {0} is a sentence such as "Json property should contain a field 'ID'" +{1} is a file path such as: C:\temp\template.json +Do not localize: "template.json" + + + Generating localization files for the following template.json file has failed: "{0}". + 在下列 template.json 檔案中產生當地語系化檔案失敗: "{0}"。 + + + + Localization files were successfully generated for the template.json file at path "{0}". + 已成功為位於路徑 "{0}" 上的 template.json 檔案產生當地語系化檔案。 + + + + Failed to find "template.json" file under the path "{0}". + 在路徑 "{0}" 下找不到 "template.json" 檔案。 + Do not localize: "template.json" + + + Validates the templates at given location. + 驗證指定位置的範本。 + + + + The location of template(s) to validate. + 要驗證的範本之位置。 + + + + <no entries> + <no entries> + used when there is no entries in the list + + + Location '{0}': found {1} templates. + 位置 '{0}': 找到了 {1} 個範本。 + {0} - file path to templates to scan; {1} - count of templates found. + + + Scanning location '{0}' for the templates.. + 正在為範本掃描位置 '{0}'.. + {0} - file path to templates to scan + + + Template {0}: the template is not valid. + 範本 {0}: 範本是無效的。 + {0} - template name and identity in format name (identity) + + + '{0}' localization for the template {1}: the localization file is not valid. The localization will be skipped. + 範本 {1} 的 '{0}' 當地語系化: 當地語系化檔案無效。將跳過當地語系化。 + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + '{0}' localization for the template {1}: the localization file is valid. + 範本 {1} 的 '{0}' 當地語系化: 當地語系化檔案是有效的。 + {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Template {0}: the template is valid. + 範本 {0}: 範本是有效的。 + {0} - template name and identity in format name (identity) + + + Found template {0}: + 找到了範本 {0}: + The header is followed by list of issues found in the template. {0} - template name and identity in format name (identity) + + + Found localization '{0}' for template {1}: + 找到範本 {1} 的當地語系化 '{0}': + The header is followed by list of issues found in the template localization. {1} - template name and identity in format name (identity), {0} - locale in format en-US + + + Verification Failed. + 驗證失敗。 + + + + Argument(s) {0} are not recognized. Must be one of: {1}. + 無法辨識引數 {0}。必須為下列之一: {1}。 + {0} and {1} is both a list of arguments + + + Specifies pattern(s) defining files to be excluded from verification. + 指定要從驗證排除的模式定義檔案。 + + + + Specifies pattern(s) defining files to be included to verification (all files are included if not specified). + 指定要包含在驗證中的模式定義檔案 (若未指定則包含所有檔案)。 + + + + Runs the template with specified arguments and compares the result with expectations files. + 使用指定的引數執行範本,並將結果與期望檔案比較。 + + + + If set to true - all template output files will be verified, unless --exclude-pattern option is used. + 如果設為 true - 除非使用 --exclude-pattern 選項,否則會驗證所有範本輸出檔案。 + Do not translate 'true'. Do not translate '--exclude-pattern' + + + If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + 如果設為 true - 驗證失敗時,驗證器不會自動啟動 Diff 工具。 + Do not translate 'true'. + + + If set to true - 'dotnet new' command is expected to return non-zero return code. + 如果設為 true - 'dotnet new' 命令預期傳回非零的傳回碼。 + Do not translate 'true'. Do not translate 'dotnet new' + + + Specifies the path to target directory to output the generated template to. + 指定要輸出所產生範本的目標目錄路徑。 + + + + Specifies optional scenario name to be used in the snapshot folder name. + 指定要在快照集資料夾名稱中使用的選用案例名稱。 + + + + Specifies path to the directory with snapshot files. + 指定快照集檔案的目錄路徑。 + + + + Name of the template to be verified. Can be already installed template or a template within local path specified with -p/--template-path option. + 要驗證的範本名稱。可以是已安裝的範本,或以 -p/--template-path 選項指定之本機路徑內的範本。 + Do not translate the '-p/--template-path' + + + Specifies the path to the directory with template to be verified. + 指定具有要驗證範本的目錄路徑。 + + + + Sets the Verifier expectations directory naming convention, by indicating which scenarios should be differentiated. + 指出應區分哪些案例,以設定驗證器預期目錄命名慣例。 + + + + If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + 如果設為 true - 'dotnet new' 命令標準輸出和錯誤內容會與產生的範本檔案一起進行驗證。 + Do not translate 'true'. Do not translate 'dotnet new' + + + "{0}" command has encountered an error. See the logs for more details. + "{0}" 命令發生錯誤。如需詳細資料,請參閱記錄檔。 + {0} will be replaced by the command name. Such as: "export" command has encountered an error. + + + There was an error while running the "--{0}" command. Error message: "{1}". See the logs for more details. + 執行 "--{0}" 命令時發生錯誤。錯誤訊息: "{1}"。如需詳細資料,請參閱記錄檔。 + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/LocalizableStrings.resx b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/LocalizableStrings.resx new file mode 100644 index 000000000000..c750531a2ea3 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/LocalizableStrings.resx @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + this is subcategory of the error message displayed after build + + + Template localization + this is subcategory of the error message displayed after build + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Microsoft.TemplateEngine.Authoring.Tasks.csproj b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Microsoft.TemplateEngine.Authoring.Tasks.csproj new file mode 100644 index 000000000000..f36611acab8b --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Microsoft.TemplateEngine.Authoring.Tasks.csproj @@ -0,0 +1,53 @@ + + + + $(BundledNETCoreAppTargetFramework);$(NetFrameworkToolCurrent) + MSBuild tasks for template authoring. + true + + true + false + true + tools + $(TargetsForTfmSpecificContentInPackage);AddBuildOutputToPackageNet;AddBuildOutputToPackageNetFramework + + true + + + + false + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Tasks/LocalizeTemplates.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Tasks/LocalizeTemplates.cs new file mode 100644 index 000000000000..0f552a91da01 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Tasks/LocalizeTemplates.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Authoring.Tasks.Utilities; +using Microsoft.TemplateEngine.TemplateLocalizer.Core; + +namespace Microsoft.TemplateEngine.Authoring.Tasks +{ + /// + /// A task that exposes template localization functionality of + /// Microsoft.TemplateEngine.TemplateLocalizer through MSBuild. + /// + public sealed class LocalizeTemplates : Build.Utilities.Task, ICancelableTask + { + private volatile CancellationTokenSource? _cancellationTokenSource; + + /// + /// Gets or sets the path to the template to be localized. + /// + [Required] + public string? TemplateFolder { get; set; } + + /// + /// Gets or sets the value indicating whether the subfolders + /// should be searched for templates. + /// + [Required] + public bool SearchSubfolders { get; set; } = true; + + /// + /// Gets or sets the list of supported languages for which + /// localization files will be created. + /// + public string[]? Languages { get; set; } + + public override bool Execute() + { + if (string.IsNullOrWhiteSpace(TemplateFolder)) + { + Log.LogError(LocalizableStrings.Log_Error_MissingRequiredProperty, nameof(TemplateFolder), nameof(LocalizeTemplates)); + return false; + } + + List templateJsonFiles = GetTemplateJsonFiles(TemplateFolder!, SearchSubfolders).ToList(); + + if (templateJsonFiles.Count == 0) + { + Log.LogError(LocalizableStrings.Localize_Log_TemplateJsonNotFound, TemplateFolder); + return false; + } + + List<(string TemplateJsonPath, Task Task)> runningExportTasks = new(templateJsonFiles.Count); + + using var loggerProvider = new MSBuildLoggerProvider(Log); + ILoggerFactory msbuildLoggerFactory = new LoggerFactory(new[] { loggerProvider }); + using CancellationTokenSource cancellationTokenSource = GetOrCreateCancellationTokenSource(); + CancellationToken cancellationToken = cancellationTokenSource.Token; + + foreach (string templateJsonPath in templateJsonFiles) + { + ExportOptions exportOptions = new(dryRun: false, targetDirectory: null, Languages ?? ExportOptions.DefaultLanguages); + Task exportTask = new TemplateLocalizer.Core.TemplateLocalizer(msbuildLoggerFactory) + .ExportLocalizationFilesAsync(templateJsonPath, exportOptions, cancellationToken); + runningExportTasks.Add((templateJsonPath, exportTask)); + } + + try + { + Task.WhenAll(runningExportTasks.Select(t => t.Task)).Wait(); + } + catch (Exception) + { + // Task.WhenAll will only throw one of the exceptions. We need to log them all. Handle this outside of catch block. + } + + bool failed = false; + foreach ((string TemplateJsonPath, Task Task) pathTaskPair in runningExportTasks) + { + if (pathTaskPair.Task.IsCanceled) + { + Log.LogError(LocalizableStrings.Localize_Log_FileProcessingCancelled, pathTaskPair.TemplateJsonPath); + } + else if (pathTaskPair.Task.IsFaulted) + { + failed = true; + Log.LogErrorFromException(pathTaskPair.Task.Exception, showStackTrace: true, showDetail: true, pathTaskPair.TemplateJsonPath); + } + else + { + // Tasks is known to have already completed. We can get the result without await. + ExportResult result = pathTaskPair.Task.Result; + if (!result.Succeeded) + { + if (!string.IsNullOrWhiteSpace(result.ErrorMessage)) + { + Log.LogError(LocalizableStrings.Localize_Log_ExportTaskFailed, result.TemplateJsonPath, result.ErrorMessage); + } + else if (result.InnerException != null) + { + Log.LogErrorFromException(result.InnerException, showStackTrace: true, showDetail: true, result.TemplateJsonPath); + } + } + failed |= !result.Succeeded; + } + } + + return !failed && !cancellationToken.IsCancellationRequested; + } + + public void Cancel() => GetOrCreateCancellationTokenSource().Cancel(); + + /// + /// Given a , finds and returns all the template.json files. The search rules are executed in the following order: + /// + /// If path points to a template.json file, it is directly returned. + /// If path points to a template directory, path to the "<directory>/.template.config/template.json" file is returned. + /// If path points to a "template.config" directory, path to the "<directory>/template.json" file is returned. + /// If path points to any other directory and is , path to all the + /// ".template.config/template.json" files under the given directory is returned. + /// + /// + /// Path to search for template.json files. + /// Indicates weather the subdirectories should be searched + /// in the case that points to a directory. This parameter has no effect + /// if points to a file. + /// A path for each of the found "template.json" files. + private static IEnumerable GetTemplateJsonFiles(string path, bool searchSubdirectories) + { + if (string.IsNullOrEmpty(path)) + { + yield break; + } + + if (File.Exists(path)) + { + yield return path; + yield break; + } + + if (!Directory.Exists(path)) + { + // This path neither points to a file nor to a directory. + yield break; + } + + if (!searchSubdirectories) + { + string filePath = Path.Combine(path, ".template.config", "template.json"); + if (File.Exists(filePath)) + { + yield return filePath; + } + else + { + filePath = Path.Combine(path, "template.json"); + if (File.Exists(filePath)) + { + yield return filePath; + } + } + + yield break; + } + + foreach (string filePath in Directory.EnumerateFiles(path, "template.json", SearchOption.AllDirectories)) + { + string? directoryName = Path.GetFileName(Path.GetDirectoryName(filePath)); + if (directoryName == ".template.config") + { + yield return filePath; + } + } + } + + private CancellationTokenSource GetOrCreateCancellationTokenSource() + { + if (_cancellationTokenSource != null) + { + return _cancellationTokenSource; + } + + CancellationTokenSource cts = new(); + if (Interlocked.CompareExchange(ref _cancellationTokenSource, cts, null) != null) + { + // Reference was already set. This instance is not needed. + cts.Dispose(); + } + + return _cancellationTokenSource; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Tasks/ValidateTemplates.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Tasks/ValidateTemplates.cs new file mode 100644 index 000000000000..89a67e014dfe --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Tasks/ValidateTemplates.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Authoring.Tasks.Utilities; +using Microsoft.TemplateEngine.Edge; +using Microsoft.TemplateEngine.Edge.Settings; + +namespace Microsoft.TemplateEngine.Authoring.Tasks.Tasks +{ + /// + /// A task that exposes template localization functionality of + /// Microsoft.TemplateEngine.TemplateLocalizer through MSBuild. + /// + public sealed class ValidateTemplates : Build.Utilities.Task, ICancelableTask + { + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + /// + /// Gets or sets the path to the template(s) to be validated. + /// + [Required] + public string? TemplateLocation { get; set; } + + public override bool Execute() + { + if (string.IsNullOrWhiteSpace(TemplateLocation)) + { + Log.LogError(LocalizableStrings.Log_Error_MissingRequiredProperty, nameof(TemplateLocation), nameof(ValidateTemplates)); + return false; + } + + string templateLocation = Path.GetFullPath(TemplateLocation); + + using var loggerProvider = new MSBuildLoggerProvider(Log); + ILoggerFactory msbuildLoggerFactory = new LoggerFactory(new[] { loggerProvider }); + CancellationToken cancellationToken = _cancellationTokenSource.Token; + + try + { + using IEngineEnvironmentSettings settings = SetupSettings(msbuildLoggerFactory); + Scanner scanner = new(settings); + ScanResult scanResult = Task.Run(async () => await scanner.ScanAsync( + templateLocation!, + logValidationResults: false, + returnInvalidTemplates: true, + cancellationToken).ConfigureAwait(false)).GetAwaiter().GetResult(); + + cancellationToken.ThrowIfCancellationRequested(); + + LogResults(scanResult); + return !Log.HasLoggedErrors; + } + catch (Exception ex) + { + Log.LogErrorFromException(ex); + return false; + } + } + + public void Cancel() => _cancellationTokenSource.Cancel(); + + public void Dispose() => _cancellationTokenSource.Dispose(); + + private IEngineEnvironmentSettings SetupSettings(ILoggerFactory loggerFactory) + { + var builtIns = new List<(Type InterfaceType, IIdentifiedComponent Instance)>(); + builtIns.AddRange(Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Components.AllComponents); + builtIns.AddRange(Microsoft.TemplateEngine.Edge.Components.AllComponents); + + ITemplateEngineHost host = new DefaultTemplateEngineHost("template-validator", "1.0", builtIns: builtIns, loggerFactory: loggerFactory); + IEngineEnvironmentSettings engineEnvironmentSettings = new EngineEnvironmentSettings(host, virtualizeSettings: true); + + return engineEnvironmentSettings; + } + + private void LogResults(ScanResult scanResult) + { + Log.LogMessage(LocalizableStrings.Validate_Log_FoundTemplate, scanResult.MountPoint.MountPointUri, scanResult.Templates.Count); + foreach (IScanTemplateInfo template in scanResult.Templates) + { + LogValidationEntries(LocalizableStrings.Validate_Log_TemplateConfiguration_Subcategory, template.ValidationErrors); + foreach (KeyValuePair locator in template.Localizations) + { + ILocalizationLocator localizationInfo = locator.Value; + LogValidationEntries(LocalizableStrings.Validate_Log_TemplateLocalization_Subcategory, localizationInfo.ValidationErrors); + } + } + + void LogValidationEntries(string subCategory, IReadOnlyList errors) + { + foreach (IValidationEntry error in errors.OrderByDescending(e => e.Severity)) + { + switch (error.Severity) + { + case IValidationEntry.SeverityLevel.Error: + Log.LogError( + subcategory: subCategory, + errorCode: error.Code, + helpKeyword: string.Empty, + file: error.Location?.Filename ?? string.Empty, + lineNumber: error.Location?.LineNumber ?? 0, + columnNumber: error.Location?.Position ?? 0, + endLineNumber: 0, + endColumnNumber: 0, + message: error.ErrorMessage); + break; + case IValidationEntry.SeverityLevel.Warning: + Log.LogWarning( + subcategory: subCategory, + warningCode: error.Code, + helpKeyword: string.Empty, + file: error.Location?.Filename ?? string.Empty, + lineNumber: error.Location?.LineNumber ?? 0, + columnNumber: error.Location?.Position ?? 0, + endLineNumber: 0, + endColumnNumber: 0, + message: error.ErrorMessage); + break; + case IValidationEntry.SeverityLevel.Info: + Log.LogMessage( + subcategory: subCategory, + code: error.Code, + helpKeyword: string.Empty, + file: error.Location?.Filename ?? string.Empty, + lineNumber: error.Location?.LineNumber ?? 0, + columnNumber: error.Location?.Position ?? 0, + endLineNumber: 0, + endColumnNumber: 0, + MessageImportance.High, + message: error.ErrorMessage); + break; + } + } + } + + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Utilities/MSBuildLogger.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Utilities/MSBuildLogger.cs new file mode 100644 index 000000000000..ec7473fa57b6 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Utilities/MSBuildLogger.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Utilities; +using Microsoft.Extensions.Logging; + +namespace Microsoft.TemplateEngine.Authoring.Tasks.Utilities +{ + /// + /// Implements an ILogger that passes the logs to the wrapped TaskLoggingHelper. + /// + internal sealed class MSBuildLogger : ILogger + { + private static readonly IDisposable Scope = new DummyDisposable(); + + private readonly string _categoryHeader; + private readonly TaskLoggingHelper _loggingHelper; + + public MSBuildLogger(string category, TaskLoggingHelper loggingHelperToWrap) + { + _categoryHeader = category + ": "; + _loggingHelper = loggingHelperToWrap; + } + + public IDisposable BeginScope(TState state) where TState : notnull + => Scope; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + switch (logLevel) + { + case LogLevel.Trace: + case LogLevel.Debug: + case LogLevel.Information: + _loggingHelper.LogMessage(_categoryHeader + formatter(state, exception)); + break; + case LogLevel.Warning: + _loggingHelper.LogWarning(_categoryHeader + formatter(state, exception)); + break; + case LogLevel.Error: + case LogLevel.Critical: + _loggingHelper.LogError(_categoryHeader + formatter(state, exception)); + break; + case LogLevel.None: + break; + default: + break; + } + } + + /// + /// A simple disposable to describe scopes with . + /// + private sealed class DummyDisposable : IDisposable + { + public void Dispose() { } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Utilities/MSBuildLoggerProvider.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Utilities/MSBuildLoggerProvider.cs new file mode 100644 index 000000000000..a0dda0b7e598 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/Utilities/MSBuildLoggerProvider.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Utilities; +using Microsoft.Extensions.Logging; + +namespace Microsoft.TemplateEngine.Authoring.Tasks.Utilities +{ + /// + /// An that creates s which passes + /// all the logs to MSBuild's . + /// + internal class MSBuildLoggerProvider : ILoggerProvider + { + private readonly TaskLoggingHelper _loggingHelper; + + public MSBuildLoggerProvider(TaskLoggingHelper loggingHelperToWrap) + { + _loggingHelper = loggingHelperToWrap; + } + + public ILogger CreateLogger(string categoryName) + { + return new MSBuildLogger(categoryName, _loggingHelper); + } + + public void Dispose() { } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/build/Microsoft.TemplateEngine.Authoring.Tasks.props b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/build/Microsoft.TemplateEngine.Authoring.Tasks.props new file mode 100644 index 000000000000..4058c0537abb --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/build/Microsoft.TemplateEngine.Authoring.Tasks.props @@ -0,0 +1,36 @@ + + + + + true + + + + . + + + false + + + . + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/build/Microsoft.TemplateEngine.Authoring.Tasks.targets b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/build/Microsoft.TemplateEngine.Authoring.Tasks.targets new file mode 100644 index 000000000000..78f4c9382ad0 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/build/Microsoft.TemplateEngine.Authoring.Tasks.targets @@ -0,0 +1,28 @@ + + + + + $(MSBuildThisFileDirectory)..\tools\net\Microsoft.TemplateEngine.Authoring.Tasks.dll + $(MSBuildThisFileDirectory)..\tools\netframework\Microsoft.TemplateEngine.Authoring.Tasks.dll + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.cs.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.cs.xlf new file mode 100644 index 000000000000..8aec6cb5435e --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.cs.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + Nepovedlo se exportovat lokalizační soubory pro soubor template.json v cestě {0}: {1}. + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + LocalizeTemplates zastavil zpracování následujícího souboru, protože operace byla zrušena: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + Soubor template.json se v cestě {0} nepodařilo najít. + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + Vlastnost „{0}“ by měla být nastavena pro cíl „{1}“. + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + Umístění „{0}“: nalezen tento počet šablon: {1}. + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + Konfigurace šablony + this is subcategory of the error message displayed after build + + + Template localization + Lokalizace šablony + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.de.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.de.xlf new file mode 100644 index 000000000000..2efb7da43d33 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.de.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + Fehler beim Exportieren der Lokalisierungsdateien für die Datei \"template.json\" unter dem Pfad “{0}”: {1}. + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + \"LocalizeTemplates\" hat die Verarbeitung der folgenden Datei beendet, da der Vorgang abgebrochen wurde: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + Die Datei \"template.json\" konnte unter dem Pfad \"{0}\" wurde nicht gefunden. + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + Eigenschaft „{0}“ sollte für das Ziel „{1}“ festgelegt werden. + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + Speicherort „{0}“: {1} Vorlagen gefunden. + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + Vorlagenkonfiguration + this is subcategory of the error message displayed after build + + + Template localization + Vorlagenlokalisierung + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.es.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.es.xlf new file mode 100644 index 000000000000..d7f71bbbfbc2 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.es.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + No se pudieron exportar los archivos de localización del archivo 'template.json' en la ruta de acceso '{0}': {1}. + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + 'LocalizeTemplates' dejó de procesar el siguiente archivo porque se canceló la operación: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + No se pudo encontrar el archivo 'template.json' bajo la ruta de acceso '{0}'. + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + La propiedad "{0}" debe establecerse para el destino "{1}". + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + Ubicación "{0}": se encontraron {1} plantillas. + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + Configuración de plantilla + this is subcategory of the error message displayed after build + + + Template localization + Localización de plantilla + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.fr.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.fr.xlf new file mode 100644 index 000000000000..ff4b5350c0d9 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.fr.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + Échec de l’exportation des fichiers de localisation pour le fichier 'template.json' sous le chemin d’accès '{0}' : {1} + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + « LocalizeTemplates » a arrêté le traitement du fichier suivant, car l’opération a été annulée : {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + Impossible de trouver le fichier 'template.json' dans le chemin '{0}' + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + La propriété '{0}' doit être définie pour la cible '{1}'. + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + Emplacement '{0}' : {1} modèles trouvés. + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + Configuration du modèle + this is subcategory of the error message displayed after build + + + Template localization + Localisation des modèles + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.it.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.it.xlf new file mode 100644 index 000000000000..e9034d2da59b --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.it.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + Non è stato possibile esportare i file di localizzazione per il file 'template.json' nel percorso '{0}': {1}. + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + 'LocalizeTemplates' ha interrotto l'elaborazione del file seguente perché l'operazione è stata annullata: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + Non è stato possibile trovare il file ‘template.json’ nel percorso ‘{0}’. + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + La proprietà '{0}' deve essere impostata per la destinazione '{1}'. + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + Percorso '{0}': {1} modelli trovati. + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + Configurazione del modello + this is subcategory of the error message displayed after build + + + Template localization + Localizzazione dei modelli + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.ja.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.ja.xlf new file mode 100644 index 000000000000..19538f9671fc --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.ja.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + パス '{0}' の下にある 'template.json' ファイルのローカライズ ファイルをエクスポートできませんでした。{1}。 + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + 操作がキャンセルされたため、'LocalizeTemplates' は次のファイルの処理を停止しました: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + パス '{0}' の下に \"template.json\" ファイルが見つかりません。 + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + プロパティ '{0}' は '{1}' ターゲットに設定する必要があります。 + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + 場所 '{0}': {1} テンプレートが見つかりました。 + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + テンプレートの構成 + this is subcategory of the error message displayed after build + + + Template localization + テンプレートのローカライズ + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.ko.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.ko.xlf new file mode 100644 index 000000000000..8c4b4a2e5517 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.ko.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + '{0}' 경로의 'template.json' 파일에 대한 지역화 파일을 내보내지 못했습니다. {1}. + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + 작업이 취소되었기 때문에 'LocalizeTemplates'가 {0} 파일 처리를 중지했습니다. + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + ‘{0}’ 경로에서 ‘template.json’ 파일을 찾지 못했습니다. + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + ‘{0}’ 속성을 ‘{1}’에 대해 설정해야 합니다. + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + 위치 '{0}': {1} 템플릿을 찾았습니다. + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + 템플릿 구성 + this is subcategory of the error message displayed after build + + + Template localization + 템플릿 지역화 + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.pl.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.pl.xlf new file mode 100644 index 000000000000..9e53ef9b519b --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.pl.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + Nie można wyeksportować plików lokalizacji dla pliku „template.json” w ścieżce „{0}”: {1}. + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + Element „LocalizeTemplates” zatrzymał przetwarzanie następującego pliku, ponieważ operacja została anulowana: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + Nie można odnaleźć pliku „template.json” w ścieżce „{0}”. + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + Właściwość „{0}” powinna być ustawiona dla elementu docelowego „{1}”. + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + Lokalizacja „{0}”: znalezione szablony: {1}. + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + Konfiguracja szablonu + this is subcategory of the error message displayed after build + + + Template localization + Lokalizacja szablonu + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.pt-BR.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.pt-BR.xlf new file mode 100644 index 000000000000..020198249a70 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.pt-BR.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + Falha ao exportar os arquivos de localização para o arquivo 'template.json' no caminho '{0}': {1}. + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + A operação foi cancelada porque 'LocalizeTemplates' parou de processar o seguinte arquivo: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + Falha ao encontrar o arquivo 'template.json' no caminho '{0}'. + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + A propriedade ''{0}'' deve ser definida para o destino ''{1}''. + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + Localização ''{0}'': {1} modelos encontrados. + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + Configuração do modelo + this is subcategory of the error message displayed after build + + + Template localization + Localização do modelo + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.ru.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.ru.xlf new file mode 100644 index 000000000000..819efe5402b0 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.ru.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + Не удалось экспортировать файлы локализации для файла \"template.json\" по пути \"{0}\": {1}. + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + Обработка следующего файла в \"LocalizeTemplates\" остановлена, поскольку операция была отменена: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + Не удалось найти файл \"template.json\" по пути \"{0}\". + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + Необходимо задать свойство "{0}" для целевого объекта "{1}". + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + Расположение "{0}": найдены шаблоны ({1}). + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + Конфигурация шаблона + this is subcategory of the error message displayed after build + + + Template localization + Локализация шаблона + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.tr.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.tr.xlf new file mode 100644 index 000000000000..a76478d988f4 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.tr.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + '{0}' yolundaki 'template.json' dosyası için şu yerelleştirme dosyaları dışarı aktarılamadı: {1}. + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + 'LocalizeTemplates', işlem iptal edildiği için şu dosyayı işlemeyi durdurdu: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + '{0}' yolunda 'template.json' dosyası bulunamadı. + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + '{1}' hedefi için '{0}' özelliği ayarlanmalıdır. + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + '{0}' konumu: {1} şablon bulundu. + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + Şablon yapılandırması + this is subcategory of the error message displayed after build + + + Template localization + Şablon yerelleştirme + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.zh-Hans.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.zh-Hans.xlf new file mode 100644 index 000000000000..bc6e5a3ec6ff --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.zh-Hans.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + 无法在路径“{0}”下导出“template.json”文件的本地化文件: {1}。 + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + “LocalizeTemplates”已停止处理以下文件,因为操作被取消: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + 在路径“{0}”下未找到“template.json”文件。 + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + 应为“{1}”目标设置属性“{0}”。 + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + 位置“{0}”: 找到 {1} 模板。 + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + 模板配置 + this is subcategory of the error message displayed after build + + + Template localization + 模板本地化 + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.zh-Hant.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.zh-Hant.xlf new file mode 100644 index 000000000000..2d8421801821 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.Tasks/xlf/LocalizableStrings.zh-Hant.xlf @@ -0,0 +1,43 @@ + + + + + + Failed to export localization files for 'template.json' file under the path '{0}': {1}. + 無法在路徑 '{0}': {1} 下匯出 'template.json' 檔案的當地語系化檔案。 + Do not localize: 'template.json' + + + 'LocalizeTemplates' stopped processing the following file, because the operation was cancelled: {0} + 'LocalizeTemplates' 已停止處理下列檔案,因為作業已取消: {0} + Do not localize: 'LocalizeTemplates'. +{0} is the full path to a file, such as 'C:/Users/someguy/Desktop/template.json' + + + Failed to find 'template.json' file under the path '{0}'. + 在路徑 ‘{0}’ 下找不到 ‘template.json’ 檔案。 + Do not localize: 'template.json' + + + The property '{0}' should be set for '{1}' target. + 應該為 '{1}' 目標設定屬性 '{0}'。 + 'Target' and 'property' are referring to MSBuild concepts with the same names. + + + Location '{0}': found {1} templates. + 位置 '{0}': 找到了 {1} 個範本。 + {0} - path configured for scanning the templates, {1} - the number of templates found. + + + Template configuration + 範本設定 + this is subcategory of the error message displayed after build + + + Template localization + 範本當地語系化 + this is subcategory of the error message displayed after build + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.XunitV3.csproj b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.XunitV3.csproj new file mode 100644 index 000000000000..2d4d285746ef --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.XunitV3.csproj @@ -0,0 +1,30 @@ + + + + + + $(DefineConstants);XUNIT_V3 + Microsoft.TemplateEngine.Authoring.TemplateApiVerifier + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/LocalizableStrings.resx b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/LocalizableStrings.resx new file mode 100644 index 000000000000..ba0a4e6465eb --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/LocalizableStrings.resx @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The template "{0}" was created successfully. + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.Shared.props b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.Shared.props new file mode 100644 index 000000000000..f09e43958a25 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.Shared.props @@ -0,0 +1,20 @@ + + + + $(NetMinimum);$(NetCurrent) + The extension of templates verification engine enabling verification testing through dotnet template engine API. + true + true + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.csproj b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.csproj new file mode 100644 index 000000000000..8d392054d3f5 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/PublicAPI.Shipped.txt b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..91b0e1a43b98 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/PublicAPI.Unshipped.txt b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..99d9cafb120c --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.TemplateVerifierOptionsExtensions +static Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.TemplateVerifierOptionsExtensions.WithInstantiationThroughTemplateCreatorApi(this Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions! options, Microsoft.TemplateEngine.Edge.Template.InputDataSet? inputDataSet) -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions! +static Microsoft.TemplateEngine.Authoring.TemplateApiVerifier.TemplateVerifierOptionsExtensions.WithInstantiationThroughTemplateCreatorApi(this Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions! options, System.Collections.Generic.IReadOnlyDictionary? inputParameters) -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions! \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/TemplateVerifierOptionsExtensions.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/TemplateVerifierOptionsExtensions.cs new file mode 100644 index 000000000000..097acd20a6d5 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/TemplateVerifierOptionsExtensions.cs @@ -0,0 +1,164 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands; +using Microsoft.TemplateEngine.Edge; +using Microsoft.TemplateEngine.Edge.Template; +using Microsoft.TemplateEngine.IDE; +using WellKnownSearchFilters = Microsoft.TemplateEngine.Utils.WellKnownSearchFilters; + +namespace Microsoft.TemplateEngine.Authoring.TemplateApiVerifier +{ + public static class TemplateVerifierOptionsExtensions + { + /// + /// Adds custom template instantiator that runs template instantiation in-proc via TemplateCreator API. + /// + /// + /// + /// + public static TemplateVerifierOptions WithInstantiationThroughTemplateCreatorApi( + this TemplateVerifierOptions options, + IReadOnlyDictionary? inputParameters) + { + return options.WithCustomInstatiation(async verifierOptions => await RunInstantiation(verifierOptions, inputParameters, null).ConfigureAwait(false)); + } + + /// + /// Adds custom template instantiator that runs template instantiation in-proc via TemplateCreator API. + /// + /// + /// + /// + public static TemplateVerifierOptions WithInstantiationThroughTemplateCreatorApi( + this TemplateVerifierOptions options, + InputDataSet? inputDataSet) + { + return options.WithCustomInstatiation(async verifierOptions => await RunInstantiation(verifierOptions, null, inputDataSet).ConfigureAwait(false)); + } + + private static async Task RunInstantiation( + TemplateVerifierOptions options, + IReadOnlyDictionary? inputParameters, + InputDataSet? inputDataSet) + { + if (!string.IsNullOrEmpty(options.DotnetExecutablePath)) + { + throw new TemplateVerificationException(LocalizableStrings.Error_DotnetPath, TemplateVerificationErrorCode.InvalidOption); + } + + if (options.TemplateSpecificArgs != null) + { + throw new TemplateVerificationException(LocalizableStrings.Error_TemplateArgsDisalowed, TemplateVerificationErrorCode.InvalidOption); + } + + // Create temp folder and instantiate there + string workingDir = options.OutputDirectory ?? Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + if (Directory.Exists(workingDir) && Directory.EnumerateFileSystemEntries(workingDir).Any()) + { + throw new TemplateVerificationException(TemplateVerifier.LocalizableStrings.VerificationEngine_Error_WorkDirExists, TemplateVerificationErrorCode.WorkingDirectoryExists); + } + + if (!ExtractParameter(inputParameters, inputDataSet, "name", out string? name) && + !ExtractParameter(inputParameters, inputDataSet, "n", out name)) + { + name = options.TemplateName; + } + + if (!ExtractParameter(inputParameters, inputDataSet, "output", out string? output) && + !ExtractParameter(inputParameters, inputDataSet, "o", out output)) + { + output = options.TemplateName; + } + + string outputPath = Path.Combine(workingDir, output!); + + var host = new DefaultTemplateEngineHost( + nameof(TemplateVerifierOptionsExtensions), + "1.0.0", + null, + new List<(Type, IIdentifiedComponent)>(), + []); + + var bootstrapper = new Bootstrapper( + host, + virtualizeConfiguration: string.IsNullOrEmpty(options.SettingsDirectory), + loadDefaultComponents: true, + hostSettingsLocation: options.SettingsDirectory, + environment: new VirtualEnvironment(options.Environment, true)); + + if (!string.IsNullOrEmpty(options.TemplatePath)) + { + await InstallTemplateAsync(bootstrapper, options.TemplatePath).ConfigureAwait(false); + } + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter(options.TemplateName) }).ConfigureAwait(false); + + if (foundTemplates.Count == 0) + { + throw new TemplateVerificationException(LocalizableStrings.Error_NoPackages, TemplateVerificationErrorCode.InstallFailed); + } + + ITemplateInfo template = foundTemplates[0].Info; + + ITemplateCreationResult result = await + (inputDataSet != null + ? bootstrapper.CreateAsync( + info: template, + name: name, + inputParameters: inputDataSet, + outputPath: outputPath) + : bootstrapper.CreateAsync( + info: template, + name: name, + parameters: inputParameters ?? new Dictionary(), + outputPath: outputPath)).ConfigureAwait(false); + + return new CommandResultData( + (int)result.Status, + result.Status == CreationResultStatus.Success ? string.Format(LocalizableStrings.CreateSuccessful, result.TemplateFullName) : string.Empty, + result.ErrorMessage ?? string.Empty, + // We do not want to use result.OutputBaseDirectory as it points to the base of template + // not a working dir of command (which is one level up - as we explicitly specify output subdir, as if '-o' was passed) + workingDir); + } + + private static async Task InstallTemplateAsync(Bootstrapper bootstrapper, string template) + { + List installRequests = new List() { new InstallRequest(Path.GetFullPath(template)) }; + + IReadOnlyList installationResults = await bootstrapper.InstallTemplatePackagesAsync(installRequests).ConfigureAwait(false); + InstallResult? failedResult = installationResults.FirstOrDefault(result => !result.Success); + if (failedResult != null) + { + throw new TemplateVerificationException(string.Format("Failed to install template: {0}, details:{1}", failedResult.InstallRequest.PackageIdentifier, failedResult.ErrorMessage), TemplateVerificationErrorCode.InstallFailed); + } + } + + private static bool ExtractParameter( + IReadOnlyDictionary? inputParameters, + InputDataSet? inputDataSet, + string key, + out string? value) + { + value = null; + if ( + inputDataSet != null && + inputDataSet.ParameterDefinitionSet.TryGetValue(key, out ITemplateParameter parameter) && + inputDataSet.TryGetValue(parameter, out InputParameterData parameterValue) && + parameterValue.Value != null) + { + value = parameterValue.Value.ToString(); + } + else + { + inputParameters?.TryGetValue(key, out value); + } + + return !string.IsNullOrEmpty(value); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.cs.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.cs.xlf new file mode 100644 index 000000000000..b71e60800e89 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.cs.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + Šablona {0} se úspěšně vytvořila. + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + Konfigurace šablony: {0} neexistuje. Při použití WithInstantiationThroughTemplateCreatorApi musí parametr TemplatePath určovat cestu k souboru template.json nebo ke kořenovému adresáři šablony (obsahující {1}). + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + Konfigurační soubor šablony nelze načíst z nakonfigurovaných přípojných bodů. + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + Při použití withInstantiationThroughTemplateCreatorApi nesmí být zadán parametr DotnetExecutablePath. + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + Nepodařilo se nainstalovat šablonu: {0}, podrobnosti: {1}. + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + Po instalaci se nenačetl žádný balíček. + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + Při použití withInstantiationThroughTemplateCreatorApi nesmí být zadán parametr TemplateSpecificArgs. Parametry by se měly předávat prostřednictvím argumentu WithInstantiationThroughTemplateCreatorApi. + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.de.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.de.xlf new file mode 100644 index 000000000000..ac81bd108442 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.de.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + Die Vorlage "{0}" wurde erfolgreich erstellt. + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + Vorlagenkonfiguration: {0} ist nicht vorhanden. Bei Verwendung von „WithInstantiationThroughTemplateCreatorApi“ muss der Parameter „TemplatePath“ den Pfad zur Datei template.json oder zum Stamm der Vorlage angeben (die {1} enthält). + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + Die Vorlagenkonfigurationsdatei konnte nicht vom konfigurierten Bereitstellungspunkt abgerufen werden. + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + Der Parameter „DotnetExecutablePath“ darf nicht angegeben werden, wenn „WithInstantiationThroughTemplateCreatorApi“ verwendet wird. + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + Fehler beim Installieren der Vorlage: {0}, Details: {1}. + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + Nach der Installation wurden keine Pakete abgerufen. + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + Der Parameter „TemplateSpecificArgs“ darf nicht angegeben werden, wenn WithInstantiationThroughTemplateCreatorApi verwendet wird. Parameter sollten über das Argument von „WithInstantiationThroughTemplateCreatorApi“ übergeben werden. + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.es.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.es.xlf new file mode 100644 index 000000000000..8d5f93d54594 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.es.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + La plantilla "{0}" se creó correctamente. + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + Configuración de plantilla: {0} no existe. Cuando se usa "WithInstantiationThroughTemplateCreatorApi", el parámetro "TemplatePath" debe especificar la ruta de acceso al archivo template.json o a la raíz de la plantilla (que contiene {1}). + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + No se pudo recuperar el archivo de configuración de plantilla desde el punto de montaje configurado. + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + No se debe especificar el parámetro "DotnetExecutablePath" cuando se usa "WithInstantiationThroughTemplateCreatorApi". + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + No se pudo instalar la plantilla: {0}, detalles: {1}. + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + No se capturaron paquetes después de la instalación. + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + No se debe especificar el parámetro "TemplateSpecificArgs" al usar WithInstantiationThroughTemplateCreatorApi. Los parámetros deben pasarse mediante el argumento de "WithInstantiationThroughTemplateCreatorApi". + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.fr.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.fr.xlf new file mode 100644 index 000000000000..c720788c8c1f --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.fr.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + Le modèle « {0} » a bien été créé. + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + Configuration du modèle : {0} n’existe pas. Quand vous utilisez 'WithInstantiationThroughTemplateCreatorApi', le paramètre 'TemplatePath' doit spécifier le chemin d’accès à template.json ou à la racine du modèle (contenant {1}). + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + Impossible d’extraire le fichier de configuration de modèle à partir du point de montage configuré. + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + Le paramètre 'DotnetExecutablePath' ne doit pas être spécifié lors de l’utilisation de 'WithInstantiationThroughTemplateCreatorApi'. + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + Échec de l’installation du modèle : {0}, détails : {1}. + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + Aucun package récupéré après l’installation. + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + Le paramètre 'TemplateSpecificArgs' ne doit pas être spécifié lors de l’utilisation de WithInstantiationThroughTemplateCreatorApi. Les paramètres doivent être passés via l’argument 'WithInstantiationThroughTemplateCreatorApi'. + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.it.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.it.xlf new file mode 100644 index 000000000000..5d033628f9f0 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.it.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + Creazione del modello "{0}" completata. + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + Configurazione del modello: {0} non esiste. Quando si usa 'WithInstantiationThroughTemplateCreatorApi', il parametro 'TemplatePath' deve specificare il percorso del file template.json o della radice del modello (contenente {1}). + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + Impossibile recuperare il file di configurazione del modello dal punto di montaggio configurato. + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + Non è necessario specificare il parametro 'DotnetExecutablePath' quando si usa 'WithInstantiationThroughTemplateCreatorApi'. + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + Non è stato possibile installare il modello: {0}, dettagli: {1}. + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + Non sono stati recuperati pacchetti dopo l'installazione. + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + Non è necessario specificare il parametro 'TemplateSpecificArgs' quando si usa WithInstantiationThroughTemplateCreatorApi. I parametri devono essere passati tramite l'argomento di 'WithInstantiationThroughTemplateCreatorApi'. + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.ja.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.ja.xlf new file mode 100644 index 000000000000..b239b1ca6c6f --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.ja.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + テンプレート "{0}" が正常に作成されました。 + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + テンプレート構成: {0} が存在しません。'WithInstantiationThroughTemplateCreatorApi' を使用する場合、'TemplatePath' パラメーターには template.json へのパスまたはテンプレートのルート ({1} を含む) へのパスを指定する必要があります。 + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + 構成されたマウント ポイントからテンプレート構成ファイルを取得できませんでした。 + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + 'WithInstantiationThroughTemplateCreatorApi' を使用する場合、'DotnetExecutablePath' パラメーターを指定することはできません。 + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + テンプレートをインストールできませんでした: {0}、詳細: {1}。 + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + インストール後に取得されたパッケージはありません。 + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + WithInstantiationThroughTemplateCreatorApi を使用する場合、'TemplateSpecificArgs' パラメーターを指定することはできません。パラメーターは、'WithInstantiationThroughTemplateCreatorApi' の引数を介して渡す必要があります。 + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.ko.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.ko.xlf new file mode 100644 index 000000000000..89e5bedc59d6 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.ko.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + "{0}" 템플릿이 성공적으로 생성되었습니다. + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + 템플릿 구성: {0}이(가) 없습니다. 'WithInstantiationThroughTemplateCreatorApi'를 사용하는 경우 'TemplatePath' 매개 변수는 template.json 또는 템플릿의 루트({1} 포함)에 대한 경로를 지정해야 합니다. + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + 구성된 탑재 지점에서 템플릿 구성 파일을 검색할 수 없습니다. + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + 'WithInstantiationThroughTemplateCreatorApi'를 사용하는 경우 'DotnetExecutablePath' 매개 변수를 지정하지 않아야 합니다. + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + 템플릿을 설치하지 못했습니다. {0}, 세부 정보: {1}. + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + 설치 후 가져온 패키지가 없습니다. + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + WithInstantiationThroughTemplateCreatorApi를 사용하는 경우 'TemplateSpecificArgs' 매개 변수를 지정하지 않아야 합니다. 매개 변수는 'WithInstantiationThroughTemplateCreatorApi'의 인수를 통해 전달해야 합니다. + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.pl.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.pl.xlf new file mode 100644 index 000000000000..c647855f98f6 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.pl.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + Pomyślnie utworzono szablon "{0}". + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + Konfiguracja szablonu: {0} nie istnieje. W przypadku używania parametru „WithInstantiationThroughTemplateCreatorApi” parametr „TemplatePath” musi określać ścieżkę do pliku template.json lub katalogu głównego szablonu (zawierającego {1}). + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + Nie można pobrać pliku konfiguracji szablonu ze skonfigurowanego punktu instalacji. + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + Nie można określić parametru „DotnetExecutablePath” w przypadku używania parametru „WithInstantiationThroughTemplateCreatorApi”. + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + Nie można zainstalować szablonu: {0}, szczegóły: {1}. + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + Po instalacji nie pobrano żadnych pakietów. + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + Nie można określić parametru „TemplateSpecificArgs” w przypadku używania parametru WithInstantiationThroughTemplateCreatorApi. Parametry powinny być przekazywane za pomocą argumentu „WithInstantiationThroughTemplateCreatorApi”. + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.pt-BR.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.pt-BR.xlf new file mode 100644 index 000000000000..8553f5f8f070 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.pt-BR.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + O modelo "{0}" foi criado com êxito. + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + Configuração do modelo: {0} não existe. Ao usar "WithInstantiationThroughTemplateCreatorApi", o parâmetro "TemplatePath" deve especificar o caminho para o template.json ou para a raiz do modelo (contendo {1}). + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + Não foi possível recuperar o arquivo de configuração do modelo do ponto de montagem configurado. + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + O parâmetro "DotnetExecutablePath" não deve ser especificado ao usar "WithInstantiationThroughTemplateCreatorApi". + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + Falha ao instalar o modelo: {0}, detalhes: {1}. + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + Nenhum pacote buscado após a instalação. + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + O parâmetro "TemplateSpecificArgs" não deve ser especificado ao usar WithInstantiationThroughTemplateCreatorApi. Os parâmetros devem ser passados por meio do argumento "WithInstantiationThroughTemplateCreatorApi". + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.ru.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.ru.xlf new file mode 100644 index 000000000000..e9bb49649c62 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.ru.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + Шаблон "{0}" успешно создан. + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + Конфигурация шаблона: {0} не существует. При использовании параметра "WithInstantiationThroughTemplateCreatorApi" параметр "TemplatePath" должен указывать путь к файлу template.json или корню шаблона (содержащему {1}). + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + Не удалось получить файл конфигурации шаблона из настроенной точки подключения. + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + Параметр "DotnetExecutablePath" не должен быть указан при использовании "WithInstantiationThroughTemplateCreatorApi". + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + Не удалось установить шаблон: {0}, подробные сведения: {1}. + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + Пакеты не получены после установки. + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + Параметр "TemplateSpecificArgs" не должен быть указан при использовании "WithInstantiationThroughTemplateCreatorApi". Параметры должны передаваться с помощью аргумента "WithInstantiationThroughTemplateCreatorApi". + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.tr.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.tr.xlf new file mode 100644 index 000000000000..482d25071c18 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.tr.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + "{0}" şablonu başarıyla oluşturuldu. + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + Şablon yapılandırması: {0} yok. 'WithInstantiationThroughTemplateCreatorApi' kullanırken 'TemplatePath' parametresi, template.json yolunu veya şablonun kök yolunu belirtmelidir ({1} içeren). + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + Şablon yapılandırma dosyası yapılandırılmış bağlama noktasından alınamadı. + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + 'WithInstantiationThroughTemplateCreatorApi' kullanılırken 'DotnetExecutablePath' parametresi belirtilmemelidir. + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + Şablon yüklenemedi: {0}, ayrıntılar: {1}. + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + Yüklemeden sonra paket getirilemedi. + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + WithInstantiationThroughTemplateCreatorApi kullanılırken 'TemplateSpecificArgs' parametresi belirtilmemelidir. Parametreler 'WithInstantiationThroughTemplateCreatorApi' bağımsız değişkeni aracılığıyla geçirilmelidir. + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.zh-Hans.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.zh-Hans.xlf new file mode 100644 index 000000000000..a30ee6946c51 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.zh-Hans.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + 已成功创建模板“{0}”。 + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + 模板配置: {0} 不存在。使用 "WithInstantiationThroughTemplateCreatorApi" 时,"TemplatePath" 参数必须指定 template.json 或模板根目录的路径(包含 {1})。 + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + 无法从配置的装入点检索模板配置文件。 + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + 使用 "WithInstantiationThroughTemplateCreatorApi" 时,不能指定 "DotnetExecutablePath" 参数。 + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + 未能安装模板: {0},详细信息: {1}。 + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + 安装后未提取任何包。 + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + 使用 WithInstantiationThroughTemplateCreatorApi 时,不能指定 "TemplateSpecificArgs" 参数。应通过 "WithInstantiationThroughTemplateCreatorApi" 的自变量传递参数。 + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.zh-Hant.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.zh-Hant.xlf new file mode 100644 index 000000000000..e0ba12822ecd --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateApiVerifier/xlf/LocalizableStrings.zh-Hant.xlf @@ -0,0 +1,44 @@ + + + + + + The template "{0}" was created successfully. + 範本「{0}」已成功建立。 + {0} holds template name + + + Template config: {0} doesn't exist. When using 'WithInstantiationThroughTemplateCreatorApi' the 'TemplatePath' parameter must specify path to the template.json or to the root of template (containing {1}). + 範本設定: {0} 不存在。使用 'WithInstantiationThroughTemplateCreatorApi' 時,'TemplatePath' 參數必須指定 template.json 或範本根目錄的路徑 (包含 {1})。 + Do not translate 'TemplatePath' and 'WithInstantiationThroughTemplateCreatorApi' + +{0} and {1} contain paths + + + Template configuration file could not be retrieved from configured mount point. + 無法從設定的掛接點擷取範本設定檔。 + + + + 'DotnetExecutablePath' parameter must not be specified when using 'WithInstantiationThroughTemplateCreatorApi'. + 使用 'WithInstantiationThroughTemplateCreatorApi' 時,不得指定 'DotnetExecutablePath' 參數。 + Do not translate 'DotnetExecutablePath' and 'WithInstantiationThroughTemplateCreatorApi' + + + Failed to install template: {0}, details: {1}. + 無法安裝範本: {0},詳細資料: {1}。 + {0} is tamplate name, {1} is error message + + + No packages fetched after installation. + 安裝後未擷取任何封裝。 + + + + 'TemplateSpecificArgs' parameter must not be specified when using WithInstantiationThroughTemplateCreatorApi. Parameters should be passed via the argument of 'WithInstantiationThroughTemplateCreatorApi'. + 使用 WithInstantiationThroughTemplateCreatorApi 時,不得指定 'TemplateSpecificArgs'。參數應透過 'WithInstantiationThroughTemplateCreatorApi' 引數傳遞。 + Do not translate 'TemplateSpecificArgs' and 'WithInstantiationThroughTemplateCreatorApi' + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateVerifier.XunitV3.csproj b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateVerifier.XunitV3.csproj new file mode 100644 index 000000000000..9acff6595a3f --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateVerifier.XunitV3.csproj @@ -0,0 +1,31 @@ + + + + + + $(DefineConstants);XUNIT_V3 + Microsoft.TemplateEngine.Authoring.TemplateVerifier + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/CommandResultData.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/CommandResultData.cs new file mode 100644 index 000000000000..bbb5c2f99a2e --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/CommandResultData.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.CommandUtils; + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands +{ + internal class CommandResultData : IInstantiationResult + { + public CommandResultData(int exitCode, string stdOut, string stdErr, string workingDirectory) + { + ExitCode = exitCode; + StdOut = stdOut; + StdErr = stdErr; + WorkingDirectory = workingDirectory; + } + + public CommandResultData(CommandResult commandResult) + : this(commandResult.ExitCode, commandResult.StdOut ?? string.Empty, commandResult.StdErr ?? string.Empty, commandResult.StartInfo.WorkingDirectory) + { } + + public int ExitCode { get; } + + public string StdOut { get; } + + public string StdErr { get; } + + public string WorkingDirectory { get; } + + public string InstantiatedContentDirectory => WorkingDirectory; + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/CommandRunner.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/CommandRunner.cs new file mode 100644 index 000000000000..1b820148f220 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/CommandRunner.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.CommandUtils; + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands +{ + internal class CommandRunner : ICommandRunner + { + public CommandResultData RunCommand(TestCommand testCommand) + { + return new CommandResultData(testCommand.Execute()); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/ICommandRunner.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/ICommandRunner.cs new file mode 100644 index 000000000000..1787064fded4 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/ICommandRunner.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.CommandUtils; + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands +{ + internal interface ICommandRunner + { + CommandResultData RunCommand(TestCommand testCommand); + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/IInstantiationResult.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/IInstantiationResult.cs new file mode 100644 index 000000000000..f5c710f7f525 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/IInstantiationResult.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands; + +/// +/// The result information of a template instantiation action. +/// +public interface IInstantiationResult +{ + /// + /// Exit code of the action (e.g. exit code of the dotnet new command). + /// 0 indicates successful action. Nonzero otherwise. + /// + int ExitCode { get; } + + /// + /// Standard output stream content for the instantiation action. + /// + string StdOut { get; } + + /// + /// Standard error stream content for the instantiation action. + /// + string StdErr { get; } + + /// + /// Path to directory containing the output of the instantiation. + /// + string InstantiatedContentDirectory { get; } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/RunInstantiation.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/RunInstantiation.cs new file mode 100644 index 000000000000..ac4e327a8797 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Commands/RunInstantiation.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands; + +/// +/// Runs the template instantiation (and installation if needed) based on given options. +/// +/// +/// +public delegate Task RunInstantiation(TemplateVerifierOptions options); diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/IPhysicalFileSystemEx.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/IPhysicalFileSystemEx.cs new file mode 100644 index 000000000000..b0dc7b88e3bb --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/IPhysicalFileSystemEx.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier +{ + internal interface IPhysicalFileSystemEx : IPhysicalFileSystem + { + /// + /// Same behavior as . + /// + Task ReadAllTextAsync(string path, CancellationToken cancellationToken = default); + + /// + /// Same behavior as . + /// + Task WriteAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default); + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/LocalizableStrings.resx b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/LocalizableStrings.resx new file mode 100644 index 000000000000..2f13e361756a --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/LocalizableStrings.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Template installation expected to pass but it had exit code '{0}'. + + + File extension passed to scrubber should not start with dot. + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + {0} is a folder path + + + Template name is mandatory, but was not supplied. + + + Unexpected error encountered. + + + Template instantiation expected to pass but it had exit code '{0}'. + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Microsoft.TemplateEngine.Authoring.TemplateVerifier.Shared.props b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Microsoft.TemplateEngine.Authoring.TemplateVerifier.Shared.props new file mode 100644 index 000000000000..e6a27cb882e9 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Microsoft.TemplateEngine.Authoring.TemplateVerifier.Shared.props @@ -0,0 +1,36 @@ + + + + $(NetMinimum);$(NetCurrent) + The verification engine for the templates for .NET template engine. + true + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Microsoft.TemplateEngine.Authoring.TemplateVerifier.csproj b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Microsoft.TemplateEngine.Authoring.TemplateVerifier.csproj new file mode 100644 index 000000000000..e0b1be99203c --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/Microsoft.TemplateEngine.Authoring.TemplateVerifier.csproj @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/PhysicalFileSystemEx.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/PhysicalFileSystemEx.cs new file mode 100644 index 000000000000..fb9e245e8802 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/PhysicalFileSystemEx.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier +{ + internal class PhysicalFileSystemEx : PhysicalFileSystem, IPhysicalFileSystemEx + { + public Task ReadAllTextAsync(string path, CancellationToken cancellationToken = default) + => File.ReadAllTextAsync(path, cancellationToken); + + public Task WriteAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default) + => File.WriteAllTextAsync(path, contents, cancellationToken); + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/PublicAPI.Shipped.txt b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..ab058de62d44 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/PublicAPI.Unshipped.txt b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..7295788b12f7 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/PublicAPI.Unshipped.txt @@ -0,0 +1,90 @@ +Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands.IInstantiationResult +Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands.IInstantiationResult.ExitCode.get -> int +Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands.IInstantiationResult.InstantiatedContentDirectory.get -> string! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands.IInstantiationResult.StdErr.get -> string! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands.IInstantiationResult.StdOut.get -> string! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands.RunInstantiation +Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition +Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition.AddScrubber(Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition.ScrubFileByPath! fileScrubber) -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition.AddScrubber(System.Action! scrubber, string? extension = null) -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition.ScrubbersDefinition(System.Action! scrubber, string? extension = null) -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition.ScrubFileByPath +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode.InstallFailed = 106 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode.InstantiationFailed = 100 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode.InternalError = 70 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode.InvalidOption = 127 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode.TemplateDoesNotExist = 103 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode.VerificationFailed = 65 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode.WorkingDirectoryExists = 73 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationException +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationException.TemplateVerificationErrorCode.get -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationException.TemplateVerificationErrorCode.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationException.TemplateVerificationException(string! message, Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode templateVerificationErrorCode) -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationException.TemplateVerificationException(string! message, Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationErrorCode templateVerificationErrorCode, System.Exception! inner) -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerificationException.TemplateVerificationException(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.CustomDirectoryVerifier.get -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.VerifyDirectory? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.CustomInstatiation.get -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands.RunInstantiation? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.CustomScrubbers.get -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DisableDefaultVerificationExcludePatterns.get -> bool +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DisableDefaultVerificationExcludePatterns.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DisableDiffTool.get -> bool +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DisableDiffTool.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DoNotAppendTemplateArgsToScenarioName.get -> bool +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DoNotAppendTemplateArgsToScenarioName.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DoNotPrependCallerMethodNameToScenarioName.get -> bool +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DoNotPrependCallerMethodNameToScenarioName.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DoNotPrependTemplateNameToScenarioName.get -> bool +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DoNotPrependTemplateNameToScenarioName.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DotnetExecutablePath.get -> string? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.DotnetExecutablePath.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.EnsureEmptyOutputDirectory.get -> bool +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.EnsureEmptyOutputDirectory.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.Environment.get -> System.Collections.Generic.IReadOnlyDictionary? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.IsCommandExpectedToFail.get -> bool +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.IsCommandExpectedToFail.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.OutputDirectory.get -> string? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.OutputDirectory.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.ScenarioName.get -> string? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.ScenarioName.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.SettingsDirectory.get -> string? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.SettingsDirectory.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.SnapshotsDirectory.get -> string? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.SnapshotsDirectory.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.StandardOutputFileExtension.get -> string? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.StandardOutputFileExtension.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.TemplateName.get -> string! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.TemplateName.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.TemplatePath.get -> string? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.TemplatePath.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.TemplateSpecificArgs.get -> System.Collections.Generic.IEnumerable? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.TemplateSpecificArgs.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.TemplateVerifierOptions(string! templateName) -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.UniqueFor.get -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.UniqueFor.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.VerificationExcludePatterns.get -> System.Collections.Generic.IEnumerable? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.VerificationExcludePatterns.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.VerificationIncludePatterns.get -> System.Collections.Generic.IEnumerable? +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.VerificationIncludePatterns.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.VerifyCommandOutput.get -> bool +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.VerifyCommandOutput.init -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.WithCustomDirectoryVerifier(Microsoft.TemplateEngine.Authoring.TemplateVerifier.VerifyDirectory! verifier) -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.WithCustomEnvironment(System.Collections.Generic.IReadOnlyDictionary! environment) -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.WithCustomInstatiation(Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands.RunInstantiation! instantiation) -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.WithCustomScrubbers(Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition! scrubbers) -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions.WithEnvironmentVariable(string! name, string! value) -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.TemplateVerifierOptions! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption +Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption.Architecture = 1 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption +Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption.None = 0 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption +Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption.OsPlatform = 2 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption +Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption.Runtime = 4 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption +Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption.RuntimeAndVersion = 8 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption +Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption.TargetFramework = 16 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption +Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption.TargetFrameworkAndVersion = 32 -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.UniqueForOption +Microsoft.TemplateEngine.Authoring.TemplateVerifier.VerificationEngine +Microsoft.TemplateEngine.Authoring.TemplateVerifier.VerificationEngine.Execute(Microsoft.Extensions.Options.IOptions! optionsAccessor, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken), string! sourceFile = "", string! callerMethod = "") -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.Authoring.TemplateVerifier.VerificationEngine.VerificationEngine(Microsoft.Extensions.Logging.ILogger! logger) -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.VerificationEngine.VerificationEngine(Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void +Microsoft.TemplateEngine.Authoring.TemplateVerifier.VerifyDirectory +static readonly Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition.Empty -> Microsoft.TemplateEngine.Authoring.TemplateVerifier.ScrubbersDefinition! \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/ScrubbersDefinition.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/ScrubbersDefinition.cs new file mode 100644 index 000000000000..b21ad9cdeef8 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/ScrubbersDefinition.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier; + +public class ScrubbersDefinition +{ + public static readonly ScrubbersDefinition Empty = new(); + + public ScrubbersDefinition(Action scrubber, string? extension = null) + { + AddScrubber(scrubber, extension); + } + + private ScrubbersDefinition() { } + + public delegate void ScrubFileByPath(string relativeFilePath, StringBuilder content); + + internal Dictionary> ScrubbersByExtension { get; } = new Dictionary>(); + + internal Action? GeneralScrubber { get; private set; } + + internal List ByPathScrubbers { get; } = new List(); + + public ScrubbersDefinition AddScrubber(Action scrubber, string? extension = null) + { + if (ReferenceEquals(this, Empty)) + { + return new ScrubbersDefinition().AddScrubber(scrubber, extension); + } + + if (string.IsNullOrWhiteSpace(extension)) + { + GeneralScrubber += scrubber; + } + // This is to get the same behavior as Verify.NET + else + { + extension = extension.Trim(); + if (extension.StartsWith('.')) + { + throw new TemplateVerificationException(LocalizableStrings.VerificationEngine_Error_ScrubberExtension, TemplateVerificationErrorCode.InvalidOption); + } + + if (ScrubbersByExtension.TryGetValue(extension, out var origScrubber)) + { + scrubber = origScrubber + scrubber; + } + + ScrubbersByExtension[extension] = scrubber; + } + + return this; + } + + public ScrubbersDefinition AddScrubber(ScrubFileByPath fileScrubber) + { + if (ReferenceEquals(this, Empty)) + { + return new ScrubbersDefinition().AddScrubber(fileScrubber); + } + + ByPathScrubbers.Add(fileScrubber); + return this; + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/TemplateVerificationErrorCode.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/TemplateVerificationErrorCode.cs new file mode 100644 index 000000000000..6a5a67543525 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/TemplateVerificationErrorCode.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier +{ + /// + /// VerificationEngine error codes in thrown . Correspond to VerificationCommand exit codes. + /// + /// Exit codes based on + /// * https://tldp.org/LDP/abs/html/exitcodes.html + /// * https://github.com/openbsd/src/blob/master/include/sysexits.h. + /// related reference: dotnet new exit codes: https://aka.ms/templating-exit-codes. + /// Future exit codes should be allocated in a range of 107 - 113. If not sufficient, a range of 79 - 99 may be used as well. + /// + public enum TemplateVerificationErrorCode + { + /// + /// Indicates failed verification - assertions defined for the scenarios were not met. + /// E.g. unexpected exit code, stdout/stderr output or created templates content. + /// + VerificationFailed = 65, + + /// + /// Unexpected internal error in . This might indicate a bug. + /// + InternalError = 70, + + /// + /// Configured working directory already exists and is not empty - so instantiation cannot proceed without destructive changes. + /// + WorkingDirectoryExists = 73, + + /// + /// Selected template (via name or path) was not found. + /// + TemplateDoesNotExist = 103, + + /// + /// The template instantiation failed and results were not created. + /// + InstantiationFailed = 100, + + /// + /// Installation/Uninstallation Failed - Processing issues. + /// + InstallFailed = 106, + + /// + /// Unrecognized option(s) and/or argument(s) for a command. + /// + InvalidOption = 127, + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/TemplateVerificationException.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/TemplateVerificationException.cs new file mode 100644 index 000000000000..693e66a92bc9 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/TemplateVerificationException.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Serialization; + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier +{ + [Serializable] + public class TemplateVerificationException : Exception + { + public TemplateVerificationException(string message, TemplateVerificationErrorCode templateVerificationErrorCode) : base(message) + { + TemplateVerificationErrorCode = templateVerificationErrorCode; + } + + public TemplateVerificationException(string message, TemplateVerificationErrorCode templateVerificationErrorCode, Exception inner) : base(message, inner) + { + TemplateVerificationErrorCode = templateVerificationErrorCode; + } + + [Obsolete("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.", DiagnosticId = "SYSLIB0051", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] + protected TemplateVerificationException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } + + public TemplateVerificationErrorCode TemplateVerificationErrorCode { get; init; } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/TemplateVerifierOptions.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/TemplateVerifierOptions.cs new file mode 100644 index 000000000000..51f83a74071c --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/TemplateVerifierOptions.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier +{ + /// + /// Configuration bag for the class. + /// + public class TemplateVerifierOptions : IOptions + { + /// + /// Initializes a new instance of the class. + /// + /// + public TemplateVerifierOptions(string templateName) => TemplateName = templateName; + + /// + /// Gets the name of the template to be verified. Can be already installed template or a template within local path specified with . + /// + public string TemplateName { get; init; } + + /// + /// Gets the path to template.json file or containing directory. + /// + public string? TemplatePath { get; init; } + + /// + /// Gets the path to custom dotnet executable (e.g. x-copy install scenario). + /// + public string? DotnetExecutablePath { get; init; } + + /// + /// Gets the custom environment variable collection to be passed to execution of dotnet commands. + /// + public IReadOnlyDictionary? Environment { get; private set; } + + /// + /// Gets the template specific arguments. + /// + public IEnumerable? TemplateSpecificArgs { get; init; } + + /// + /// Gets the directory with snapshot files. + /// + public string? SnapshotsDirectory { get; init; } + + /// + /// If set to true - 'dotnet new' command standard output and error contents will be verified along with the produced template files. + /// + public bool VerifyCommandOutput { get; init; } + + /// + /// If set to true - 'dotnet new' command is expected to return nonzero return code. + /// Otherwise a zero exit code and no error output is expected. + /// + public bool IsCommandExpectedToFail { get; init; } + + /// + /// If set to true - the diff tool won't be automatically started by the Verifier on verification failures. + /// + public bool DisableDiffTool { get; init; } + + /// + /// If set to true - all template output files will be verified, unless are specified. + /// Otherwise a default exclusions (to be documented - mostly binaries etc.). + /// + public bool DisableDefaultVerificationExcludePatterns { get; init; } + + /// + /// Set of patterns defining files to be excluded from verification. + /// + public IEnumerable? VerificationExcludePatterns { get; init; } + + /// + /// Gets a set of patterns defining files to be included into verification (unless excluded by ). + /// By default all files are included (unless excluded). + /// + public IEnumerable? VerificationIncludePatterns { get; init; } + + /// + /// Gets the target directory to output the generated template. + /// If explicitly specified, it won't be cleaned up upon successful run of test scenario. + /// + public string? OutputDirectory { get; init; } + + /// + /// Gets the settings directory for template engine (in memory location used if not specified). + /// + public string? SettingsDirectory { get; init; } + + /// + /// Gets the Verifier expectations directory naming convention - by indicating which scenarios should be differentiated. + /// + public UniqueForOption? UniqueFor { get; init; } + + /// + /// Gets the delegates that perform custom scrubbing of template output contents before verifications. + /// + public ScrubbersDefinition? CustomScrubbers { get; private set; } + + /// + /// Gets the delegate that performs custom verification of template output contents. + /// + public VerifyDirectory? CustomDirectoryVerifier { get; private set; } + + /// + /// Gets the custom scenario name; if specified it will be used as part of verification snapshot. + /// + public string? ScenarioName { get; init; } + + /// + /// , if the instantiation args should not be appended to verification snapshot name. + /// + public bool DoNotAppendTemplateArgsToScenarioName { get; init; } + + /// + /// , if the template name should not be prepended to verification snapshot name. + /// + public bool DoNotPrependTemplateNameToScenarioName { get; init; } + + /// + /// , if the caller method name should not be prepended to verification snapshot name. + /// + public bool DoNotPrependCallerMethodNameToScenarioName { get; init; } + + /// + /// , if the output directory has to be empty before the test execution. + /// + public bool EnsureEmptyOutputDirectory { get; init; } = true; + + /// + /// Gets the extension of autogeneratedfiles with stdout and stderr content. + /// + public string? StandardOutputFileExtension { get; init; } + + /// + /// Gets the delegate that performs custom generation of template output contents. + /// + public RunInstantiation? CustomInstatiation { get; private set; } + + // Enable if too many underlying features are asked. + // On the other hand if possible, we should wrap everyting and dfo not expose underlying technology to allow for change. + // public Action VerifySettingsAdjustor { get; init; } + + TemplateVerifierOptions IOptions.Value => this; + + /// + /// Adds environment variables collection to be passed to execution of dotnet commands. + /// + /// + /// + public TemplateVerifierOptions WithCustomEnvironment(IReadOnlyDictionary environment) + { + Dictionary newEnvironemnt = new(environment); + if (Environment != null) + { + newEnvironemnt.Merge(Environment); + } + Environment = newEnvironemnt; + return this; + } + + /// + /// Adds environment variable to be passed to execution of dotnet commands. + /// + /// + /// + /// + public TemplateVerifierOptions WithEnvironmentVariable(string name, string value) + { + return WithCustomEnvironment(new Dictionary() { { name, value } }); + } + + /// + /// Adds a custom scrubber definition. + /// The scrubber definition can alter the template content (globally or based on the file extension), before the verifications occur. + /// + /// + /// + public TemplateVerifierOptions WithCustomScrubbers(ScrubbersDefinition scrubbers) + { + CustomScrubbers = scrubbers; + return this; + } + + /// + /// Adds on optional custom verifier implementation. + /// If custom verifier is provided, no default verifications of content will be performed - the caller is responsible for performing the verifications. + /// + /// + /// + public TemplateVerifierOptions WithCustomDirectoryVerifier(VerifyDirectory verifier) + { + CustomDirectoryVerifier = verifier; + return this; + } + + /// + /// Adds custom template instantiator. + /// If custom template generator is provided it's responsible for proper instantiation of a template (as well as installation if needed) based on given options. + /// + /// + /// + public TemplateVerifierOptions WithCustomInstatiation(RunInstantiation instantiation) + { + CustomInstatiation = instantiation; + return this; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/UniqueForOption.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/UniqueForOption.cs new file mode 100644 index 000000000000..d6788bb4ac1b --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/UniqueForOption.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier +{ + [Flags] + public enum UniqueForOption + { + None = 0, + Architecture = 1, + OsPlatform = 2, + Runtime = 4, + RuntimeAndVersion = 8, + TargetFramework = 16, + TargetFrameworkAndVersion = 32 + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/VerificationEngine.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/VerificationEngine.cs new file mode 100644 index 000000000000..037e151ac645 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/VerificationEngine.cs @@ -0,0 +1,518 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !XUNIT_V3 +using System.Reflection; +#endif +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands; +using Microsoft.TemplateEngine.CommandUtils; +using Microsoft.TemplateEngine.Utils; +using VerifyTests.DiffPlex; + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier +{ + public class VerificationEngine + { + private static readonly IReadOnlyList DefaultVerificationExcludePatterns = new List() + { + @"**/obj/*", + @"**\obj\*", + @"**/bin/*", + @"**\bin\*", + "*.exe", + "*.dll", + "*.", + }; + + private readonly ILogger _logger; + private readonly ILoggerFactory? _loggerFactory; + private readonly ICommandRunner _commandRunner = new CommandRunner(); + private readonly IPhysicalFileSystemEx _fileSystem = new PhysicalFileSystemEx(); + + public VerificationEngine(ILogger logger) + { + _logger = logger; + } + + public VerificationEngine(ILoggerFactory loggerFactory) + : this(loggerFactory.CreateLogger(typeof(VerificationEngine))) + { + _loggerFactory = loggerFactory; + } + + internal VerificationEngine(ICommandRunner commandRunner, ILogger logger) + : this(logger) + { + _commandRunner = commandRunner; + } + + /// + /// Asynchronously performs the scenario and its verification based on given configuration options. + /// + /// Configuration of the scenario and verification. + /// + /// + /// + /// A Task to be awaited. + public async Task Execute( + IOptions optionsAccessor, + CancellationToken cancellationToken = default, + [CallerFilePath] string sourceFile = "", + [CallerMemberName] string callerMethod = "") + { + if (optionsAccessor == null) + { + throw new ArgumentNullException(nameof(optionsAccessor)); + } + + TemplateVerifierOptions options = optionsAccessor.Value; + + RunInstantiation instantiate = + options.CustomInstatiation ?? + (verifierOptions => Task.FromResult(RunDotnetNewCommand(verifierOptions, _commandRunner, _loggerFactory, _logger))); + + IInstantiationResult commandResult = await instantiate(options).ConfigureAwait(false); + + if (options.IsCommandExpectedToFail) + { + if (commandResult.ExitCode == 0) + { + throw new TemplateVerificationException( + LocalizableStrings.VerificationEngine_Error_UnexpectedPass, + TemplateVerificationErrorCode.VerificationFailed); + } + } + else + { + if (commandResult.ExitCode != 0) + { + throw new TemplateVerificationException( + string.Format(LocalizableStrings.VerificationEngine_Error_UnexpectedFail, commandResult.ExitCode), + TemplateVerificationErrorCode.InstantiationFailed); + } + + // We do not expect stderr in passing command. + // However if verification of stdout and stderr is opted-in - we will let that verification validate the stderr content + if (!options.VerifyCommandOutput && !string.IsNullOrEmpty(commandResult.StdErr)) + { + throw new TemplateVerificationException( + string.Format( + LocalizableStrings.VerificationEngine_Error_UnexpectedStdErr, + Environment.NewLine, + commandResult.StdErr), + TemplateVerificationErrorCode.InstantiationFailed); + } + } + + await VerifyResult( + options, + commandResult, + new CallerInfo() { CallerMethod = callerMethod, CallerSourceFile = sourceFile, ContentDirectory = commandResult.InstantiatedContentDirectory }) + .ConfigureAwait(false); + + // if everything is successful - let's delete the created files (unless placed into explicitly requested dir) + if (string.IsNullOrEmpty(options.OutputDirectory) && _fileSystem.DirectoryExists(commandResult.InstantiatedContentDirectory)) + { + _fileSystem.DirectoryDelete(commandResult.InstantiatedContentDirectory, true); + } + } + + internal static Task CreateVerificationTask( + CallerInfo callerInfo, + TemplateVerifierOptions options, + IPhysicalFileSystemEx fileSystem) + { + List exclusionsList = options.DisableDefaultVerificationExcludePatterns + ? new() + : new(DefaultVerificationExcludePatterns); + + if (options.VerificationExcludePatterns != null) + { + exclusionsList.AddRange(options.VerificationExcludePatterns); + } + + List excludeGlobs = exclusionsList.Select(pattern => (IPatternMatcher)Glob.Parse(pattern)).ToList(); + List includeGlobs = new(); + + if (options.VerificationIncludePatterns != null) + { + includeGlobs.AddRange(options.VerificationIncludePatterns.Select(pattern => Glob.Parse(pattern))); + } + + if (!includeGlobs.Any()) + { + includeGlobs.Add(Glob.MatchAll); + } + + if (options.CustomDirectoryVerifier != null) + { + return options.CustomDirectoryVerifier( + callerInfo.ContentDirectory, + new Lazy>( + GetVerificationContent(callerInfo.ContentDirectory, includeGlobs, excludeGlobs, options.CustomScrubbers, fileSystem))); + } + + VerifySettings verifySettings = new(); + + // Scrubbers by file: https://github.com/VerifyTests/Verify/issues/673 + if (options.CustomScrubbers != null) + { + if (options.CustomScrubbers.GeneralScrubber != null) + { + verifySettings.AddScrubber(options.CustomScrubbers.GeneralScrubber); + } + + foreach (var pair in options.CustomScrubbers.ScrubbersByExtension) + { + verifySettings.AddScrubber(pair.Key, pair.Value); + } + } + + // UseFileName replaces the entire type+method+parameters naming. + // This prevents Verify.XunitV3 from auto-appending [Theory] parameters + // to snapshot file paths, which breaks matching since TemplateVerifier + // already manages naming uniqueness via ScenarioName. + string scenarioPrefix = options.DoNotPrependTemplateNameToScenarioName ? string.Empty : options.TemplateName; + if (!options.DoNotPrependCallerMethodNameToScenarioName && !string.IsNullOrEmpty(callerInfo.CallerMethod)) + { + scenarioPrefix = callerInfo.CallerMethod + (string.IsNullOrEmpty(scenarioPrefix) ? null : ".") + scenarioPrefix; + } + scenarioPrefix = string.IsNullOrEmpty(scenarioPrefix) ? "_" : scenarioPrefix; + string scenarioName = GetScenarioName(options); + string fileName = string.IsNullOrEmpty(scenarioName) ? scenarioPrefix : $"{scenarioPrefix}.{scenarioName}"; + verifySettings.UseFileName(fileName); + + string snapshotsDir = options.SnapshotsDirectory ?? "Snapshots"; + if (!string.IsNullOrEmpty(callerInfo.CallerDirectory) && !Path.IsPathRooted(snapshotsDir)) + { + snapshotsDir = Path.Combine(callerInfo.CallerDirectory, snapshotsDir); + } + verifySettings.UseDirectory(snapshotsDir); + verifySettings.UseDiffPlex(OutputType.Compact); + verifySettings.UseSplitModeForUniqueDirectory(); + + if ((options.UniqueFor ?? UniqueForOption.None) != UniqueForOption.None) + { + foreach (UniqueForOption value in Enum.GetValues(typeof(UniqueForOption))) + { + if ((options.UniqueFor & value) == value) + { + switch (value) + { + case UniqueForOption.None: + break; + case UniqueForOption.Architecture: + verifySettings.UniqueForArchitecture(); + break; + case UniqueForOption.OsPlatform: + verifySettings.UniqueForOSPlatform(); + break; + case UniqueForOption.Runtime: + verifySettings.UniqueForRuntime(); + break; + case UniqueForOption.RuntimeAndVersion: + verifySettings.UniqueForRuntimeAndVersion(); + break; + case UniqueForOption.TargetFramework: + verifySettings.UniqueForTargetFramework(); + break; + case UniqueForOption.TargetFrameworkAndVersion: + verifySettings.UniqueForTargetFrameworkAndVersion(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } + + if (options.DisableDiffTool) + { + verifySettings.DisableDiff(); + } + + return Verifier.VerifyDirectory( + callerInfo.ContentDirectory, + include: (filePath) => + { + string relativePath = fileSystem.PathRelativeTo(filePath, callerInfo.ContentDirectory); + return includeGlobs.Any(g => g.IsMatch(relativePath)) && !excludeGlobs.Any(g => g.IsMatch(relativePath)); + }, + fileScrubber: ExtractFileScrubber(options, callerInfo.ContentDirectory, fileSystem), + settings: verifySettings, + // Need to overwrite arg with CallerFileAttribute as this assembly is compiled on possibly different OS, than + // the actual caller of the API. + // The info is not used in any output paths of Verify (as we inject custom naming), but it is transformed via + // Path utilities and checked for non-null - which can break in case of usage on different OS than was the built time one + sourceFile: callerInfo.CallerSourceFile); + } + + private static FileScrubber? ExtractFileScrubber(TemplateVerifierOptions options, string contentDir, IPhysicalFileSystemEx fileSystem) + { + if (!(options.CustomScrubbers?.ByPathScrubbers.Any() ?? false)) + { + return null; + } + + return (fullPath, builder) => + { + string relativePath = fileSystem.PathRelativeTo(fullPath, contentDir); + options.CustomScrubbers.ByPathScrubbers.ForEach(scrubberByPath => scrubberByPath(relativePath, builder)); + }; + } + + private static string GetScenarioName(TemplateVerifierOptions options) + { + // TBD: once the custom SDK switching feature is implemented - here we should append the sdk distinguisher if UniqueForOption.Runtime requested + + var scenarioName = options.ScenarioName + (options.DoNotAppendTemplateArgsToScenarioName ? null : EncodeArgsAsPath(options.TemplateSpecificArgs)); + if (string.IsNullOrEmpty(scenarioName)) + { + scenarioName = "_"; + } + return scenarioName; + } + + private static string EncodeArgsAsPath(IEnumerable? args) + { + if (args == null || !args.Any()) + { + return string.Empty; + } + + Regex r = new Regex(string.Format("[{0}]", Regex.Escape(new string(Path.GetInvalidFileNameChars())))); + return r.Replace(string.Join('#', args), string.Empty); + } + + private static IInstantiationResult RunDotnetNewCommand(TemplateVerifierOptions options, ICommandRunner commandRunner, ILoggerFactory? loggerFactory, ILogger logger) + { + // Create temp folder and instantiate there + string workingDir = options.OutputDirectory ?? Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + if (options.EnsureEmptyOutputDirectory && Directory.Exists(workingDir) && Directory.EnumerateFileSystemEntries(workingDir).Any()) + { + throw new TemplateVerificationException(LocalizableStrings.VerificationEngine_Error_WorkDirExists, TemplateVerificationErrorCode.WorkingDirectoryExists); + } + + Directory.CreateDirectory(workingDir); + ILogger commandLogger = loggerFactory?.CreateLogger(typeof(DotnetCommand)) ?? logger; + string? customHiveLocation = options.SettingsDirectory; + + if (!string.IsNullOrEmpty(options.TemplatePath)) + { + customHiveLocation ??= Path.Combine(Path.GetTempPath(), Path.GetRandomFileName(), "home"); + var installCommand = + new DotnetNewCommand(commandLogger, "install", options.TemplatePath) + .WithCustomHive(customHiveLocation) + .WithCustomExecutablePath(options.DotnetExecutablePath) + .WithEnvironmentVariables(options.Environment) + .WithWorkingDirectory(workingDir); + + CommandResultData installCommandResult = commandRunner.RunCommand(installCommand); + + if (installCommandResult.ExitCode != 0) + { + throw new TemplateVerificationException( + string.Format(LocalizableStrings.VerificationEngine_Error_InstallUnexpectedFail, installCommandResult.ExitCode), + TemplateVerificationErrorCode.InstantiationFailed); + } + } + + List cmdArgs = new() + { + options.TemplateName + }; + if (options.TemplateSpecificArgs != null) + { + cmdArgs.AddRange(options.TemplateSpecificArgs); + } + + // let's make sure the template outputs are named and placed deterministically + if (!cmdArgs.Select(arg => arg.Trim()) + .Any(arg => new[] { "-n", "--name" }.Contains(arg, StringComparer.OrdinalIgnoreCase))) + { + cmdArgs.Add("-n"); + cmdArgs.Add(options.TemplateName); + } + if (!cmdArgs.Select(arg => arg.Trim()) + .Any(arg => new[] { "-o", "--output" }.Contains(arg, StringComparer.OrdinalIgnoreCase))) + { + cmdArgs.Add("-o"); + cmdArgs.Add(options.TemplateName); + } + + var command = new DotnetNewCommand(loggerFactory?.CreateLogger(typeof(DotnetCommand)) ?? logger, cmdArgs.ToArray()); + + if (!string.IsNullOrEmpty(customHiveLocation)) + { + command.WithCustomHive(customHiveLocation); + } + else + { + command.WithoutCustomHive(); + } + + command + .WithCustomExecutablePath(options.DotnetExecutablePath) + .WithEnvironmentVariables(options.Environment) + .WithWorkingDirectory(workingDir) + .WithNoUpdateCheck(); + + var result = commandRunner.RunCommand(command); + // Cleanup, unless the settings dir was externally passed + if (!string.IsNullOrEmpty(customHiveLocation) && string.IsNullOrEmpty(options.SettingsDirectory)) + { + Directory.Delete(customHiveLocation, true); + } + return result; + } + + private static void DummyMethod() + { } + + private static async IAsyncEnumerable<(string FilePath, string ScrubbedContent)> GetVerificationContent( + string contentDir, + List includeMatchers, + List excludeMatchers, + ScrubbersDefinition? scrubbers, + IPhysicalFileSystemEx fileSystem) + { + foreach (string filePath in fileSystem.EnumerateFiles(contentDir, "*", SearchOption.AllDirectories)) + { + string relativePath = fileSystem.PathRelativeTo(filePath, contentDir); + + if (!includeMatchers.Any(g => g.IsMatch(relativePath))) + { + continue; + } + + if (excludeMatchers.Any(g => g.IsMatch(relativePath))) + { + continue; + } + + string content = await fileSystem.ReadAllTextAsync(filePath).ConfigureAwait(false); + + if (scrubbers != null) + { + string extension = Path.GetExtension(filePath); + // This is to get the same behavior as Verify.NET + if (extension.Length > 0) + { + extension = extension[1..]; + } + StringBuilder? sb = null; + + if (scrubbers.ByPathScrubbers.Any()) + { + sb = new StringBuilder(content); + scrubbers.ByPathScrubbers.ForEach(scrubberByPath => scrubberByPath(relativePath, sb)); + } + + if (!string.IsNullOrEmpty(extension) && scrubbers.ScrubbersByExtension.TryGetValue(extension, out Action? scrubber)) + { + sb ??= new StringBuilder(content); + scrubber(sb); + } + + if (scrubbers.GeneralScrubber != null) + { + sb ??= new StringBuilder(content); + scrubbers.GeneralScrubber(sb); + } + + if (sb != null) + { + content = sb.ToString(); + } + } + + yield return new(relativePath, content); + } + } + + private async Task VerifyResult(TemplateVerifierOptions args, IInstantiationResult commandResultData, CallerInfo callerInfo) + { +#if !XUNIT_V3 + UseVerifyAttribute a = new UseVerifyAttribute(); + + // https://github.com/VerifyTests/Verify/blob/d8cbe38f527d6788ecadd6205c82803bec3cdfa6/src/Verify.Xunit/Verifier.cs#L10 + // need to simulate execution from tests + var v = DummyMethod; + MethodInfo mi = v.Method; + a.Before(mi); +#endif + + if (args.VerifyCommandOutput) + { + if (_fileSystem.DirectoryExists(Path.Combine(commandResultData.InstantiatedContentDirectory, SpecialFiles.StandardStreamsDir))) + { + throw new TemplateVerificationException( + string.Format( + LocalizableStrings.VerificationEngine_Error_StdOutFolderExists, + SpecialFiles.StandardStreamsDir), + TemplateVerificationErrorCode.InternalError); + } + + _fileSystem.CreateDirectory(Path.Combine(commandResultData.InstantiatedContentDirectory, SpecialFiles.StandardStreamsDir)); + + await _fileSystem.WriteAllTextAsync( + Path.Combine(commandResultData.InstantiatedContentDirectory, SpecialFiles.StandardStreamsDir, SpecialFiles.StdOut + (args.StandardOutputFileExtension ?? SpecialFiles.DefaultExtension)), + commandResultData.StdOut) + .ConfigureAwait(false); + + await _fileSystem.WriteAllTextAsync( + Path.Combine(commandResultData.InstantiatedContentDirectory, SpecialFiles.StandardStreamsDir, SpecialFiles.StdErr + (args.StandardOutputFileExtension ?? SpecialFiles.DefaultExtension)), + commandResultData.StdErr) + .ConfigureAwait(false); + } + + Task verifyTask = CreateVerificationTask(callerInfo, args, _fileSystem); + + try + { + await verifyTask.ConfigureAwait(false); + } + catch (Exception e) + { + if (e is TemplateVerificationException) + { + throw; + } + if (e.GetType().Name == "VerifyException") + { + throw new TemplateVerificationException(e.Message, TemplateVerificationErrorCode.VerificationFailed); + } + else + { + _logger.LogError(e, LocalizableStrings.VerificationEngine_Error_Unexpected); + throw; + } + } + } + + internal readonly struct CallerInfo + { + public string CallerSourceFile { get; init; } + + public string? CallerMethod { get; init; } + + public string ContentDirectory { get; init; } + + public string CallerDirectory => string.IsNullOrEmpty(CallerSourceFile) ? string.Empty : Path.GetDirectoryName(CallerSourceFile)!; + } + + private static class SpecialFiles + { + public const string StandardStreamsDir = "std-streams"; + public const string StdOut = "stdout"; + public const string StdErr = "stderr"; + public const string DefaultExtension = ".txt"; + public static readonly string[] FileNames = { StdOut, StdErr }; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/VerifyDirectory.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/VerifyDirectory.cs new file mode 100644 index 000000000000..9612b2054c00 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/VerifyDirectory.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier; + +/// +/// Delegate signature for performing custom directory content verifications. +/// Expectable verification failures should be signaled with . +/// API provider can either perform content enumeration, skipping and scrubbing by themselves (then the second argument can be ignored) +/// or the contentFetcher can be awaited to get the content of files - filtered by exclusion patterns and scrubbed by scrubbers. +/// +/// +/// +/// +public delegate Task VerifyDirectory(string contentDirectory, Lazy> contentFetcher); diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.cs.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.cs.xlf new file mode 100644 index 000000000000..923339dcc54a --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.cs.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + Očekávalo se, že instalace šablony bude úspěšné, ale měla ukončovací kód{0}. + + + + File extension passed to scrubber should not start with dot. + Přípona souboru předaná scrubberu by neměla začínat tečkou. + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + Složka [{0}] by neměla ve výstupu šablony existovat – v takovém případě nelze ověřit stdout/stderr. + {0} is a folder path + + + Template name is mandatory, but was not supplied. + Název šablony je povinný, ale nebyl uveden. + + + + Unexpected error encountered. + Došlo k neočekávané chybě. + + + + Template instantiation expected to pass but it had exit code '{0}'. + Očekávalo se, že vytvoření instance šablony bude úspěšné, ale mělo ukončovací kód{0}. + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + Očekávalo se, že vytvoření instance šablony selže, ale bylo úspěšné.. + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + Očekávalo se, že instance šablony nebude mít žádný výstup stderr, ale byl zjištěn výstup stderr:{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + Pracovní adresář již existuje a není prázdný. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.de.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.de.xlf new file mode 100644 index 000000000000..2868fdb78700 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.de.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + Es wurde erwartet, dass die Vorlageninstallation erfolgreich ist, aber sie wies den Exitcode „{0}“ auf. + + + + File extension passed to scrubber should not start with dot. + Die an die Bereinigung übergebene Dateierweiterung darf nicht mit einem Punkt beginnen. + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + Der Ordner [{0}] sollte in der Vorlagenausgabe nicht vorhanden sein – stdout/stderr kann in diesem Fall nicht überprüft werden. + {0} is a folder path + + + Template name is mandatory, but was not supplied. + Der Vorlagenname ist obligatorisch, wurde jedoch nicht angegeben. + + + + Unexpected error encountered. + Ein unerwarteter Fehler ist aufgetreten. + + + + Template instantiation expected to pass but it had exit code '{0}'. + Es wurde erwartet, dass die Vorlageninstantiierung erfolgreich ist, aber sie wies den Exitcode „{0}“ auf. + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + Es wurde ein Fehler bei der Vorlageninstantiierung erwartet, aber sie war erfolgreich. + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + Es wurde erwartet, dass die Vorlageninstanziierung keine stderr-Ausgabe hat, aber es wurde eine stderr-Ausgabe gefunden:{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + Das Arbeitsverzeichnis ist bereits vorhanden und nicht leer. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.es.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.es.xlf new file mode 100644 index 000000000000..5ee98fcf4b31 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.es.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + Se esperaba que se pasara la instalación de la plantilla, pero tenía el código de salida "{0}". + + + + File extension passed to scrubber should not start with dot. + La extensión de archivo pasada a la limpieza no debe comenzar con un punto. + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + La carpeta [{0}] no debe existir en la salida de la plantilla; no se puede comprobar stdout/stderr en ese caso. + {0} is a folder path + + + Template name is mandatory, but was not supplied. + El nombre de plantilla es obligatorio, pero no se ha proporcionado. + + + + Unexpected error encountered. + Error inesperado. + + + + Template instantiation expected to pass but it had exit code '{0}'. + Se esperaba que se pasara la creación de instancias de plantilla, pero tenía el código de salida "{0}". + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + Se esperaba un error en la creación de instancias de plantilla, pero se ha pasado. + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + Se esperaba que la creación de instancias de plantilla no tuviera ninguna salida stderr, pero se ha encontrado la salida stderr:{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + El directorio de trabajo ya existe y no está vacío. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.fr.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.fr.xlf new file mode 100644 index 000000000000..5d9ec70a39e5 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.fr.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + L’installation du modèle devait réussir, mais le code de sortie était '{0}'. + + + + File extension passed to scrubber should not start with dot. + L’extension de fichier passée au programme de nettoyage ne doit pas commencer par un point. + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + Le dossier [{0}] ne doit pas exister dans la sortie du modèle. Impossible de vérifier stdout/stderr dans ce cas. + {0} is a folder path + + + Template name is mandatory, but was not supplied. + Le nom du modèle est obligatoire, mais il n’a pas été fourni. + + + + Unexpected error encountered. + Une erreur inattendue s'est produite. + + + + Template instantiation expected to pass but it had exit code '{0}'. + L’instanciation du modèle devait réussir, mais le code de sortie était '{0}'. + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + L’instanciation du modèle était censée échouer, mais elle a réussi. + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + L’instanciation du modèle était censée ne pas avoir de sortie stderr, mais la sortie stderr a été rencontrée :{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + Le répertoire de travail existe déjà et n’est pas vide. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.it.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.it.xlf new file mode 100644 index 000000000000..ef618e88f8ce --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.it.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + È previsto il passaggio dell'installazione del modello, ma il codice di uscita '{0}'. + + + + File extension passed to scrubber should not start with dot. + L'estensione di file passata alla pulitura non deve iniziare con il punto. + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + La cartella [{0}] non deve esistere nell'output del modello. In tal caso, non è possibile verificare stdout/stderr. + {0} is a folder path + + + Template name is mandatory, but was not supplied. + Il nome del modello è obbligatorio, ma non è stato specificato. + + + + Unexpected error encountered. + Si è verificato un errore imprevisto. + + + + Template instantiation expected to pass but it had exit code '{0}'. + È previsto il passaggio della creazione di un'istanza del modello, ma il codice di uscita '{0}'. + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + La creazione di un'istanza del modello ha esito negativo ma è stata superata. + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + Per la creazione di un'istanza del modello non è previsto alcun output stderr, ma è stato rilevato l'output stderr:{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + La directory di lavoro esiste già e non è vuota. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.ja.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.ja.xlf new file mode 100644 index 000000000000..afbba1c87bf3 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.ja.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + テンプレートのインストールは成功する必要がありますが、終了コードが '{0}' ありました。 + + + + File extension passed to scrubber should not start with dot. + スクラブに渡されるファイル拡張子はドットで始まってはいけません。 + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + フォルダー [{0}] はテンプレート出力に存在してはなりません。このような場合、stdout/stderr を検証できません。 + {0} is a folder path + + + Template name is mandatory, but was not supplied. + テンプレート名は必須ですが、指定されていません。 + + + + Unexpected error encountered. + 予期しないエラーが発生しました。 + + + + Template instantiation expected to pass but it had exit code '{0}'. + テンプレートのインスタンス化は成功する必要がありますが、終了コードが '{0}' ありました。 + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + テンプレートのインスタンス化は失敗する必要がありますが、成功しました。 + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + テンプレートのインスタンス化には stderr 出力が予期されていないが、stderr 出力が見つかりました:{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + 作業ディレクトリは既に存在し、空ではありません。 + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.ko.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.ko.xlf new file mode 100644 index 000000000000..9886f916883d --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.ko.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + 템플릿 설치는 통과해야 하지만 종료 코드 '{0}'이(가) 있습니다. + + + + File extension passed to scrubber should not start with dot. + 스크러버에 전달된 파일 확장자는 점으로 시작하지 않아야 합니다. + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + 폴더 [{0}]은(는) 템플릿 출력에 없어야 합니다. 이 경우 stdout/stderr을 확인할 수 없습니다. + {0} is a folder path + + + Template name is mandatory, but was not supplied. + 템플릿 이름은 필수 항목이지만 제공되지 않았습니다. + + + + Unexpected error encountered. + 예기치 않은 오류가 발생했습니다. + + + + Template instantiation expected to pass but it had exit code '{0}'. + 템플릿 인스턴스화를 통과해야 하지만 종료 코드 '{0}'이(가) 있습니다. + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + 템플릿 인스턴스화가 실패할 것으로 예상되었지만 통과했습니다. + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + 템플릿 인스턴스화에는 stderr 출력이 없을 것으로 예상되지만 stderr 출력이 발생했습니다.{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + 작업 디렉터리가 이미 존재하며 비어 있지 않습니다. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.pl.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.pl.xlf new file mode 100644 index 000000000000..0a543d1e2a61 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.pl.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + Instalacja szablonu powinna zakończyć się powodzeniem, ale zawiera kod zakończenia „{0}”. + + + + File extension passed to scrubber should not start with dot. + Rozszerzenie pliku przekazane do szybkiej kontroli nie powinno rozpoczynać się od kropki. + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + Folder [{0}] nie powinien istnieć w danych wyjściowych szablonu — w takim przypadku nie można zweryfikować elementu stdout/stderr. + {0} is a folder path + + + Template name is mandatory, but was not supplied. + Nazwa szablonu jest obowiązkowa, ale nie została podana. + + + + Unexpected error encountered. + Wykryto nieoczekiwany błąd. + + + + Template instantiation expected to pass but it had exit code '{0}'. + Oczekiwano przekazania wystąpienia szablonu, ale jego kod zakończenia to: „{0}”. + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + Utworzenie wystąpienia szablonu powinno zakończyć się niepowodzeniem, ale powiodło się. + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + Utworzenie wystąpienia szablonu nie powinno mieć żadnych danych wyjściowych stderr, ale napotkano dane wyjściowe stderr:{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + Katalog roboczy już istnieje i nie jest pusty. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.pt-BR.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.pt-BR.xlf new file mode 100644 index 000000000000..d7b606f6a7a2 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.pt-BR.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + Esperava-se que a instalação do modelo fosse aprovada, mas tinha o código de saída '{0}'. + + + + File extension passed to scrubber should not start with dot. + A extensão de arquivo passada para o programa de limpeza não deve começar com ponto. + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + A pasta [{0}] não deve existir na saída do modelo - não é possível verificar stdout/stderr nesse caso. + {0} is a folder path + + + Template name is mandatory, but was not supplied. + O nome do modelo é obrigatório, mas não foi fornecido. + + + + Unexpected error encountered. + Erro inesperado encontrado. + + + + Template instantiation expected to pass but it had exit code '{0}'. + Esperava-se que a instanciação do modelo fosse aprovada, mas tinha o código de saída '{0}'. + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + Esperava-se que a instanciação do modelo falhasse, mas foi aprovada. + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + Espera-se que a instanciação do modelo não tenha nenhuma saída stderr, mas a saída stderr foi encontrada:{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + O diretório de trabalho já existe e não está vazio. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.ru.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.ru.xlf new file mode 100644 index 000000000000..940e4ad567ed --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.ru.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + Ожидалась, что установка шаблона будет выполнена, однако операция закончилась кодом завершения "{0}". + + + + File extension passed to scrubber should not start with dot. + Расширение файла, переданное в средство очистки, не должно начинаться с точки. + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + Папка [{0}] не должна существовать в выводе шаблона — в этом случае будет невозможно проверить stdout/stderr. + {0} is a folder path + + + Template name is mandatory, but was not supplied. + Имя шаблона является обязательным, но оно не указано. + + + + Unexpected error encountered. + Произошла непредвиденная ошибка. + + + + Template instantiation expected to pass but it had exit code '{0}'. + Ожидалось, что создание экземпляра шаблона будет выполнено, однако операция закончилась кодом завершения "{0}". + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + При создании экземпляра шаблона ожидался сбой, но операция была выполнена. + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + Ожидалось, что при создании экземпляра шаблона не будет выходных данных stderr, но выходные данные stderr были обнаружены:{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + Рабочий каталог уже существует, и он не пуст. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.tr.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.tr.xlf new file mode 100644 index 000000000000..5416689613e0 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.tr.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + Şablon yüklemenin başarılı olması bekleniyordu ancak '{0}' çıkış kodu oluştu. + + + + File extension passed to scrubber should not start with dot. + Temizleyiciye geçirilen dosya uzantısı noktayla başlamamalıdır. + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + [{0}] klasörü şablon çıkışında olmamalıdır; böyle bir durumda stdout/stderr doğrulanamıyor. + {0} is a folder path + + + Template name is mandatory, but was not supplied. + Zorunlu şablon adı sağlanmadı. + + + + Unexpected error encountered. + Beklenmeyen bir hatayla karşılaşıldı. + + + + Template instantiation expected to pass but it had exit code '{0}'. + Şablon örneği oluşturmanın başarılı olması bekleniyordu ancak '{0}' çıkış kodu oluştu. + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + Şablon örneği oluşturmanın başarısız olması bekleniyordu ancak başarılı oldu. + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + Şablon örneği oluşturmanın stderr çıkışına sahip olması beklenmiyordu, ancak stderr çıkışıyla karşılaşıldı:{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + Çalışma dizini zaten var ve boş değil. + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.zh-Hans.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.zh-Hans.xlf new file mode 100644 index 000000000000..fdfc074e1539 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.zh-Hans.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + 模板安装应通过,但它具有退出代码 "{0}"。 + + + + File extension passed to scrubber should not start with dot. + 传递给清理程序的文件扩展名不应以点开头。 + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + 模板输出中不应存在文件夹 [{0}] - 在这种情况下无法验证 stdout/stderr。 + {0} is a folder path + + + Template name is mandatory, but was not supplied. + 模板名称是必需的,但未提供。 + + + + Unexpected error encountered. + 遇到了意外错误。 + + + + Template instantiation expected to pass but it had exit code '{0}'. + 模板实例化应通过,但它具有退出代码 "{0}"。 + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + 模板实例化预期会失败,但已通过。 + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + 模板实例化预期没有任何 stderr 输出,但遇到了 stderr 输出: {0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + 工作目录已存在且不为空。 + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.zh-Hant.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.zh-Hant.xlf new file mode 100644 index 000000000000..3489611fbfd9 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.Authoring.TemplateVerifier/xlf/LocalizableStrings.zh-Hant.xlf @@ -0,0 +1,52 @@ + + + + + + Template installation expected to pass but it had exit code '{0}'. + 範本安裝預期會通過,但有結束代碼 '{0}'。 + + + + File extension passed to scrubber should not start with dot. + 傳遞至清除程式的副檔名不可以點開頭。 + + + + The folder [{0}] should not exist in the template output - cannot verify stdout/stderr in such case. + 資料夾 [{0}] 不應存在於範本輸出中 - 在此類案例中無法驗證 stdout/stderr。 + {0} is a folder path + + + Template name is mandatory, but was not supplied. + 範本名稱是必要項目,但未提供。 + + + + Unexpected error encountered. + 發生未預期的錯誤。 + + + + Template instantiation expected to pass but it had exit code '{0}'. + 範本具現化預期會通過,但有結束代碼 '{0}'。 + {0} is an exit code number + + + Template instantiation expected to fail but it passed. + 範本具現化預期會失敗,但已通過。 + + + + Template instantiation expected not to have any stderr output, but stderr output was encountered:{0}{1} + 範本具現化預期沒有任何 stderr 輸出,但發生 stderr 輸出:{0}{1} + {0} is newline, {1} is the standard error content + + + The working directory already exists and is not empty. + 工作目錄已經存在,且非空白。 + + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Exceptions/JsonMemberMissingException.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Exceptions/JsonMemberMissingException.cs new file mode 100644 index 000000000000..5f6dfc52f0cf --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Exceptions/JsonMemberMissingException.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core.Exceptions +{ + /// + /// Represents an exception thrown to indicate that a json element with the given name + /// was not found, even though it was expected to exist under the given parent json element. + /// + internal sealed class JsonMemberMissingException : Exception + { + /// + /// Creates an instance of . + /// + /// Name of the Json element that was expected to contain + /// a member with the given name. + /// Name of the missing member. + public JsonMemberMissingException(string owningElementName, string memberName) + : base(string.Format(LocalizableStrings.stringExtractor_log_jsonMemberIsMissing, owningElementName, memberName)) + { + OwningElementName = owningElementName; + MemberName = memberName; + } + + /// + /// Gets the name of the json element that was expected to contain the missing member. + /// + public string OwningElementName { get; } + + /// + /// Gets the name of the member that was missing. + /// + public string MemberName { get; } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Exceptions/LocalizationKeyIsNotUniqueException.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Exceptions/LocalizationKeyIsNotUniqueException.cs new file mode 100644 index 000000000000..1c2e1ea1934f --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Exceptions/LocalizationKeyIsNotUniqueException.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core.Exceptions +{ + /// + /// Exception that is thrown when the template.json file contains elements + /// whose identifiers are not unique. + /// + internal class LocalizationKeyIsNotUniqueException : TemplateLocalizerException + { + /// + /// Creates an instance of . + /// + /// The localization key that should have been unique, but was used more than once. + /// Identifier of the JSON element containing the reused key. + public LocalizationKeyIsNotUniqueException(string reusedKey, string owningElementIdentifier) + : base(string.Format(LocalizableStrings.stringExtractor_log_jsonKeyIsNotUnique, owningElementIdentifier, reusedKey)) + { + ReusedKey = reusedKey; + OwningElementIdentifier = owningElementIdentifier; + } + + /// + /// The localization key that should have been unique, but was used more than once. + /// + /// "myManualInstruction". + /// "firstSymbol". + public string ReusedKey { get; } + + /// + /// Identifier of the JSON element containing the reused key. + /// + /// "postActions/pa0/manualInstructions". + /// "symbols". + public string OwningElementIdentifier { get; } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Exceptions/TemplateLocalizerException.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Exceptions/TemplateLocalizerException.cs new file mode 100644 index 000000000000..310f8dda3007 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Exceptions/TemplateLocalizerException.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core.Exceptions +{ + /// + /// Base exception type for all the exceptions specific to Template Localizer. + /// + internal class TemplateLocalizerException : Exception + { + public TemplateLocalizerException() : base() { } + + public TemplateLocalizerException(string message) : base(message) { } + + public TemplateLocalizerException(string message, Exception innerException) : base(message, innerException) { } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExportOptions.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExportOptions.cs new file mode 100644 index 000000000000..303ad67d461d --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExportOptions.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core +{ + public readonly struct ExportOptions : IEquatable + { + /// + /// Creates an instance of . + /// + /// Specifies whether export operation skip flushing files to disk. + /// Path to the directory where the generated files will be saved into. + /// A set of languages for which the localizable files will be exported. + public ExportOptions(bool dryRun, string? targetDirectory = default, IEnumerable? languages = default) + { + DryRun = dryRun; + TargetDirectory = targetDirectory; + Languages = languages; + } + + /// + /// Gets the default list of languages for which the localizable files will be exported. + /// + public static IReadOnlyList DefaultLanguages { get; } = new[] + { + "cs", + "de", + "en", + "es", + "fr", + "it", + "ja", + "ko", + "pl", + "pt-BR", + "ru", + "tr", + "zh-Hans", + "zh-Hant", + }; + + /// + /// Gets the languages for which localizable files will be exported. + /// + public IEnumerable? Languages { get; } + + /// + /// Gets the path to the directory to export into. If null, files will be exported into + /// a "localize" folder next to the template.json file. + /// + public string? TargetDirectory { get; } + + /// + /// Gets a value indicating whether the export process should skip + /// flushing the file changes to file system. + /// + public bool DryRun { get; } + + public static bool operator ==(ExportOptions x, ExportOptions y) => x.Equals(y); + + public static bool operator !=(ExportOptions x, ExportOptions y) => !(x == y); + + public bool Equals(ExportOptions other) + { + return DryRun == other.DryRun + && Languages == other.Languages + && TargetDirectory == other.TargetDirectory; + } + + public override bool Equals(object obj) + { + if (obj is not ExportOptions other) + { + return false; + } + + return Equals(other); + } + + public override int GetHashCode() + { + return unchecked((((((17 * 23) + DryRun.GetHashCode()) * 23) + + (TargetDirectory?.GetHashCode() ?? 0)) * 23) + + (Languages?.GetHashCode() ?? 0)); + } + + public override string ToString() + { + return $"({DryRun}, {TargetDirectory}, {string.Join(", ", Languages ?? Enumerable.Empty())})"; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExportResult.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExportResult.cs new file mode 100644 index 000000000000..c2b161d714fc --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExportResult.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core +{ + /// + /// Represents the result of an export operation where template.json files are used to generate templatestrings.json files. + /// + public sealed class ExportResult + { + /// + /// Creates an instance of . + /// + public ExportResult(string? templateJsonPath) : this(templateJsonPath, null, null) { } + + /// + /// Creates an instance of . + /// + public ExportResult(string? templateJsonPath, string? errorMessage) : this(templateJsonPath, errorMessage, null) { } + + /// + /// Creates an instance of . + /// + public ExportResult(string? templateJsonPath, Exception? innerException) : this(templateJsonPath, null, innerException) { } + + /// + /// Creates an instance of . + /// + public ExportResult(string? templateJsonPath, string? errorMessage, Exception? innerException) + { + TemplateJsonPath = templateJsonPath; + ErrorMessage = errorMessage; + InnerException = innerException; + } + + /// + /// Gets the path to the template.json file that was used as input. + /// + public string? TemplateJsonPath { get; } + + /// + /// Gets the success state of the operation. + /// + public bool Succeeded => ErrorMessage == null && InnerException == null; + + /// + /// Gets the message explaining the underlying error, if the operation has failed. + /// + public string? ErrorMessage { get; } + + /// + /// Gets the related exception in the case that the operation fails. + /// Value of this property may be null if the underlying error is detected + /// at the application logic and no actual exceptions occured. + /// + public Exception? InnerException { get; } + + public override string ToString() + { + return $"{nameof(ExportResult)} {{{TemplateJsonPath}, {ErrorMessage}, {InnerException?.Message}}}"; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExtendedJavascriptEncoder.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExtendedJavascriptEncoder.cs new file mode 100644 index 000000000000..5cefa5b36486 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/ExtendedJavascriptEncoder.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Text; +using System.Text.Encodings.Web; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core +{ + internal class ExtendedJavascriptEncoder : JavaScriptEncoder + { + public override int MaxOutputCharactersPerInputCharacter => UnsafeRelaxedJsonEscaping.MaxOutputCharactersPerInputCharacter; + + public override unsafe int FindFirstCharacterToEncode(char* text, int textLength) + { + ReadOnlySpan input = new(text, textLength); + int idx = 0; + + while (Rune.DecodeFromUtf16(input.Slice(idx), out Rune result, out int charsConsumed) == OperationStatus.Done) + { + if (WillEncode(result.Value)) + { + // This character needs to be escaped. Break out. + break; + } + idx += charsConsumed; + } + + if (idx == input.Length) + { + // None of the characters in the string needs to be escaped. + return -1; + } + return idx; + } + + public override bool WillEncode(int unicodeScalar) + { + if (unicodeScalar == 0x00A0) + { + // Don't escape no-break space. + return false; + } + else + { + return UnsafeRelaxedJsonEscaping.WillEncode(unicodeScalar); + } + } + + public override unsafe bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten) + { + return UnsafeRelaxedJsonEscaping.TryEncodeUnicodeScalar(unicodeScalar, buffer, bufferLength, out numberOfCharactersWritten); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/ChildValueKeyCreator.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/ChildValueKeyCreator.cs new file mode 100644 index 000000000000..dfce6f9e9a41 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/ChildValueKeyCreator.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Microsoft.TemplateEngine.TemplateLocalizer.Core.Exceptions; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core.KeyCreators +{ + /// + /// Creates a key for a given element using the value of one of its members. + /// The type of the value of the member should be string. + /// + internal sealed class ChildValueKeyCreator : IJsonKeyCreator + { + /// + /// Creates an instance of . + /// + /// Name of the member containing the value of the key. + /// Default value to be used if the element does not have + /// a property with name specified in and it is the only + /// child of the parent element. If the value is null and the specified member doesn't exist, + /// will be thrown. + public ChildValueKeyCreator(string memberPropertyName, string? onlyChildDefaultValue = default) + { + MemberPropertyName = memberPropertyName; + OnlyChildDefaultValue = onlyChildDefaultValue; + } + + /// + /// Gets the name of the property to be searched in the member list + /// of the that the key will be created for. + /// + public string MemberPropertyName { get; } + + /// + /// Gets the default value to be used in the case that the element doesn't have a member with + /// the specified name and it is the only child of its parent. + /// + public string? OnlyChildDefaultValue { get; } + + /// + public string CreateKey(JsonElement element, string? elementName, string? parentElementName, int indexInParent, int parentChildCount) + { + string key; + if (element.TryGetProperty(MemberPropertyName, out JsonElement keyProperty) && keyProperty.ValueKind == JsonValueKind.String) + { + key = keyProperty.GetString() ?? string.Empty; + } + else if (OnlyChildDefaultValue != null && parentChildCount == 1) + { + key = OnlyChildDefaultValue; + } + else + { + string owningElementName = (parentElementName == null ? elementName : (parentElementName + TemplateStringExtractor.KeySeparator + elementName)) ?? string.Empty; + throw new JsonMemberMissingException(owningElementName, MemberPropertyName); + } + + return parentElementName == null ? key : string.Concat(parentElementName, TemplateStringExtractor.KeySeparator, key); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/IJsonKeyCreator.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/IJsonKeyCreator.cs new file mode 100644 index 000000000000..f07d74bdd153 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/IJsonKeyCreator.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core.KeyCreators +{ + /// + /// Creates keys for a given of a JSON document that can be used to uniquely identify the element + /// among its siblings under the same parent . + /// + internal interface IJsonKeyCreator + { + /// + /// Creates a key for the given . + /// + /// The element that the key will uniquely represent under the same parent. + /// Name of the element that the key is being created for. + /// Name of the parent element. + /// Index of this element with respect to its siblings under the same parent. + /// The number of children that the parent element has. + /// The key string. + string CreateKey(JsonElement element, string? elementName, string? parentElementName, int indexInParent, int parentChildCount); + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/IndexBasedKeyCreator.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/IndexBasedKeyCreator.cs new file mode 100644 index 000000000000..15be6354bda6 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/IndexBasedKeyCreator.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core.KeyCreators +{ + /// + /// Creates a key for a given element using the elements index with respect to its siblings. + /// + internal sealed class IndexBasedKeyCreator : IJsonKeyCreator + { + /// + public string CreateKey(JsonElement element, string? elementName, string? parentElementName, int indexInParent, int parentChildCount) + { + return string.Concat(parentElementName, "[", indexInParent.ToString(), "]"); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/NameKeyCreator.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/NameKeyCreator.cs new file mode 100644 index 000000000000..51baca6fa501 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/KeyCreators/NameKeyCreator.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core.KeyCreators +{ + /// + /// Creates a key for a given element using the name of the element. + /// + internal sealed class NameKeyCreator : IJsonKeyCreator + { + /// + public string CreateKey(JsonElement element, string? elementName, string? parentElementName, int indexInParent, int parentChildCount) + { + return elementName ?? string.Empty; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/LocalizableStrings.resx b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/LocalizableStrings.resx new file mode 100644 index 000000000000..8f5c48a16c7d --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/LocalizableStrings.resx @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Adding into localizable strings: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + {0} is a file path. + + + Failed to read the existing strings from '{0}' + {0} is a file path. + + + Loading existing localizations from file '{0}' + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + {0} is a file path. + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Microsoft.TemplateEngine.TemplateLocalizer.Core.csproj b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Microsoft.TemplateEngine.TemplateLocalizer.Core.csproj new file mode 100644 index 000000000000..5031a737679d --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Microsoft.TemplateEngine.TemplateLocalizer.Core.csproj @@ -0,0 +1,32 @@ + + + + $(NetMinimum);$(NetCurrent);netstandard2.0;$(NetFrameworkMinimum) + The core API for Template Localizer tool. + true + true + true + true + + + + + + annotations + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/PublicAPI.Shipped.txt b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..a4e0a6e6c1c9 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/PublicAPI.Shipped.txt @@ -0,0 +1,28 @@ +#nullable enable +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.DryRun.get -> bool +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.Equals(Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions other) -> bool +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.ExportOptions() -> void +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.ExportOptions(bool dryRun, string? targetDirectory = null, System.Collections.Generic.IEnumerable? languages = null) -> void +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.Languages.get -> System.Collections.Generic.IEnumerable? +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.TargetDirectory.get -> string? +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportResult +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportResult.ErrorMessage.get -> string? +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportResult.ExportResult(string? templateJsonPath) -> void +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportResult.ExportResult(string? templateJsonPath, string? errorMessage) -> void +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportResult.ExportResult(string? templateJsonPath, string? errorMessage, System.Exception? innerException) -> void +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportResult.ExportResult(string? templateJsonPath, System.Exception? innerException) -> void +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportResult.InnerException.get -> System.Exception? +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportResult.Succeeded.get -> bool +Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportResult.TemplateJsonPath.get -> string? +Microsoft.TemplateEngine.TemplateLocalizer.Core.TemplateLocalizer +Microsoft.TemplateEngine.TemplateLocalizer.Core.TemplateLocalizer.ExportLocalizationFilesAsync(string! templateJsonPath, Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.TemplateLocalizer.Core.TemplateLocalizer.TemplateLocalizer() -> void +Microsoft.TemplateEngine.TemplateLocalizer.Core.TemplateLocalizer.TemplateLocalizer(Microsoft.Extensions.Logging.ILoggerFactory? loggerFactory) -> void +override Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.Equals(object! obj) -> bool +override Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.GetHashCode() -> int +override Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.ToString() -> string! +override Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportResult.ToString() -> string! +static Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.DefaultLanguages.get -> System.Collections.Generic.IReadOnlyList! +static Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.operator !=(Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions x, Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions y) -> bool +static Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions.operator ==(Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions x, Microsoft.TemplateEngine.TemplateLocalizer.Core.ExportOptions y) -> bool diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/PublicAPI.Unshipped.txt b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateLocalizer.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateLocalizer.cs new file mode 100644 index 000000000000..e5e900676fb0 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateLocalizer.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.TemplateEngine.TemplateLocalizer.Core.Exceptions; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core +{ + /// + /// Creates and updates localization files for template.json files. + /// Public members of this type is thread-safe. + /// + public sealed class TemplateLocalizer + { + private readonly ILoggerFactory _loggerFactory; + + private readonly ILogger _logger; + + public TemplateLocalizer() : this(null) { } + + public TemplateLocalizer(ILoggerFactory? loggerFactory) + { + _loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + _logger = _loggerFactory.CreateLogger(); + } + + public async Task ExportLocalizationFilesAsync(string templateJsonPath, ExportOptions options, CancellationToken cancellationToken = default) + { + JsonDocumentOptions jsonOptions = new() + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + try + { + using FileStream fileStream = new(templateJsonPath, FileMode.Open, FileAccess.Read); + using JsonDocument jsonDocument = await JsonDocument.ParseAsync(fileStream, jsonOptions, cancellationToken).ConfigureAwait(false); + + TemplateStringExtractor stringExtractor = new(jsonDocument, _loggerFactory); + IReadOnlyList templateJsonStrings = stringExtractor.ExtractStrings(out string templateJsonLanguage); + + string targetDirectory = options.TargetDirectory ?? Path.Combine(Path.GetDirectoryName(templateJsonPath) ?? string.Empty, "localize"); + IEnumerable languages = ExportOptions.DefaultLanguages; + if (options.Languages?.Any() ?? false) + { + languages = options.Languages; + } + + await TemplateStringUpdater.UpdateStringsAsync( + templateJsonStrings, + templateJsonLanguage, + languages, + targetDirectory, + options.DryRun, + _logger, + cancellationToken).ConfigureAwait(false); + + return new ExportResult(templateJsonPath); + } + catch (Exception exception) + when (exception is JsonMemberMissingException or LocalizationKeyIsNotUniqueException) + { + // Output a more friendly text without stack trace for known errors. + return new ExportResult(templateJsonPath, exception.Message); + } + catch (Exception exception) + { + return new ExportResult(templateJsonPath, null, exception); + } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateString.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateString.cs new file mode 100644 index 000000000000..268246d21afe --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateString.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core +{ + /// + /// Represents a string in template.json file that needs to be localized. + /// + internal readonly struct TemplateString : IEquatable + { + /// + /// Creates an instance of . + /// + /// A string that uniquely identifies this template field in the template.json file. + /// A string that uniquely identifies this field in localized templatestrings.json file. + /// Localizable string. + public TemplateString(string identifier, string localizationKey, string value) + { + Identifier = identifier; + LocalizationKey = localizationKey; + Value = value; + } + + /// + /// Gets the string that identifies this localizable string in template.json file. + /// Given a template.json file and an , it is possible to + /// directly find the corresponding JSON element. + /// + /// "symbols.Framework.choices.[0].displayName". + public string Identifier { get; } + + /// + /// Gets the key of this localizable string, which identifies this in templatestrings.json + /// file. This is different than in the way that it is intended to + /// be more user friendly. + /// + /// "symbols.Framework.choices.net6_0.displayName". + public string LocalizationKey { get; } + + /// + /// Gets the value of the string found at the location identified with . + /// + public string Value { get; } + + /// + public bool Equals(TemplateString other) + { + return Identifier == other.Identifier + && LocalizationKey == other.LocalizationKey + && Value == other.Value; + } + + /// + public override bool Equals(object obj) + { + if (obj is not TemplateString other) + { + return false; + } + + return Equals(other); + } + + /// + public override int GetHashCode() + { + return unchecked((((((17 * 23) + Identifier.GetHashCode()) * 23) + + (LocalizationKey?.GetHashCode() ?? 0)) * 23) + + (Value?.GetHashCode() ?? 0)); + } + + /// + public override string ToString() + { + return string.Concat('{', LocalizationKey, ',', Value, '}'); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateStringExtractor.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateStringExtractor.cs new file mode 100644 index 000000000000..063c32ce2367 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateStringExtractor.cs @@ -0,0 +1,245 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable SA1114 // Comments on parameters be allowed https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2917 + +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.TemplateEngine.TemplateLocalizer.Core.Exceptions; +using Microsoft.TemplateEngine.TemplateLocalizer.Core.KeyCreators; +using Microsoft.TemplateEngine.TemplateLocalizer.Core.TraversalRules; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core +{ + /// + /// Extracts localizable strings from template.json files. + /// + internal sealed class TemplateStringExtractor + { + internal const char KeySeparator = '/'; + private const string DefaultTemplateJsonLanguage = "en"; + private static readonly IJsonKeyCreator DefaultArrayKeyExtractor = new IndexBasedKeyCreator(); + private static readonly IJsonKeyCreator DefaultObjectKeyExtractor = new NameKeyCreator(); + + private readonly ILogger _logger; + private readonly JsonDocument _jsonDocument; + + /// + /// The rules that define which fields in the template json should be extracted to a templatestrings.json file. + /// + private readonly TraversalRule _documentRootTraversalRule = + // Root element should be included in any case. + new AllInclusiveTraversalRule().WithChildren( + // Include "author" under the root. + new StringFilteredTraversalRule("author"), + // Include "name" under the root. + new StringFilteredTraversalRule("name"), + // Include "description" under the root. + new StringFilteredTraversalRule("description"), + // Include "symbols" under the root, if they also comply with child rules. + new StringFilteredTraversalRule("symbols").WithChild( + // Any symbol is included, skip none. + new AllInclusiveTraversalRule().WithChildren( + // Include "displayName" of each symbol. + new StringFilteredTraversalRule("displayName"), + // Include "description" of each symbol. + new StringFilteredTraversalRule("description"), + // Include "choices" of symbols, if they also comply with child rules. + new StringFilteredTraversalRule("choices", new ChildValueKeyCreator("choice")).WithChild( + // Include any element of the "choices" array. No choice will be skipped. + new AllInclusiveTraversalRule().WithChildren( + // Include "displayName" of each choice. + new StringFilteredTraversalRule("displayName"), + // Include "description" of each choice. + new StringFilteredTraversalRule("description"))))), + // Include "postActions" under the root, if they also comply with child rules. + new StringFilteredTraversalRule("postActions", new ChildValueKeyCreator("id")).WithChild( + // Any post action in "postActions" array should be included. Skip none. + new AllInclusiveTraversalRule().WithChildren( + // Include "description" of the post action. + new StringFilteredTraversalRule("description"), + // Include "manualInstructions" of the post action, if they also comply with child rules. + new RegexFilteredTraversalRule("manualInstructions", new ChildValueKeyCreator("id", onlyChildDefaultValue: "default")).WithChild( + // Include all the manual instructions in the array. Skip none. + new AllInclusiveTraversalRule().WithChild( + // Include "text" of the post action. + new StringFilteredTraversalRule("text")))))); + + public TemplateStringExtractor(JsonDocument document, ILoggerFactory? loggerFactory = null) + { + _jsonDocument = document; + ILoggerFactory loggerFactory1 = loggerFactory ?? NullLoggerFactory.Instance; + _logger = loggerFactory1.CreateLogger(); + } + + /// + /// Extracts localizable strings from the json document as well as the language of the strings. + /// + /// The language of the extracted strings. + /// The list of localizable strings. + /// thrown if a required the json document does not contain + /// one of the necessary fields to extract localizable strings. + /// thrown if any two json elements under the same + /// parent has the same identifier. + public IReadOnlyList ExtractStrings(out string language) + { + List extractedStrings = new(); + + TraversalArgs traversalArgs = new( + identifierPrefix: string.Empty, + keyPrefix: string.Empty, + rules: new List() { _documentRootTraversalRule }, + extractedStrings, + extractedStringIds: new HashSet()); + + TraverseJsonElements( + _jsonDocument.RootElement, + elementName: string.Empty, + localizationKey: string.Empty, + traversalArgs); + + language = GetTemplateLanguage(_jsonDocument); + + return extractedStrings; + } + + private static string GetTemplateLanguage(JsonDocument jsonDocument) + { + if (jsonDocument.RootElement.TryGetProperty("authoringLanguage", out JsonElement langElement) && + langElement.ValueKind == JsonValueKind.String) + { + string? language = langElement.GetString(); + return string.IsNullOrWhiteSpace(language) ? DefaultTemplateJsonLanguage : language!; + } + + return DefaultTemplateJsonLanguage; + } + + private void TraverseJsonElements( + JsonElement element, + string elementName, + string localizationKey, + TraversalArgs args) + { + using IDisposable? loggerScope = _logger.BeginScope(elementName); + List complyingRules = args.Rules.Where(r => r.AllowsTraversalOfIdentifier(elementName)).ToList(); + + if (complyingRules.Count == 0) + { + // This identifier was filtered out. + _logger.LogDebug(LocalizableStrings.stringExtractor_log_jsonElementExcluded, args.IdentifierPrefix + KeySeparator + elementName); + return; + } + + JsonValueKind valueKind = element.ValueKind; + if (valueKind == JsonValueKind.String) + { + ProcessStringElement(element, elementName, localizationKey, args); + return; + } + + string newIdentifierPrefix = args.IdentifierPrefix + KeySeparator + elementName; + if (valueKind == JsonValueKind.Array) + { + TraversalArgs newData = args; + newData.IdentifierPrefix = newIdentifierPrefix; + newData.Rules = complyingRules; + ProcessArrayElement(element, elementName, newData); + return; + } + + if (valueKind == JsonValueKind.Object) + { + TraversalArgs newData = args; + newData.IdentifierPrefix = newIdentifierPrefix; + newData.Rules = complyingRules; + newData.KeyPrefix = args.KeyPrefix + KeySeparator + localizationKey; + ProcessObjectElement(element, elementName, newData); + } + } + + private void ProcessStringElement(JsonElement element, string elementName, string key, TraversalArgs data) + { + string identifier = (data.IdentifierPrefix + KeySeparator + elementName).ToLowerInvariant(); + + if (data.ExtractedStringIds.Contains(identifier)) + { + // This string was already included by an earlier rule, possibly with a different key. Skip. + _logger.LogDebug(LocalizableStrings.stringExtractor_log_skippingAlreadyAddedElement, identifier); + return; + } + + string finalKey = data.KeyPrefix == null ? key : (data.KeyPrefix + KeySeparator + key); + + if (finalKey.StartsWith(KeySeparator + string.Empty + KeySeparator)) + { + // Omit the dots generated by the root element and the initial empty prefix. + finalKey = finalKey.Substring(2); + } + + _ = data.ExtractedStringIds.Add(identifier); + data.ExtractedStrings.Add(new TemplateString(identifier, finalKey, element.GetString() ?? string.Empty)); + _logger.LogTrace(LocalizableStrings.stringExtractor_log_jsonElementAdded, identifier); + } + + private void ProcessArrayElement(JsonElement element, string elementName, TraversalArgs args) + { + HashSet childKeys = new(); + int childrenCount = element.GetArrayLength(); + foreach (TraversalRule rule in args.Rules) + { + int childIndex = 0; + childKeys.Clear(); + foreach (JsonElement child in element.EnumerateArray()) + { + string childElementName = childIndex.ToString(); + string? childKey = (rule.KeyCreator ?? DefaultArrayKeyExtractor).CreateKey(child, childElementName, elementName, childIndex, childrenCount); + + if (!childKeys.Add(childKey)) + { + // Child key was already used before. Keys should be unique. + throw new LocalizationKeyIsNotUniqueException(childKey, args.IdentifierPrefix); + } + + TraversalArgs nextArgs = args; + nextArgs.Rules = rule.ChildRules; + + using IDisposable? loggerScope = _logger.BeginScope(childElementName); + TraverseJsonElements(child, childElementName, childKey, nextArgs); + childIndex++; + } + } + } + + private void ProcessObjectElement(JsonElement element, string elementName, TraversalArgs args) + { + HashSet childKeys = new(); + int childrenCount = element.EnumerateObject().Count(); + + foreach (TraversalRule rule in args.Rules) + { + int childIndex = 0; + childKeys.Clear(); + foreach (JsonProperty child in element.EnumerateObject()) + { + string childElementName = child.Name; + string childKey = (rule.KeyCreator ?? DefaultObjectKeyExtractor).CreateKey(child.Value, childElementName, elementName, childIndex, childrenCount); + + if (!childKeys.Add(childKey)) + { + // Child key was already used before. Keys should be unique. + throw new LocalizationKeyIsNotUniqueException(childKey, args.IdentifierPrefix); + } + + TraversalArgs nextArgs = args; + nextArgs.Rules = rule.ChildRules; + + using IDisposable? loggerScope = _logger.BeginScope(childElementName); + TraverseJsonElements(child.Value, childElementName, childKey, nextArgs); + childIndex++; + } + } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateStringUpdater.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateStringUpdater.cs new file mode 100644 index 000000000000..2edcb8d3f588 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TemplateStringUpdater.cs @@ -0,0 +1,213 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core +{ + internal static class TemplateStringUpdater + { + /// + /// The UTF8 BOM sequence 0xEF,0xBB,0xBF cached in a static field. + /// + private static readonly byte[] Utf8Bom = new UTF8Encoding(true).GetPreamble(); + + /// + /// Updates the templatestrings.json files for given languages with the provided strings. + /// + /// Template strings to be included in the templatestrings.json files. + /// These strings are typicall extracted from template.json file using . + /// The language of the as declared in the template.json file. + /// The list of languages for which templatestrings.json file will be created. + /// The directory that will contain the generated templatestrings.json files. + /// If true, the changes will not be written to file system. + /// to be used for logging. + /// Cancellation token to cancel the operation. + /// The task that tracks the status of the async operation. + public static async Task UpdateStringsAsync( + IEnumerable strings, + string templateJsonLanguage, + IEnumerable languages, + string targetDirectory, + bool dryRun, + ILogger logger, + CancellationToken cancellationToken) + { + if (!dryRun) + { + _ = Directory.CreateDirectory(targetDirectory); + } + + foreach (string language in languages) + { + string locFilePath = Path.Combine(targetDirectory, "templatestrings." + language + ".json"); + + Dictionary? existingStrings = await GetExistingStringsAsync(locFilePath, logger, cancellationToken) + .ConfigureAwait(false); + + if (!dryRun) + { + // Ignore existing translations for the original language, only keep the comments. + bool forceUpdate = language == templateJsonLanguage; + await SaveTemplateStringsFileAsync(strings, existingStrings, locFilePath, forceUpdate, logger, cancellationToken) + .ConfigureAwait(false); + } + } + } + + private static async Task> GetExistingStringsAsync(string locFilePath, ILogger logger, CancellationToken cancellationToken) + { + try + { + logger.LogDebug(LocalizableStrings.stringUpdater_log_loadingLocFile, locFilePath); + using FileStream openStream = File.OpenRead(locFilePath); + + JsonSerializerOptions serializerOptions = new() + { + AllowTrailingCommas = true, + MaxDepth = 1, + }; + + return await JsonSerializer.DeserializeAsync>(openStream, serializerOptions, cancellationToken) + .ConfigureAwait(false) + ?? new Dictionary(); + } + catch (IOException ex) when (ex is DirectoryNotFoundException or FileNotFoundException) + { + // templatestrings.json file doesn't exist. It will be created from scratch. + return new(); + } + catch (Exception) + { + logger.LogError(LocalizableStrings.stringUpdater_log_failedToReadLocFile, locFilePath); + throw; + } + } + + private static async Task SaveTemplateStringsFileAsync( + IEnumerable templateStrings, + Dictionary? existingStrings, + string filePath, + bool forceUpdate, + ILogger logger, + CancellationToken cancellationToken) + { + JsonWriterOptions writerOptions = new() + { + // Allow unescaped characters in the strings. This allows writing "aren't" instead of "aren\u0027t". + // This is only considered unsafe in a context where symbols may be interpreted as special characters. + // For instance, '<' character should be escaped in html documents where this json will be embedded. + Encoder = new ExtendedJavascriptEncoder(), + Indented = true, + }; + + // Determine what strings to write. If they are identical to the existing ones, no need to make disk IO. + List<(string Key, string Value)> valuesToWrite = new(); + + foreach (TemplateString templateString in templateStrings) + { + if (!forceUpdate && (existingStrings?.TryGetValue(templateString.LocalizationKey, out string? localizedText) ?? false)) + { + logger.LogDebug(LocalizableStrings.stringUpdater_log_localizedStringAlreadyExists, templateString.LocalizationKey); + } + else + { + // Existing file did not contain a localized version of the string. + // Or we want 'forceUpdate': ignore the existing localizations. + // Use the original value from template.json. + localizedText = templateString.Value; + } + + valuesToWrite.Add((templateString.LocalizationKey, localizedText!)); + + // A translation and the related comment should be next to each other. Write the comment now before any other text. + string commentKey = "_" + templateString.LocalizationKey + ".comment"; + if (existingStrings != null && existingStrings.TryGetValue(commentKey, out string? comment)) + { + valuesToWrite.Add((commentKey, comment)); + } + } + + if (SequenceEqual(valuesToWrite, existingStrings)) + { + // Data appears to be same as before. Don't rewrite it. + // Rewriting the same data causes differences in encoding etc, which marks files as 'changed' in git. + logger.LogDebug(LocalizableStrings.stringUpdater_log_dataIsUnchanged, filePath); + return; + } + + logger.LogDebug(LocalizableStrings.stringUpdater_log_openingTemplatesJson, filePath); + using FileStream fileStream = new(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); + await TruncateFileWhilePreservingBom(fileStream, cancellationToken).ConfigureAwait(false); + + using Utf8JsonWriter jsonWriter = new(fileStream, writerOptions); + + jsonWriter.WriteStartObject(); + + foreach ((string key, string value) in valuesToWrite) + { + jsonWriter.WritePropertyName(key); + jsonWriter.WriteStringValue(value); + } + + jsonWriter.WriteEndObject(); + await jsonWriter.FlushAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Truncates the file represented by the given to contain only the UTF8 BOM preamble + /// or to contain nothing (i.e. become empty) if the stream does not start with the UTF8 BOM sequence. + /// + /// The representing the file to truncate. + /// The async cancellation token. + /// A task that represents the asynchronous operation. + private static async Task TruncateFileWhilePreservingBom(FileStream fileStream, CancellationToken cancellationToken) + { + byte[] preamble = new byte[Utf8Bom.Length]; + int offset = 0; + int read; + // Read bytes from the stream until we fill the preamble array or hit EOF. + do + { +#if NET + read = await fileStream.ReadAsync(preamble.AsMemory(offset, preamble.Length - offset), cancellationToken).ConfigureAwait(false); +#else + read = await fileStream.ReadAsync(preamble, offset, preamble.Length - offset, cancellationToken).ConfigureAwait(false); +#endif + offset += read; + // Optimization to not call .ReadAsync twice + if (offset == preamble.Length) + { + break; + } + } + while (read > 0); + + fileStream.SetLength(offset == Utf8Bom.Length && preamble.SequenceEqual(Utf8Bom) ? offset : 0); + } + + private static bool SequenceEqual(List<(string, string)> lhs, Dictionary? rhs) + { + if (lhs.Count != (rhs?.Count ?? 0)) + { + return false; + } + + if (rhs != null) + { + foreach ((string key, string value) in lhs) + { + if (!rhs.TryGetValue(key, out string existingValue) + || value != existingValue) + { + return false; + } + } + } + + return true; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalArgs.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalArgs.cs new file mode 100644 index 000000000000..d8394686c42a --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalArgs.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.TemplateLocalizer.Core.TraversalRules; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core +{ + /// + /// Helper type to simplify the signature of method + /// . + /// + internal struct TraversalArgs + { + public TraversalArgs( + string identifierPrefix, + string keyPrefix, + IEnumerable rules, + List extractedStrings, + HashSet extractedStringIds) + { + IdentifierPrefix = identifierPrefix; + KeyPrefix = keyPrefix; + Rules = rules; + ExtractedStrings = extractedStrings; + ExtractedStringIds = extractedStringIds; + } + + /// + /// Gets or sets the prefix to be put in front of the json element identifier. + /// + public string IdentifierPrefix { get; set; } + + /// + /// Gets or sets the prefix to be put in front of the localizable string key. + /// + public string KeyPrefix { get; set; } + + /// + /// Gets or sets the rules to be used while traversing the json data. + /// + public IEnumerable Rules { get; set; } + + /// + /// Gets the list of strings that were extracted up to this point in the execution. + /// + public List ExtractedStrings { get; } + + /// + /// Gets a set of identifiers that were extracted up to this point in the execution. + /// This property is used to ensure the uniqueness of identifiers. + /// + public HashSet ExtractedStringIds { get; } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/AllInclusiveTraversalRule.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/AllInclusiveTraversalRule.cs new file mode 100644 index 000000000000..12c669b7c36e --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/AllInclusiveTraversalRule.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.TemplateLocalizer.Core.KeyCreators; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core.TraversalRules +{ + /// + /// Allows all identifiers to be traversed. + /// + internal sealed class AllInclusiveTraversalRule : TraversalRule + { + public AllInclusiveTraversalRule(IJsonKeyCreator? keyCreator = default) + : base(keyCreator) { } + + /// + public override bool AllowsTraversalOfIdentifier(string identifier) + { + return true; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/RegexFilteredTraversalRule.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/RegexFilteredTraversalRule.cs new file mode 100644 index 000000000000..51cf7e46b7ca --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/RegexFilteredTraversalRule.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; +using Microsoft.TemplateEngine.TemplateLocalizer.Core.KeyCreators; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core.TraversalRules +{ + /// + /// Filters identifiers based on whether they match with a given regex. + /// + internal sealed class RegexFilteredTraversalRule : TraversalRule + { + private readonly Regex _regex; + + /// + /// Creates an instance of . + /// + /// Regex pattern string that will be used to match json element names. + /// to be used when creating a key for the match elements. + public RegexFilteredTraversalRule(string regexPattern, IJsonKeyCreator? keyCreator = default) + : this(new Regex(regexPattern), keyCreator) + { } + + /// + /// Creates an instance of . + /// + /// Regex pattern that will be used to match json element names. + /// to be used when creating a key for the match elements. + public RegexFilteredTraversalRule(Regex regex, IJsonKeyCreator? keyCreator = default) + : base(keyCreator) + { + _regex = regex; + } + + /// + public override bool AllowsTraversalOfIdentifier(string identifier) + { + Match match = _regex.Match(identifier); + return match.Success && match.Length == identifier.Length; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/StringFilteredTraversalRule.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/StringFilteredTraversalRule.cs new file mode 100644 index 000000000000..bb38ed5b04f5 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/StringFilteredTraversalRule.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.TemplateLocalizer.Core.KeyCreators; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core.TraversalRules +{ + /// + /// Filters identifiers based on whether they exactly match a given string. + /// + internal sealed class StringFilteredTraversalRule : TraversalRule + { + private readonly string _identifierToMatch; + + /// + /// Creates an instance of . + /// + /// The exact json element name that this rule will accept and not filter out. + /// to be used when creating a key for the match elements. + public StringFilteredTraversalRule(string identifierToMatch, IJsonKeyCreator? keyCreator = default) + : base(keyCreator) + { + _identifierToMatch = identifierToMatch; + } + + /// + public override bool AllowsTraversalOfIdentifier(string identifier) + { + return identifier == _identifierToMatch; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/TraversalRule.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/TraversalRule.cs new file mode 100644 index 000000000000..536b3f7e7ca2 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/TraversalRules/TraversalRule.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.TemplateLocalizer.Core.KeyCreators; + +namespace Microsoft.TemplateEngine.TemplateLocalizer.Core.TraversalRules +{ + /// + /// Represents a rule that defines whether a given identifier should be further traversed. + /// Also includes the preferred to be used for creating keys for elements. + /// + internal abstract class TraversalRule + { + private readonly List _childRules = new(); + + protected TraversalRule(IJsonKeyCreator? keyCreator = default) + { + KeyCreator = keyCreator; + } + + /// + /// Key extractor to be used when calculating the key for the elements complying with this rule. + /// + public IJsonKeyCreator? KeyCreator { get; } + + /// + /// Gets the rules that the children of this json element should comply with. + /// + public IReadOnlyList ChildRules => _childRules; + + /// + /// Returns if the given identifier complies with this rule. + /// + /// Identifier of the element. + /// True if element complies with the rule. False if the element is filtered out. + public abstract bool AllowsTraversalOfIdentifier(string identifier); + + /// + /// Adds the given rule to the child ruls list. + /// + /// Returns . + public TraversalRule WithChild(TraversalRule childRule) + { + _childRules.Add(childRule); + return this; + } + + /// + /// Adds the given set of rules to the child rules list. + /// + /// Returns . + public TraversalRule WithChildren(params TraversalRule[] childRules) + { + _childRules.AddRange(childRules); + return this; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/Rune.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/Rune.cs new file mode 100644 index 000000000000..5a22f9a350a7 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/Rune.cs @@ -0,0 +1,537 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !NET + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Text +{ + // netstandard2.0 compatible implementation of System.Text.Rune. + // Copied from: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Encodings.Web/src/Polyfills/System.Text.Rune.netstandard20.cs + internal readonly struct Rune + { + private const int MaxUtf16CharsPerRune = 2; // supplementary plane code points are encoded as 2 UTF-16 code units + + private const char HighSurrogateStart = '\ud800'; + private const char LowSurrogateStart = '\udc00'; + private const int HighSurrogateRange = 0x3FF; + + private readonly uint _value; + + /// + /// Creates a from the provided Unicode scalar value. + /// + /// + /// If does not represent a value Unicode scalar value. + /// + public Rune(uint value) + { + if (!UnicodeUtility.IsValidUnicodeScalar(value)) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + _value = value; + } + + /// + /// Creates a from the provided Unicode scalar value. + /// + /// + /// If does not represent a value Unicode scalar value. + /// + public Rune(int value) + : this((uint)value) + { + } + + // non-validating ctor +#pragma warning disable IDE0060 // Remove unused parameter + private Rune(uint scalarValue, bool unused) +#pragma warning restore IDE0060 // Remove unused parameter + { + UnicodeDebug.AssertIsValidScalar(scalarValue); + _value = scalarValue; + } + + /// + /// A instance that represents the Unicode replacement character U+FFFD. + /// + public static Rune ReplacementChar => UnsafeCreate(UnicodeUtility.ReplacementChar); + + /// + /// Returns true if and only if this scalar value is ASCII ([ U+0000..U+007F ]) + /// and therefore representable by a single UTF-8 code unit. + /// + public bool IsAscii => UnicodeUtility.IsAsciiCodePoint(_value); + + /// + /// Returns true if and only if this scalar value is within the BMP ([ U+0000..U+FFFF ]) + /// and therefore representable by a single UTF-16 code unit. + /// + public bool IsBmp => UnicodeUtility.IsBmpCodePoint(_value); + + /// + /// Returns the length in code units () of the + /// UTF-16 sequence required to represent this scalar value. + /// + /// + /// The return value will be 1 or 2. + /// + public int Utf16SequenceLength + { + get + { + int codeUnitCount = UnicodeUtility.GetUtf16SequenceLength(_value); + Debug.Assert(codeUnitCount is > 0 and <= MaxUtf16CharsPerRune); + return codeUnitCount; + } + } + + /// + /// Returns the Unicode scalar value as an integer. + /// + public int Value => (int)_value; + + public static bool operator ==(Rune left, Rune right) => left._value == right._value; + + public static bool operator !=(Rune left, Rune right) => left._value != right._value; + + public static bool IsControl(Rune value) + { + // Per the Unicode stability policy, the set of control characters + // is forever fixed at [ U+0000..U+001F ], [ U+007F..U+009F ]. No + // characters will ever be added to or removed from the "control characters" + // group. See https://www.unicode.org/policies/stability_policy.html. + + // Logic below depends on Rune.Value never being -1 (since Rune is a validating type) + // 00..1F (+1) => 01..20 (&~80) => 01..20 + // 7F..9F (+1) => 80..A0 (&~80) => 00..20 + + return ((value._value + 1) & ~0x80u) <= 0x20u; + } + + /// + /// Decodes the at the beginning of the provided UTF-16 source buffer. + /// + /// + /// + /// If the source buffer begins with a valid UTF-16 encoded scalar value, returns , + /// and outs via the decoded and via the + /// number of s used in the input buffer to encode the . + /// + /// + /// If the source buffer is empty or contains only a standalone UTF-16 high surrogate character, returns , + /// and outs via and via the length of the input buffer. + /// + /// + /// If the source buffer begins with an ill-formed UTF-16 encoded scalar value, returns , + /// and outs via and via the number of + /// s used in the input buffer to encode the ill-formed sequence. + /// + /// + /// + /// The general calling convention is to call this method in a loop, slicing the buffer by + /// elements on each iteration of the loop. On each iteration of the loop + /// will contain the real scalar value if successfully decoded, or it will contain if + /// the data could not be successfully decoded. This pattern provides convenient automatic U+FFFD substitution of + /// invalid sequences while iterating through the loop. + /// + public static OperationStatus DecodeFromUtf16(ReadOnlySpan source, out Rune result, out int charsConsumed) + { + if (!source.IsEmpty) + { + // First, check for the common case of a BMP scalar value. + // If this is correct, return immediately. + + char firstChar = source[0]; + if (TryCreate(firstChar, out result)) + { + charsConsumed = 1; + return OperationStatus.Done; + } + + // First thing we saw was a UTF-16 surrogate code point. + // Let's optimistically assume for now it's a high surrogate and hope + // that combining it with the next char yields useful results. + + if ((uint)source.Length > 1) + { + char secondChar = source[1]; + if (TryCreate(firstChar, secondChar, out result)) + { + // Success! Formed a supplementary scalar value. + charsConsumed = 2; + return OperationStatus.Done; + } + else + { + // Either the first character was a low surrogate, or the second + // character was not a low surrogate. This is an error. + goto InvalidData; + } + } + else if (!char.IsHighSurrogate(firstChar)) + { + // Quick check to make sure we're not going to report NeedMoreData for + // a single-element buffer where the data is a standalone low surrogate + // character. Since no additional data will ever make this valid, we'll + // report an error immediately. + goto InvalidData; + } + } + + // If we got to this point, the input buffer was empty, or the buffer + // was a single element in length and that element was a high surrogate char. + + charsConsumed = source.Length; + result = ReplacementChar; + return OperationStatus.NeedMoreData; + + InvalidData: + + charsConsumed = 1; // maximal invalid subsequence for UTF-16 is always a single code unit in length + result = ReplacementChar; + return OperationStatus.InvalidData; + } + + /// + /// Attempts to create a from the provided input value. + /// + public static bool TryCreate(char ch, out Rune result) + { + uint extendedValue = ch; + if (!UnicodeUtility.IsSurrogateCodePoint(extendedValue)) + { + result = UnsafeCreate(extendedValue); + return true; + } + else + { + result = default; + return false; + } + } + + /// + /// Attempts to create a from the provided UTF-16 surrogate pair. + /// Returns if the input values don't represent a well-formed UTF-16surrogate pair. + /// + public static bool TryCreate(char highSurrogate, char lowSurrogate, out Rune result) + { + // First, extend both to 32 bits, then calculate the offset of + // each candidate surrogate char from the start of its range. + + uint highSurrogateOffset = (uint)highSurrogate - HighSurrogateStart; + uint lowSurrogateOffset = (uint)lowSurrogate - LowSurrogateStart; + + // This is a single comparison which allows us to check both for validity at once since + // both the high surrogate range and the low surrogate range are the same length. + // If the comparison fails, we call to a helper method to throw the correct exception message. + + if ((highSurrogateOffset | lowSurrogateOffset) <= HighSurrogateRange) + { + // The 0x40u << 10 below is to account for uuuuu = wwww + 1 in the surrogate encoding. + result = UnsafeCreate((highSurrogateOffset << 10) + ((uint)lowSurrogate - LowSurrogateStart) + (0x40u << 10)); + return true; + } + else + { + // Didn't have a high surrogate followed by a low surrogate. + result = default; + return false; + } + } + + /// + /// Decodes the at the beginning of the provided UTF-8 source buffer. + /// + /// + /// + /// If the source buffer begins with a valid UTF-8 encoded scalar value, returns , + /// and outs via the decoded and via the + /// number of s used in the input buffer to encode the . + /// + /// + /// If the source buffer is empty or contains only a partial UTF-8 subsequence, returns , + /// and outs via and via the length of the input buffer. + /// + /// + /// If the source buffer begins with an ill-formed UTF-8 encoded scalar value, returns , + /// and outs via and via the number of + /// s used in the input buffer to encode the ill-formed sequence. + /// + /// + /// + /// The general calling convention is to call this method in a loop, slicing the buffer by + /// elements on each iteration of the loop. On each iteration of the loop + /// will contain the real scalar value if successfully decoded, or it will contain if + /// the data could not be successfully decoded. This pattern provides convenient automatic U+FFFD substitution of + /// invalid sequences while iterating through the loop. + /// + public static OperationStatus DecodeFromUtf8(ReadOnlySpan source, out Rune result, out int bytesConsumed) + { + // This method follows the Unicode Standard's recommendation for detecting + // the maximal subpart of an ill-formed subsequence. See The Unicode Standard, + // Ch. 3.9 for more details. In summary, when reporting an invalid subsequence, + // it tries to consume as many code units as possible as long as those code + // units constitute the beginning of a longer well-formed subsequence per Table 3-7. + + int index = 0; + + // Try reading input[0]. + + if ((uint)index >= (uint)source.Length) + { + goto NeedsMoreData; + } + + uint tempValue = source[index]; + if (!UnicodeUtility.IsAsciiCodePoint(tempValue)) + { + goto NotAscii; + } + + Finish: + + bytesConsumed = index + 1; + Debug.Assert(bytesConsumed is >= 1 and <= 4); // Valid subsequences are always length [1..4] + result = UnsafeCreate(tempValue); + return OperationStatus.Done; + + NotAscii: + + // Per Table 3-7, the beginning of a multibyte sequence must be a code unit in + // the range [C2..F4]. If it's outside of that range, it's either a standalone + // continuation byte, or it's an overlong two-byte sequence, or it's an out-of-range + // four-byte sequence. + + if (!UnicodeUtility.IsInRangeInclusive(tempValue, 0xC2, 0xF4)) + { + goto FirstByteInvalid; + } + + tempValue = (tempValue - 0xC2) << 6; + + // Try reading input[1]. + + index++; + if ((uint)index >= (uint)source.Length) + { + goto NeedsMoreData; + } + + // Continuation bytes are of the form [10xxxxxx], which means that their two's + // complement representation is in the range [-65..-128]. This allows us to + // perform a single comparison to see if a byte is a continuation byte. + + int thisByteSignExtended = (sbyte)source[index]; + if (thisByteSignExtended >= -64) + { + goto Invalid; + } + + tempValue += (uint)thisByteSignExtended; + tempValue += 0x80; // remove the continuation byte marker + tempValue += (0xC2 - 0xC0) << 6; // remove the leading byte marker + + if (tempValue < 0x0800) + { + Debug.Assert(UnicodeUtility.IsInRangeInclusive(tempValue, 0x0080, 0x07FF)); + goto Finish; // this is a valid 2-byte sequence + } + + // This appears to be a 3- or 4-byte sequence. Since per Table 3-7 we now have + // enough information (from just two code units) to detect overlong or surrogate + // sequences, we need to perform these checks now. + + if (!UnicodeUtility.IsInRangeInclusive(tempValue, ((0xE0 - 0xC0) << 6) + (0xA0 - 0x80), ((0xF4 - 0xC0) << 6) + (0x8F - 0x80))) + { + // The first two bytes were not in the range [[E0 A0]..[F4 8F]]. + // This is an overlong 3-byte sequence or an out-of-range 4-byte sequence. + goto Invalid; + } + + if (UnicodeUtility.IsInRangeInclusive(tempValue, ((0xED - 0xC0) << 6) + (0xA0 - 0x80), ((0xED - 0xC0) << 6) + (0xBF - 0x80))) + { + // This is a UTF-16 surrogate code point, which is invalid in UTF-8. + goto Invalid; + } + + if (UnicodeUtility.IsInRangeInclusive(tempValue, ((0xF0 - 0xC0) << 6) + (0x80 - 0x80), ((0xF0 - 0xC0) << 6) + (0x8F - 0x80))) + { + // This is an overlong 4-byte sequence. + goto Invalid; + } + + // The first two bytes were just fine. We don't need to perform any other checks + // on the remaining bytes other than to see that they're valid continuation bytes. + + // Try reading input[2]. + + index++; + if ((uint)index >= (uint)source.Length) + { + goto NeedsMoreData; + } + + thisByteSignExtended = (sbyte)source[index]; + if (thisByteSignExtended >= -64) + { + goto Invalid; // this byte is not a UTF-8 continuation byte + } + + tempValue <<= 6; + tempValue += (uint)thisByteSignExtended; + tempValue += 0x80; // remove the continuation byte marker + tempValue -= (0xE0 - 0xC0) << 12; // remove the leading byte marker + + if (tempValue <= 0xFFFF) + { + Debug.Assert(UnicodeUtility.IsInRangeInclusive(tempValue, 0x0800, 0xFFFF)); + goto Finish; // this is a valid 3-byte sequence + } + + // Try reading input[3]. + + index++; + if ((uint)index >= (uint)source.Length) + { + goto NeedsMoreData; + } + + thisByteSignExtended = (sbyte)source[index]; + if (thisByteSignExtended >= -64) + { + goto Invalid; // this byte is not a UTF-8 continuation byte + } + + tempValue <<= 6; + tempValue += (uint)thisByteSignExtended; + tempValue += 0x80; // remove the continuation byte marker + tempValue -= (0xF0 - 0xE0) << 18; // remove the leading byte marker + + UnicodeDebug.AssertIsValidSupplementaryPlaneScalar(tempValue); + goto Finish; // this is a valid 4-byte sequence + + FirstByteInvalid: + + index = 1; // Invalid subsequences are always at least length 1. + + Invalid: + + Debug.Assert(index is >= 1 and <= 3); // Invalid subsequences are always length 1..3 + bytesConsumed = index; + result = ReplacementChar; + return OperationStatus.InvalidData; + + NeedsMoreData: + + Debug.Assert(index is >= 0 and <= 3); // Incomplete subsequences are always length 0..3 + bytesConsumed = index; + result = ReplacementChar; + return OperationStatus.NeedMoreData; + } + + public override bool Equals(object? obj) => (obj is Rune other) && Equals(other); + + public bool Equals(Rune other) => this == other; + + public override int GetHashCode() => Value; + + /// + /// Encodes this to a UTF-16 destination buffer. + /// + /// The buffer to which to write this value as UTF-16. + /// + /// The number of s written to , + /// or 0 if the destination buffer is not large enough to contain the output. + /// True if the value was written to the buffer; otherwise, false. + public bool TryEncodeToUtf16(Span destination, out int charsWritten) + { + if (destination.Length >= 1) + { + if (IsBmp) + { + charsWritten = 1; + return true; + } + else if (destination.Length >= 2) + { + UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar(_value, out destination[0], out destination[1]); + charsWritten = 2; + return true; + } + } + + // Destination buffer not large enough + + charsWritten = default; + return false; + } + + /// + /// Encodes this to a destination buffer as UTF-8 bytes. + /// + /// The buffer to which to write this value as UTF-8. + /// + /// The number of s written to , + /// or 0 if the destination buffer is not large enough to contain the output. + /// True if the value was written to the buffer; otherwise, false. + public bool TryEncodeToUtf8(Span destination, out int bytesWritten) + { + // The bit patterns below come from the Unicode Standard, Table 3-6. + + if (destination.Length >= 1) + { + if (IsAscii) + { + bytesWritten = 1; + return true; + } + + if (destination.Length >= 2) + { + if (_value <= 0x7FFu) + { + // Scalar 00000yyy yyxxxxxx -> bytes [ 110yyyyy 10xxxxxx ] + bytesWritten = 2; + return true; + } + + if (destination.Length >= 3) + { + if (_value <= 0xFFFFu) + { + // Scalar zzzzyyyy yyxxxxxx -> bytes [ 1110zzzz 10yyyyyy 10xxxxxx ] + bytesWritten = 3; + return true; + } + + if (destination.Length >= 4) + { + // Scalar 000uuuuu zzzzyyyy yyxxxxxx -> bytes [ 11110uuu 10uuzzzz 10yyyyyy 10xxxxxx ] + bytesWritten = 4; + return true; + } + } + } + } + + // Destination buffer not large enough + + bytesWritten = default; + return false; + } + + /// + /// Creates a without performing validation on the input. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static Rune UnsafeCreate(uint scalarValue) => new(scalarValue, false); + } +} + +#endif \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeDebug.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeDebug.cs new file mode 100644 index 000000000000..bdb2444cd1f1 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeDebug.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace System.Text +{ + // Utility class needed for netstandard2.0 System.Text.Rune implementation. + // Copied from: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeDebug.cs + internal static class UnicodeDebug + { + [Conditional("DEBUG")] + internal static void AssertIsBmpCodePoint(uint codePoint) + { + if (!UnicodeUtility.IsBmpCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid BMP code point."); + } + } + + [Conditional("DEBUG")] + internal static void AssertIsHighSurrogateCodePoint(uint codePoint) + { + if (!UnicodeUtility.IsHighSurrogateCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid UTF-16 high surrogate code point."); + } + } + + [Conditional("DEBUG")] + internal static void AssertIsLowSurrogateCodePoint(uint codePoint) + { + if (!UnicodeUtility.IsLowSurrogateCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid UTF-16 low surrogate code point."); + } + } + + [Conditional("DEBUG")] + internal static void AssertIsValidCodePoint(uint codePoint) + { + if (!UnicodeUtility.IsValidCodePoint(codePoint)) + { + Debug.Fail($"The value {ToHexString(codePoint)} is not a valid Unicode code point."); + } + } + + [Conditional("DEBUG")] + internal static void AssertIsValidScalar(uint scalarValue) + { + if (!UnicodeUtility.IsValidUnicodeScalar(scalarValue)) + { + Debug.Fail($"The value {ToHexString(scalarValue)} is not a valid Unicode scalar value."); + } + } + + [Conditional("DEBUG")] + internal static void AssertIsValidSupplementaryPlaneScalar(uint scalarValue) + { + if (!UnicodeUtility.IsValidUnicodeScalar(scalarValue) || UnicodeUtility.IsBmpCodePoint(scalarValue)) + { + Debug.Fail($"The value {ToHexString(scalarValue)} is not a valid supplementary plane Unicode scalar value."); + } + } + + /// + /// Formats a code point as the hex string "U+XXXX". + /// + /// + /// The input value doesn't have to be a real code point in the Unicode codespace. It can be any integer. + /// + private static string ToHexString(uint codePoint) + { + return FormattableString.Invariant($"U+{codePoint:X4}"); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeUtility.cs b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeUtility.cs new file mode 100644 index 000000000000..c38fd2504c89 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/Utilities/UnicodeUtility.cs @@ -0,0 +1,187 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace System.Text +{ + // Utility class needed for netstandard2.0 System.Text.Rune implementation. + // Copied from: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeUtility.cs + internal static class UnicodeUtility + { + /// + /// The Unicode replacement character U+FFFD. + /// + public const uint ReplacementChar = 0xFFFD; + + /// + /// Returns the Unicode plane (0 through 16, inclusive) which contains this code point. + /// + public static int GetPlane(uint codePoint) + { + UnicodeDebug.AssertIsValidCodePoint(codePoint); + + return (int)(codePoint >> 16); + } + + /// + /// Returns a Unicode scalar value from two code points representing a UTF-16 surrogate pair. + /// + public static uint GetScalarFromUtf16SurrogatePair(uint highSurrogateCodePoint, uint lowSurrogateCodePoint) + { + UnicodeDebug.AssertIsHighSurrogateCodePoint(highSurrogateCodePoint); + UnicodeDebug.AssertIsLowSurrogateCodePoint(lowSurrogateCodePoint); + + // This calculation comes from the Unicode specification, Table 3-5. + // Need to remove the D800 marker from the high surrogate and the DC00 marker from the low surrogate, + // then fix up the "wwww = uuuuu - 1" section of the bit distribution. The code is written as below + // to become just two instructions: shl, lea. + + return (highSurrogateCodePoint << 10) + lowSurrogateCodePoint - ((0xD800U << 10) + 0xDC00U - (1 << 16)); + } + + /// + /// Given a Unicode scalar value, gets the number of UTF-16 code units required to represent this value. + /// + public static int GetUtf16SequenceLength(uint value) + { + UnicodeDebug.AssertIsValidScalar(value); + + value -= 0x10000; // if value < 0x10000, high byte = 0xFF; else high byte = 0x00 + value += 2 << 24; // if value < 0x10000, high byte = 0x01; else high byte = 0x02 + value >>= 24; // shift high byte down + return (int)value; // and return it + } + + /// + /// Decomposes an astral Unicode scalar into UTF-16 high and low surrogate code units. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void GetUtf16SurrogatesFromSupplementaryPlaneScalar(uint value, out char highSurrogateCodePoint, out char lowSurrogateCodePoint) + { + UnicodeDebug.AssertIsValidSupplementaryPlaneScalar(value); + + // This calculation comes from the Unicode specification, Table 3-5. + + highSurrogateCodePoint = (char)((value + ((0xD800u - 0x40u) << 10)) >> 10); + lowSurrogateCodePoint = (char)((value & 0x3FFu) + 0xDC00u); + } + + /// + /// Given a Unicode scalar value, gets the number of UTF-8 code units required to represent this value. + /// + public static int GetUtf8SequenceLength(uint value) + { + UnicodeDebug.AssertIsValidScalar(value); + + // The logic below can handle all valid scalar values branchlessly. + // It gives generally good performance across all inputs, and on x86 + // it's only six instructions: lea, sar, xor, add, shr, lea. + + // 'a' will be -1 if input is < 0x800; else 'a' will be 0 + // => 'a' will be -1 if input is 1 or 2 UTF-8 code units; else 'a' will be 0 + + int a = ((int)value - 0x0800) >> 31; + + // The number of UTF-8 code units for a given scalar is as follows: + // - U+0000..U+007F => 1 code unit + // - U+0080..U+07FF => 2 code units + // - U+0800..U+FFFF => 3 code units + // - U+10000+ => 4 code units + // + // If we XOR the incoming scalar with 0xF800, the chart mutates: + // - U+0000..U+F7FF => 3 code units + // - U+F800..U+F87F => 1 code unit + // - U+F880..U+FFFF => 2 code units + // - U+10000+ => 4 code units + // + // Since the 1- and 3-code unit cases are now clustered, they can + // both be checked together very cheaply. + + value ^= 0xF800u; + value -= 0xF880u; // if scalar is 1 or 3 code units, high byte = 0xFF; else high byte = 0x00 + value += 4 << 24; // if scalar is 1 or 3 code units, high byte = 0x03; else high byte = 0x04 + value >>= 24; // shift high byte down + + // Final return value: + // - U+0000..U+007F => 3 + (-1) * 2 = 1 + // - U+0080..U+07FF => 4 + (-1) * 2 = 2 + // - U+0800..U+FFFF => 3 + ( 0) * 2 = 3 + // - U+10000+ => 4 + ( 0) * 2 = 4 + return (int)value + (a * 2); + } + + /// + /// Returns iff is an ASCII + /// character ([ U+0000..U+007F ]). + /// + /// + /// Per http://www.unicode.org/glossary/#ASCII, ASCII is only U+0000..U+007F. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAsciiCodePoint(uint value) => value <= 0x7Fu; + + /// + /// Returns iff is in the + /// Basic Multilingual Plane (BMP). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsBmpCodePoint(uint value) => value <= 0xFFFFu; + + /// + /// Returns iff is a UTF-16 high surrogate code point, + /// i.e., is in [ U+D800..U+DBFF ], inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHighSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xD800U, 0xDBFFU); + + /// + /// Returns iff is between + /// and , inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) => (value - lowerBound) <= (upperBound - lowerBound); + + /// + /// Returns iff is a UTF-16 low surrogate code point, + /// i.e., is in [ U+DC00..U+DFFF ], inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLowSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xDC00U, 0xDFFFU); + + /// + /// Returns iff is a UTF-16 surrogate code point, + /// i.e., is in [ U+D800..U+DFFF ], inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSurrogateCodePoint(uint value) => IsInRangeInclusive(value, 0xD800U, 0xDFFFU); + + /// + /// Returns iff is a valid Unicode code + /// point, i.e., is in [ U+0000..U+10FFFF ], inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidCodePoint(uint codePoint) => codePoint <= 0x10FFFFU; + + /// + /// Returns iff is a valid Unicode scalar + /// value, i.e., is in [ U+0000..U+D7FF ], inclusive; or [ U+E000..U+10FFFF ], inclusive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidUnicodeScalar(uint value) + { + // This is an optimized check that on x86 is just three instructions: lea, xor, cmp. + // + // After the subtraction operation, the input value is modified as such: + // [ 00000000..0010FFFF ] -> [ FFEF0000..FFFFFFFF ] + // + // We now want to _exclude_ the range [ FFEFD800..FFEFDFFF ] (surrogates) from being valid. + // After the xor, this particular exclusion range becomes [ FFEF0000..FFEF07FF ]. + // + // So now the range [ FFEF0800..FFFFFFFF ] contains all valid code points, + // excluding surrogates. This allows us to perform a single comparison. + + return ((value - 0x110000u) ^ 0xD800u) >= 0xFFEF0800u; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.cs.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.cs.xlf new file mode 100644 index 000000000000..f0fb277dae3a --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.cs.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + Probíhá přidávání do lokalizovatelných řetězců: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + Následující element ze souboru template.json se do lokalizace nezahrne, protože neodpovídá žádným pravidlům pro lokalizovatelné elementy: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Každý podřízený prvek {0} by měl mít jedinečné ID. V současnosti ID {1} sdílí více podřízených prvků. + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + Element JSON {0} musí mít člen {1}. + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + Následující element ze souboru template.json se přeskočí, protože už byl přidaný do seznamu lokalizovatelných řetězců: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + Zdá se, že obsah následujícího souboru je stejný jako dříve. Soubor nebude přepsán. Soubor: {0} + {0} is a file path. + + + Failed to read the existing strings from '{0}' + Existující řetězce z cesty {0} se nepodařilo přečíst. + {0} is a file path. + + + Loading existing localizations from file '{0}' + Existující lokalizace se načítají ze souboru {0}. + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + Soubor již obsahuje lokalizovaný řetězec pro klíče {0}. Stará hodnota bude zachována. + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + Následující soubor templatestrings.json se otevírá pro zápis: {0} + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.de.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.de.xlf new file mode 100644 index 000000000000..c1fa27598be8 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.de.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + Hinzufügen zu lokalisierbaren Zeichenfolgen: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + Das folgende Element in der template.json wird nicht in die Lokalisierungen einbezogen, da es mit keiner der Regeln für lokalisierbare Elemente übereinstimmt: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Jedes untergeordnete Element von \"{0}\" muss eine eindeutige ID aufweisen. Zurzeit wird die ID “{1}” von mehreren untergeordneten Elementen verwendet. + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + Das JSON-Element \"{0}\" muss einen Member \"{1}\" aufweisen. + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + Das folgende Element in der template.json wird übersprungen, da es bereits der Liste der lokalisierbaren Zeichenfolgen hinzugefügt wurde: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + Der Inhalt der folgenden Datei scheint mit den vorherigen identisch zu sein. Die Datei wird nicht überschrieben. Datei: “{0}”. + {0} is a file path. + + + Failed to read the existing strings from '{0}' + Fehler beim Lesen der vorhandenen Zeichenfolgen aus \"{0}\" + {0} is a file path. + + + Loading existing localizations from file '{0}' + Vorhandene Lokalisierungen werden aus der Datei \"{0}\" geladen... + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + Die Datei enthält bereits eine lokalisierte Zeichenfolge für den Schlüssel “{0}”. Der ursprüngliche Wert wird beibehalten. + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + Öffnen der folgenden templatestrings.json-Datei zum Schreiben: \"{0}\". + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.es.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.es.xlf new file mode 100644 index 000000000000..22640519f562 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.es.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + Agregar en cadenas localizables: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + El elemento siguiente en el template.json no se incluirá en las localizaciones porque no coincide con ninguna de las reglas para elementos localizables: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Cada elemento secundario de '{0}' debe tener un id. único. Actualmente, el id. '{1}' se comparte entre varios elementos secundarios. + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + El elemento JSON '{0}' debe tener un miembro '{1}'. + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + Se omitirá el siguiente elemento en el archivo template.json, ya que ya que fue agregado a la lista de cadenas localizables: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + El contenido del archivo siguiente parece ser el mismo que antes. El archivo no se sobrescribirá. Archivo: '{0}' + {0} is a file path. + + + Failed to read the existing strings from '{0}' + Error al leer las cadenas existentes de '{0}' + {0} is a file path. + + + Loading existing localizations from file '{0}' + Cargando las localizaciones existentes desde el archivo '{0}' + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + El archivo ya contiene una cadena localizada para la clave '{0}'. Se conservará el valor antiguo. + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + Abriendo el siguiente archivo templatestrings.json para escritura: '{0}' + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.fr.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.fr.xlf new file mode 100644 index 000000000000..6c102d6489b2 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.fr.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + Ajout dans des chaînes localisables : {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + L’élément suivant dans le fichier template.json ne sera pas inclus dans les localisations, car il ne correspond à aucune règle pour les éléments localisables : {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Chaque enfant de «{0}» doit avoir un ID unique. Actuellement, l’ID «{1}» est partagé par plusieurs enfants. + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + L’élément json «{0}» doit avoir un membre «{1}». + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + L’élément suivant dans le fichier template.json est ignoré, car il a déjà été ajouté à la liste des chaînes localisables : {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + Le contenu du fichier suivant semble identique à celui d’avant. Le fichier ne sera pas remplacé. Fichier : '{0}' + {0} is a file path. + + + Failed to read the existing strings from '{0}' + Échec de la lecture des chaînes existantes à partir de «{0}» + {0} is a file path. + + + Loading existing localizations from file '{0}' + Chargement des localisations existantes à partir du fichier «{0}» + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + Le fichier contient déjà une chaîne localisée pour la clé '{0}'. L’ancienne valeur sera conservée. + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + Ouverture du fichier templatestrings.json suivant pour l’écriture : «{0}» + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.it.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.it.xlf new file mode 100644 index 000000000000..a595fd15f459 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.it.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + Aggiunta nelle stringhe localizzabili: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + Il seguente elemento in template.json non sarà incluso nelle localizzazioni perché non corrisponde ad alcuna regola per gli elementi localizzabili: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + A ogni elemento figlio di ‘{0}’ deve essere assegnato un ID univoco. L'ID ‘{1}’ è attualmente condiviso da più elementi figlio. + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + L'elemento JSON ‘{0}’ deve includere un membro ‘{1}’. + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + Il seguente elemento in template.json verrà ignorato perché è già stato aggiunto all'elenco delle stringhe localizzabili: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + Il contenuto del file seguente sembra essere lo stesso di prima. Il file non verrà sovrascritto. File: '{0}' + {0} is a file path. + + + Failed to read the existing strings from '{0}' + Non è stato possibile leggere le stringhe esistenti da ‘{0}’ + {0} is a file path. + + + Loading existing localizations from file '{0}' + Caricamento localizzazioni esistenti dal file ‘{0}’ + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + Il file contiene già una stringa localizzata per la chiave '{0}'. Il valore precedente verrà mantenuto. + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + Apertura del seguente file templatestrings.json per la scrittura: ‘{0}’ + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ja.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ja.xlf new file mode 100644 index 000000000000..c8bdf3b3628c --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ja.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + ローカライズ可能な文字列に追加しています: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + template.json 内の次の要素は、ローカライズ可能な要素のどの規則にも一致しないため、ローカライズに含まれません: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + '{0}' の各子には一意の ID を指定する必要があります。現在、ID '{1}' は複数の子によって共有されています。 + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + Json 要素 '{0}' には、メンバー '{1}' が必要です。 + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + template.json の次の要素は、ローカライズ可能な文字列の一覧に既に追加されているため、スキップされます: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + 次のファイルの内容は以前と同じようです。このファイルは上書きされません。ファイル: '{0}' + {0} is a file path. + + + Failed to read the existing strings from '{0}' + '{0}' から既存の文字列を読み取れませんでした + {0} is a file path. + + + Loading existing localizations from file '{0}' + ファイル '{0}' から既存のローカライズを読み込んでいます + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + このファイルには、キー '{0}' 用にローカライズされた文字列が既に含まれています。古い値は保持されます。 + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + 次の templatestrings.json ファイルを書き込むために開いています: '{0}' + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ko.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ko.xlf new file mode 100644 index 000000000000..5f68d9ad038f --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ko.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + 지역화 가능한 문자열에 추가 중: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + template.json의 다음 요소는 지역화 가능한 요소에 대한 규칙과 일치하지 않으므로 지역화에 포함되지 않습니다. {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + ‘{0}’의 각 자식에는 고유한 ID가 있어야 합니다. 현재 ID ‘{1}’은(는) 여러 자식이 공유합니다. + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + Json 요소 ‘{0}’에는 ‘{1}’ 구성원이 있어야 합니다. + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + template.json의 다음 요소는 지역화 가능한 문자열 목록에 이미 추가되었으므로 건너뜁니다. {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + 다음 파일의 내용이 이전 내용과 같은 것 같습니다. 파일을 덮어쓰지 않습니다. 파일: '{0}' + {0} is a file path. + + + Failed to read the existing strings from '{0}' + ‘{0}’에서 기존 문자열을 읽지 못했습니다. + {0} is a file path. + + + Loading existing localizations from file '{0}' + ‘{0}’ 파일에서 기존 지역화를 로드하는 중 + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + 파일에 키 '{0}' 대한 지역화된 문자열이 이미 포함되어 있습니다. 이전 값은 유지됩니다. + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + 쓰기를 위해 templatestrings.json 파일을 여는 중: ‘{0}’ + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pl.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pl.xlf new file mode 100644 index 000000000000..c34d7db62889 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pl.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + Dodawanie do tłumaczonych ciągów: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + Następujący element w pliku template.json nie zostanie uwzględniony w lokalizacjach, ponieważ nie jest zgodny z żadną regułą dla tłumaczonych elementów: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Każdy element podrzędny „{0}” powinien mieć unikatowy identyfikator. Obecnie identyfikator „{1}” jest współdzielony przez wiele elementów podrzędnych. + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + Element JSON „{0}” musi mieć element członkowski „{1}”. + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + Następujący element w pliku template.json zostanie pominięty, ponieważ został już dodany do listy tłumaczonych ciągów: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + Zawartość następującego pliku wygląda na taką samą jak wcześniej. Plik nie zostanie zastąpiony. Plik: „{0}” + {0} is a file path. + + + Failed to read the existing strings from '{0}' + Nie można odczytać istniejących ciągów z „{0}” + {0} is a file path. + + + Loading existing localizations from file '{0}' + Ładowanie istniejących lokalizacji z pliku „{0}” + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + Plik zawiera już zlokalizowany ciąg dla klucza „{0}”. Stara wartość zostanie zachowana. + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + Otwieranie następującego pliku templatestrings.json do zapisu: „{0}” + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pt-BR.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pt-BR.xlf new file mode 100644 index 000000000000..52ab91119a02 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.pt-BR.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + Adicionando em cadeia de caracteres localizáveis: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + O seguinte elemento no template.json não será incluído nas localizações porque não corresponde a nenhuma das regras para elementos localizáveis: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Cada filho de '{0}' deve ter uma ID exclusiva. Atualmente, a ID '{1}' é compartilhada por vários filhos. + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + O elemento JSON '{0}' deve ter um membro '{1}'. + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + O seguinte elemento no template.json será ignorado, pois já foi adicionado à lista de cadeia de caracteres localizáveis: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + O conteúdo do arquivo a seguir parece ser o mesmo de antes. O arquivo não será substituído. Arquivo: '{0}' + {0} is a file path. + + + Failed to read the existing strings from '{0}' + Falha ao ler as cadeias de caracteres existentes de '{0}' + {0} is a file path. + + + Loading existing localizations from file '{0}' + Carregando localizações existentes do arquivo '{0}' + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + O arquivo já contém uma caracteres existentes localizada para a chave '{0}'. O antigo valor será preservado. + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + Abrindo o seguinte arquivo templatestrings.json para gravação: '{0}' + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ru.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ru.xlf new file mode 100644 index 000000000000..2a750f20f4a8 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.ru.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + Добавление в локализуемые строки: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + Следующий элемент в файле template.json не будет включен в локализации, так как он не соответствует ни одному из правил для локализованных элементов: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + У каждого дочернего элемента \"{0}\" должен быть уникальный идентификатор. Сейчас идентификатор \"{1}\" используется несколькими дочерними элементами. + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + У элемента JSON \"{0}\" должен быть член \"{1}\". + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + Следующий элемент в файле template.json будет пропущен, так как он уже добавлен в список локализуемых строк: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + Содержимое следующего файла, по-видимому, не изменилось. Этот файл не будет перезаписан. Файл: \"{0}\" + {0} is a file path. + + + Failed to read the existing strings from '{0}' + Не удалось прочитать существующие строки из \"{0}\" + {0} is a file path. + + + Loading existing localizations from file '{0}' + Загрузка существующих локализаций из файла \"{0}\" + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + Этот файл уже содержит локализованную строку для ключа \"{0}\". Прежнее значение будет сохранено. + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + Открытие следующего файла templatestrings.json для записи: \"{0}\" + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.tr.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.tr.xlf new file mode 100644 index 000000000000..50bd8cce7015 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.tr.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + {0}, yerelleştirilebilir dizelere ekleniyor + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + template.json dosyasındaki şu öğe, yerelleştirilebilir öğelerin kurallarıyla eşleşmediğinden yerelleştirmelere dahil edilmeyecek: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + Her '{0}' alt öğesi benzersiz bir kimliğe sahip olmalıdır. Şu anda '{1}' kimliğini birden çok alt öğe paylaşıyor. + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + '{0}' JSON öğesi bir üye '{1}' içermelidir. + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + template.json dosyasındaki şu öğe yerelleştirilebilir dizeler listesine zaten eklendiğinden atlanacak: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + Aşağıdaki dosyanın içeriği öncekiyle aynı gibi görünüyor. Dosyanın üzerine yazılmayacak. Dosya: '{0}' + {0} is a file path. + + + Failed to read the existing strings from '{0}' + '{0}' içindeki mevcut dizeler okunamadı + {0} is a file path. + + + Loading existing localizations from file '{0}' + '{0}' dosyasından mevcut yerelleştirmeler yükleniyor + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + Dosya, '{0}' anahtarı için zaten yerelleştirilmiş bir dize içeriyor. Eski değer korunacak. + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + Şu templatestrings.json dosyası yazma için açılıyor: '{0}' + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hans.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hans.xlf new file mode 100644 index 000000000000..ff1ded401482 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hans.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + 添加到可本地化的字符串: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + 由于与可本地化元素中的任何元素都不匹配,因此本地化中将不会包含 template.json 中的以下元素: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + “{0}”的每个子级应具有唯一 ID。当前,ID“{1}”由多个子级共享。 + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + Json 元素“{0}”必须具有成员“{1}”。 + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + 由于已添加到可本地化的字符串列表,系统将会跳过 template.json 中的以下元素: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + 以下文件的内容似乎与以前相同。该文件不会被覆盖。文件:“{0}” + {0} is a file path. + + + Failed to read the existing strings from '{0}' + 从“{0}”中读取现有字符串失败 + {0} is a file path. + + + Loading existing localizations from file '{0}' + 正在从文件“{0}”加载现有本地化 + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + 该文件已包含键“{0}”的本地化字符串。将保留旧值。 + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + 打开以下 templatestrings.json 文件用于写入:“{0}” + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hant.xlf b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hant.xlf new file mode 100644 index 000000000000..a81b427d306f --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateEngine.TemplateLocalizer.Core/xlf/LocalizableStrings.zh-Hant.xlf @@ -0,0 +1,58 @@ + + + + + + Adding into localizable strings: {0} + 正在新增至可當地語系化的字串: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The following element in the template.json will not be included in the localizations because it does not match any of the rules for localizable elements: {0} + template.json 中的下列元素不會包含在當地語系化中,因為它不符合可當地語系化元素的任何規則: {0} + {0} is any string from a json file. Such as "postActions", "myParameter", "author" etc. + + + Each child of '{0}' should have a unique id. Currently, the id '{1}' is shared by multiple children. + ‘{0}’ 的每個子項都應該有唯一識別碼。目前有多個子項共用識別碼 ‘{1}’。 + {0} is an identifier string similar to "postActions/0/manualInstructions/2/text" +{1} is a user-defined string such as "myPostAction", "pa0", "postActionFirst" etc. + + + Json element '{0}' must have a member '{1}'. + Json 元素 ‘{0}’ 必須有成員 ‘{1}’。 + {0} and {1} are strings such as "postActions", "manualInstructions", "id" etc. + + + The following element in the template.json will be skipped since it was already added to the list of localizable strings: {0} + 因為已將下列元素新增至可當地語系化的字串清單,將略過 template.json 中的下列元素: {0} + {0} is a string similar to "postActions/0/manualInstructions/2/text" + + + The contents of the following file seems to be the same as before. The file will not be overwritten. File: '{0}' + 下列檔案的內容似乎與之前相同。檔案不會被覆寫。檔案: '{0}' + {0} is a file path. + + + Failed to read the existing strings from '{0}' + 無法從 ‘{0}’ 讀取現有字串 + {0} is a file path. + + + Loading existing localizations from file '{0}' + 正在從檔案 ‘{0}’ 載入現有的當地語系化 + {0} is a file path. + + + The file already contains a localized string for key '{0}'. The old value will be preserved. + 檔案已包含金鑰'{0}'的當地語系化字串。將保留舊值。 + {0} is a file path. + + + Opening the following templatestrings.json file for writing: '{0}' + 正在開啟下列 templatestrings.json 檔案以進行寫入: ‘{0}’ + {0} is a file path. + + + + \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/AdditionalDataExtensions.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/AdditionalDataExtensions.cs new file mode 100644 index 000000000000..caca7b5f9831 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/AdditionalDataExtensions.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. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.AdditionalData +{ + internal static class AdditionalDataExtensions + { + internal static IDictionary? ProduceAdditionalData(this ITemplatePackageInfo package, IReadOnlyList producers) + { + Dictionary? additionalData = new Dictionary(); + foreach (var producer in producers) + { + try + { + var data = producer.CreateDataForTemplatePackage(package); + if (data != null) + { + additionalData[producer.DataUniqueName] = data; + } + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception ex) + { + Verbose.WriteLine($"Failed to produce additional data for {package.Name}@{package.Version} with producer {producer.DataUniqueName}, details: {ex}"); + } + } + if (additionalData.Any()) + { + return additionalData; + } + return null; + } + + internal static IDictionary? ProduceAdditionalData(this IScanTemplateInfo template, IReadOnlyList producers, IEngineEnvironmentSettings environmentSettings) + { + Dictionary? additionalData = new Dictionary(); + foreach (var producer in producers) + { + try + { + var data = producer.CreateDataForTemplate(template, environmentSettings); + if (data != null) + { + additionalData[producer.DataUniqueName] = data; + } + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception ex) + { + Verbose.WriteLine($"Failed to produce additional data for {template.Name} with producer {producer.DataUniqueName}, details: {ex}"); + } + } + if (additionalData.Any()) + { + return additionalData; + } + return null; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostDataProducer.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostDataProducer.cs new file mode 100644 index 000000000000..b7a3297badfc --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostDataProducer.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Utils; +using Microsoft.TemplateSearch.Common.Abstractions; +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.AdditionalData +{ + internal class CliHostDataProducer : IAdditionalDataProducer + { + private const string CliHostDataName = "cliHostData"; + private const string CliHostIdentifier = "dotnetcli"; + + private readonly Dictionary _hostDataForPackByTemplate = new(StringComparer.OrdinalIgnoreCase); + + private readonly Dictionary> _hostDataForPack = new(new ITemplatePackageInfoComparer()); + + internal CliHostDataProducer() + { + _hostDataForPackByTemplate = new Dictionary(); + } + + public string DataUniqueName => CliHostDataName; + + public object Data => _hostDataForPackByTemplate; + + public object? CreateDataForTemplate(IScanTemplateInfo template, IEngineEnvironmentSettings environment) + { + _ = template.HostConfigFiles.TryGetValue(CliHostIdentifier, out string? cliHostConfig); + CliHostTemplateDataLoader hostDataLoader = new CliHostTemplateDataLoader(environment); + CliHostTemplateData hostData = hostDataLoader.ReadHostSpecificTemplateData(template.ToITemplateInfo(hostFilePath: cliHostConfig)); + // store the host data if it has any info that could affect searching for this template. + if (hostData.IsHidden || hostData.SymbolInfo.Count > 0) + { + return hostData; + } + return null; + } + + public void CreateDataForTemplatePack(IDownloadedPackInfo packInfo, IReadOnlyList templateList, IEngineEnvironmentSettings environment) + { + CliHostTemplateDataLoader hostDataLoader = new CliHostTemplateDataLoader(environment); + Dictionary dataForPack = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (IScanTemplateInfo template in templateList) + { + _ = template.HostConfigFiles.TryGetValue(CliHostIdentifier, out string? cliHostConfig); + CliHostTemplateData hostData = hostDataLoader.ReadHostSpecificTemplateData(template.ToITemplateInfo(hostFilePath: cliHostConfig)); + + // store the host data if it has any info that could affect searching for this template. + if (hostData.IsHidden || hostData.SymbolInfo.Count > 0) + { + _hostDataForPackByTemplate[template.Identity] = hostData; + dataForPack[template.Identity] = hostData; + } + } + _hostDataForPack[packInfo] = dataForPack; + } + + public object? CreateDataForTemplatePackage(ITemplatePackageInfo packInfo) => null; + + private class ITemplatePackageInfoComparer : IEqualityComparer + { + public bool Equals(ITemplatePackageInfo? x, ITemplatePackageInfo? y) + { + if (x == null && y == null) + { + return true; + } + + if (x == null || y == null) + { + return false; + } + + return x.Name.Equals(y.Name, StringComparison.OrdinalIgnoreCase) + && (x.Version?.Equals(y.Version, StringComparison.OrdinalIgnoreCase) ?? (x.Version == y.Version)); + } + + public int GetHashCode([DisallowNull] ITemplatePackageInfo obj) + { + return new + { + a = obj.Version?.ToLowerInvariant(), + b = obj.Name.ToLowerInvariant() + }.GetHashCode(); + } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostSearchCacheData.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostSearchCacheData.cs new file mode 100644 index 000000000000..ef33279cd738 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostSearchCacheData.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.AdditionalData +{ + public static class CliHostSearchCacheData + { + public const string DataName = "cliHostData"; + private static readonly string[] HostDataPropertyNames = new[] { "isHidden", "SymbolInfo", "UsageExamples" }; + + public static Func Reader => (obj) => + { + if (obj is not JsonObject cacheObject) + { + return CliHostTemplateData.Default; + } + try + { + if (HostDataPropertyNames.Contains(cacheObject.First().Key, StringComparer.OrdinalIgnoreCase)) + { + return new CliHostTemplateData(cacheObject); + } + + //fallback to old behavior + Dictionary cliData = new Dictionary(); + foreach (var data in cacheObject) + { + try + { + cliData[data.Key] = new CliHostTemplateData(data.Value as JsonObject); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error deserializing the cli host specific template data for template {data.Key}, details:{ex}"); + } + } + return cliData; + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error deserializing the cli host specific template data {cacheObject}, details:{ex}"); + } + return CliHostTemplateData.Default; + }; + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostTemplateData.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostTemplateData.cs new file mode 100644 index 000000000000..6c4a78df9765 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostTemplateData.cs @@ -0,0 +1,203 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.AdditionalData +{ + [System.Text.Json.Serialization.JsonConverter(typeof(CliHostTemplateData.CustomJsonConverter))] + internal class CliHostTemplateData + { + private const string IsHiddenKey = "isHidden"; + private const string LongNameKey = "longName"; + private const string ShortNameKey = "shortName"; + private const string AlwaysShowKey = "alwaysShow"; + + internal CliHostTemplateData(JsonObject? jObject) + { + var symbolsInfo = new Dictionary>(); + + if (jObject == null) + { + SymbolInfo = symbolsInfo; + return; + } + + JsonNode? usagesNode = Microsoft.TemplateEngine.JExtensions.GetPropertyCaseInsensitive(jObject, nameof(UsageExamples)); + if (usagesNode is JsonArray usagesArray) + { + UsageExamples = new List(usagesArray.Select(v => v?.ToString()).Where(v => v != null).OfType()); + } + + JsonNode? symbolsNode = Microsoft.TemplateEngine.JExtensions.GetPropertyCaseInsensitive(jObject, nameof(SymbolInfo)); + if (symbolsNode is JsonObject symbols) + { + foreach (var symbolInfo in symbols) + { + if (symbolInfo.Value is not JsonObject symbol) + { + continue; + } + + var symbolProperties = new Dictionary(); + + foreach (var symbolProperty in symbol) + { + symbolProperties[symbolProperty.Key] = symbolProperty.Value?.ToString() ?? string.Empty; + } + + symbolsInfo[symbolInfo.Key] = symbolProperties; + } + } + SymbolInfo = symbolsInfo; + + IsHidden = jObject.TryGetPropertyValue(nameof(IsHidden), out JsonNode? isHiddenNode) + && isHiddenNode is JsonValue isHiddenVal + && isHiddenVal.TryGetValue(out bool boolVal) && boolVal; + + } + + internal CliHostTemplateData( + IReadOnlyDictionary> symbolInfo, + IEnumerable? usageExamples = null, + bool isHidden = false) + { + SymbolInfo = symbolInfo; + UsageExamples = usageExamples?.ToArray() ?? []; + IsHidden = isHidden; + } + + public IReadOnlyList UsageExamples { get; } = []; + + public IReadOnlyDictionary> SymbolInfo { get; } + + public bool IsHidden { get; } + + public HashSet HiddenParameterNames + { + get + { + HashSet hiddenNames = new HashSet(); + foreach (KeyValuePair> paramInfo in SymbolInfo) + { + if (paramInfo.Value.TryGetValue(IsHiddenKey, out string? hiddenStringValue) + && bool.TryParse(hiddenStringValue, out bool hiddenBoolValue) + && hiddenBoolValue) + { + hiddenNames.Add(paramInfo.Key); + } + } + + return hiddenNames; + } + } + + public HashSet ParametersToAlwaysShow + { + get + { + HashSet parametersToAlwaysShow = new HashSet(StringComparer.Ordinal); + foreach (KeyValuePair> paramInfo in SymbolInfo) + { + if (paramInfo.Value.TryGetValue(AlwaysShowKey, out string? alwaysShowValue) + && bool.TryParse(alwaysShowValue, out bool alwaysShowBoolValue) + && alwaysShowBoolValue) + { + parametersToAlwaysShow.Add(paramInfo.Key); + } + } + + return parametersToAlwaysShow; + } + } + + public Dictionary LongNameOverrides + { + get + { + Dictionary map = new Dictionary(); + + foreach (KeyValuePair> paramInfo in SymbolInfo) + { + if (paramInfo.Value.TryGetValue(LongNameKey, out string? longNameOverride)) + { + map.Add(paramInfo.Key, longNameOverride); + } + } + + return map; + } + } + + public Dictionary ShortNameOverrides + { + get + { + Dictionary map = new Dictionary(); + + foreach (KeyValuePair> paramInfo in SymbolInfo) + { + if (paramInfo.Value.TryGetValue(ShortNameKey, out string? shortNameOverride)) + { + map.Add(paramInfo.Key, shortNameOverride); + } + } + + return map; + } + } + + internal static CliHostTemplateData Default { get; } = new CliHostTemplateData(null); + + internal string DisplayNameForParameter(string parameterName) + { + if (SymbolInfo.TryGetValue(parameterName, out IReadOnlyDictionary? configForParam) + && configForParam.TryGetValue(LongNameKey, out string? longName)) + { + return longName; + } + + return parameterName; + } + + private class CustomJsonConverter : System.Text.Json.Serialization.JsonConverter + { + public override CliHostTemplateData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + + public override void Write(Utf8JsonWriter writer, CliHostTemplateData value, JsonSerializerOptions options) + { + if (value == null) + { + return; + } + writer.WriteStartObject(); + if (value.IsHidden) + { + writer.WritePropertyName(nameof(IsHidden)); + writer.WriteBooleanValue(value.IsHidden); + } + if (value.SymbolInfo.Any()) + { + writer.WritePropertyName(nameof(SymbolInfo)); + JsonSerializer.Serialize(writer, value.SymbolInfo, options); + } + + if (value.UsageExamples != null && value.UsageExamples.Any(e => !string.IsNullOrWhiteSpace(e))) + { + writer.WritePropertyName(nameof(UsageExamples)); + writer.WriteStartArray(); + foreach (string example in value.UsageExamples) + { + if (!string.IsNullOrWhiteSpace(example)) + { + writer.WriteStringValue(example); + } + } + writer.WriteEndArray(); + } + writer.WriteEndObject(); + } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostTemplateDataLoader.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostTemplateDataLoader.cs new file mode 100644 index 000000000000..3a1f9080a19a --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/CliHostTemplateDataLoader.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.AdditionalData +{ + internal class CliHostTemplateDataLoader + { + private readonly IEngineEnvironmentSettings _engineEnvironment; + + internal CliHostTemplateDataLoader(IEngineEnvironmentSettings engineEnvironment) + { + _engineEnvironment = engineEnvironment; + } + + internal CliHostTemplateData ReadHostSpecificTemplateData(ITemplateInfo templateInfo) + { + IMountPoint? mountPoint = null; + + if (templateInfo is ITemplateInfoHostJsonCache { HostData: string hostData }) + { + try + { + if (!string.IsNullOrWhiteSpace(hostData)) + { + JsonObject jObject = JExtensions.ParseJsonObject(hostData); + return new CliHostTemplateData(jObject); + } + } + catch (Exception e) + { + Console.Error.WriteLine( + "Failed to load dotnet CLI host data for template {0} from cache.", + templateInfo.ShortNameList?[0] ?? templateInfo.Name); + Console.Error.WriteLine("Details: {0}", e); + } + } + + IFile? file = null; + try + { + if (!string.IsNullOrEmpty(templateInfo.HostConfigPlace) && _engineEnvironment.TryGetMountPoint(templateInfo.MountPointUri, out mountPoint) && mountPoint != null) + { + file = mountPoint.FileInfo(templateInfo.HostConfigPlace); + if (file != null && file.Exists) + { + using Stream stream = file.OpenRead(); + using TextReader textReader = new StreamReader(stream, true); + string jsonContent = textReader.ReadToEnd(); + var jsonData = JExtensions.ParseJsonObject(jsonContent); + + return new CliHostTemplateData(jsonData); + } + } + } + catch (Exception e) + { + Console.Error.WriteLine( + "Failed to load dotnet CLI host data for template {0} from {1}. The host data will be ignored.", + templateInfo.ShortNameList?[0] ?? templateInfo.Name, + file?.GetDisplayPath() ?? (templateInfo.MountPointUri + templateInfo.HostConfigPlace)); + Console.Error.WriteLine("Details: {0}", e); + } + finally + { + mountPoint?.Dispose(); + } + return CliHostTemplateData.Default; + } + } + +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/IAdditionalDataProducer.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/IAdditionalDataProducer.cs new file mode 100644 index 000000000000..24d56f7869f5 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/AdditionalData/IAdditionalDataProducer.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateSearch.Common.Abstractions; +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.AdditionalData +{ + internal interface IAdditionalDataProducer + { + string DataUniqueName { get; } + + [Obsolete] + object? Data { get; } + + object? CreateDataForTemplatePackage(ITemplatePackageInfo packInfo); + + object? CreateDataForTemplate(IScanTemplateInfo templates, IEngineEnvironmentSettings environment); + + [Obsolete] + void CreateDataForTemplatePack(IDownloadedPackInfo packInfo, IReadOnlyList templates, IEngineEnvironmentSettings environment); + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/CommandArgs.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/CommandArgs.cs new file mode 100644 index 000000000000..96958f1e45cd --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/CommandArgs.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateSearch.TemplateDiscovery +{ + internal enum SupportedQueries + { + PackageTypeQuery, + TemplateQuery + } + + internal class CommandArgs + { + internal CommandArgs(DirectoryInfo outputPath) + { + OutputPath = outputPath; + } + + internal DirectoryInfo? LocalPackagePath { get; init; } + + internal DirectoryInfo OutputPath { get; init; } + + internal int PageSize { get; init; } + + internal bool SaveCandidatePacks { get; init; } + + internal bool RunOnlyOnePage { get; init; } + + internal bool IncludePreviewPacks { get; init; } + + internal bool DontFilterOnTemplateJson { get; init; } + + internal bool Verbose { get; init; } + + internal bool TestEnabled { get; init; } + + internal IReadOnlyList Queries { get; init; } = new List(); + + internal bool DiffMode { get; init; } + + internal FileInfo? DiffOverrideSearchCacheLocation { get; init; } + + internal FileInfo? DiffOverrideKnownPackagesLocation { get; init; } + + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/ConsoleExtensions.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/ConsoleExtensions.cs new file mode 100644 index 000000000000..febf91b9e696 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/ConsoleExtensions.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateSearch.TemplateDiscovery +{ + internal static class ConsoleExtensions + { + public static string Verbose(this string text) + { + return $"[verbose] {text}"; + } + } + + /// + /// Use this class to do verbose output. + /// + internal static class Verbose + { + /// + /// Defines if verbose output is enabled. + /// + internal static bool IsEnabled { get; set; } + + /// + /// Writes the output conditionally if verbose mode is enabled. + /// + /// text to write. + internal static void WriteLine(string text) + { + if (IsEnabled) + { + Console.WriteLine(text.Verbose()); + } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Filters/FilterNonMicrosoftAuthors.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Filters/FilterNonMicrosoftAuthors.cs new file mode 100644 index 000000000000..d0158060c38c --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Filters/FilterNonMicrosoftAuthors.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Edge; +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.Filters; + +internal class FilterNonMicrosoftAuthors +{ + private const string FilterId = "Template.json contains Author=Microsoft"; + + private static readonly ITemplateEngineHost Host = TemplateEngineHostHelper.CreateHost("filterHost"); + + internal static Func SetupPackFilter() + { + static PreFilterResult Filter(IDownloadedPackInfo packInfo) + { + // All NuGet packages that start with Microsoft. are OK since that is protected prefix on NuGet.org + if (packInfo.Name.StartsWith("Microsoft.")) + { + return new PreFilterResult(FilterId, isFiltered: false); + } + using EngineEnvironmentSettings environmentSettings = new EngineEnvironmentSettings(Host, virtualizeSettings: true); + foreach (IMountPointFactory factory in environmentSettings.Components.OfType()) + { + if (factory.TryMount(environmentSettings, null, packInfo.Path, out IMountPoint? mountPoint)) + { + foreach (var templateJson in mountPoint!.Root.EnumerateFiles("template.json", SearchOption.AllDirectories)) + { + try + { + using var streamReader = new StreamReader(templateJson.OpenRead()); + var jObject = JExtensions.ParseJsonObject(streamReader.ReadToEnd()); + var author = jObject.ToString("author"); + if (author?.Contains("microsoft", StringComparison.OrdinalIgnoreCase) ?? false) + { + return new PreFilterResult(FilterId, isFiltered: true, $"{templateJson.FullPath} has Author=Microsoft and package id is {packInfo.Name}"); + } + } + catch (Exception) + { + return new PreFilterResult(FilterId, isFiltered: false); + } + } + mountPoint.Dispose(); + } + } + return new PreFilterResult(FilterId, isFiltered: false); + } + + return Filter; + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Filters/SkipTemplatePacksFilter.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Filters/SkipTemplatePacksFilter.cs new file mode 100644 index 000000000000..8d3a6bcf7d08 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Filters/SkipTemplatePacksFilter.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.Filters +{ + internal sealed class SkipTemplatePacksFilter + { + private const string FilterId = "Permanently skipped packages"; + + private static readonly List PackagesToBeSkipped = new List + { + "microsoft.dotnet.common.itemtemplates", + "microsoft.dotnet.common.projecttemplates", + "microsoft.dotnet.test.projecttemplates", + "microsoft.dotnet.web.itemtemplates", + "microsoft.dotnet.web.projecttemplates", + "microsoft.dotnet.web.spa.projecttemplates", + "microsoft.dotnet.winforms.projecttemplates", + "microsoft.dotnet.wpf.projecttemplates", + //NUnit package is included to SDK, however not managed by Microsoft - keep it in to check for updates + //"nunit3.dotnetnew.template", + "microsoft.aspnetcore.components.webassembly.template", + "microsoft.maui.templates", + "microsoft.android.templates", + "microsoft.ios.templates", + "microsoft.maccatalyst.templates", + "microsoft.macos.templates", + "microsoft.tvos.templates" + + }; + + internal static Func SetupPackFilter() + { + static PreFilterResult Filter(IDownloadedPackInfo packInfo) + { + foreach (string package in PackagesToBeSkipped) + { + if (packInfo.Name.StartsWith(package, StringComparison.OrdinalIgnoreCase)) + { + return new PreFilterResult(FilterId, isFiltered: true, $"Package {packInfo.Name} is skipped as it matches the package name to be permanently skipped."); + } + } + return new PreFilterResult(FilterId, isFiltered: false); + } + + return Filter; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Filters/TemplateJsonExistencePackFilter.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Filters/TemplateJsonExistencePackFilter.cs new file mode 100644 index 000000000000..ee7318ef9a2a --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Filters/TemplateJsonExistencePackFilter.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Edge; +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.Filters +{ + internal static class TemplateJsonExistencePackFilter + { + private const string FilterId = "Template.json existence"; + + private static readonly ITemplateEngineHost Host = TemplateEngineHostHelper.CreateHost("filterHost"); + + internal static Func SetupPackFilter() + { + static PreFilterResult Filter(IDownloadedPackInfo packInfo) + { + using EngineEnvironmentSettings environmentSettings = new EngineEnvironmentSettings(Host, virtualizeSettings: true); + foreach (IMountPointFactory factory in environmentSettings.Components.OfType()) + { + if (factory.TryMount(environmentSettings, null, packInfo.Path, out IMountPoint? mountPoint)) + { + bool hasTemplateJson = mountPoint!.Root.EnumerateFiles("template.json", SearchOption.AllDirectories).Any(); + mountPoint.Dispose(); + + if (hasTemplateJson) + { + return new PreFilterResult(FilterId, isFiltered: false); + } + + break; // this factory mounted the pack. No more checking is needed. + } + } + + return new PreFilterResult(FilterId, isFiltered: true, "Package did not contain any template.json files"); + } + + return Filter; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Microsoft.TemplateSearch.TemplateDiscovery.csproj b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Microsoft.TemplateSearch.TemplateDiscovery.csproj new file mode 100644 index 000000000000..3824b7cbca60 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Microsoft.TemplateSearch.TemplateDiscovery.csproj @@ -0,0 +1,39 @@ + + + + $(NetMinimum);$(NetCurrent) + Exe + true + false + true + Major + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NuGetPackSourceCheckerFactory.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NuGetPackSourceCheckerFactory.cs new file mode 100644 index 000000000000..bce56e4e1c88 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NuGetPackSourceCheckerFactory.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.TemplateEngine; +using Microsoft.TemplateSearch.Common; +using Microsoft.TemplateSearch.TemplateDiscovery.AdditionalData; +using Microsoft.TemplateSearch.TemplateDiscovery.Filters; +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.NuGet +{ + internal class NuGetPackSourceCheckerFactory : IPackCheckerFactory + { + private static readonly Dictionary SupportedProviders = new Dictionary() + { + { SupportedQueries.PackageTypeQuery, "packageType=Template" }, + { SupportedQueries.TemplateQuery, "q=template" } + }; + + public async Task CreatePackSourceCheckerAsync(CommandArgs config, CancellationToken cancellationToken) + { + List providers = new List(); + + if (!config.Queries.Any()) + { + providers.AddRange(SupportedProviders.Select(kvp => new NuGetPackProvider(kvp.Key.ToString(), kvp.Value, config.OutputPath, config.PageSize, config.RunOnlyOnePage, config.IncludePreviewPacks))); + } + else + { + foreach (SupportedQueries provider in config.Queries.Distinct()) + { + providers.Add(new NuGetPackProvider(provider.ToString(), SupportedProviders[provider], config.OutputPath, config.PageSize, config.RunOnlyOnePage, config.IncludePreviewPacks)); + } + } + + List> preFilterList = new List>(); + + if (!config.DontFilterOnTemplateJson) + { + preFilterList.Add(TemplateJsonExistencePackFilter.SetupPackFilter()); + } + preFilterList.Add(SkipTemplatePacksFilter.SetupPackFilter()); + preFilterList.Add(FilterNonMicrosoftAuthors.SetupPackFilter()); + + PackPreFilterer preFilterer = new PackPreFilterer(preFilterList); + + IReadOnlyList additionalDataProducers = new List() + { + new CliHostDataProducer() + }; + + TemplateSearchCache? existingCache = config.DiffMode ? await LoadExistingCacheAsync(config, cancellationToken).ConfigureAwait(false) : null; + IEnumerable? knownPackages = config.DiffMode ? await LoadKnownPackagesListAsync(config, cancellationToken).ConfigureAwait(false) : null; + + return new PackSourceChecker(providers, preFilterer, additionalDataProducers, config.SaveCandidatePacks, existingCache, knownPackages); + } + + private static async Task?> LoadKnownPackagesListAsync(CommandArgs config, CancellationToken cancellationToken) + { + Verbose.WriteLine($"Loading existing non-packages information."); + const string uri = "https://dotnettemplating.blob.core.windows.net/search/nonTemplatePacks.json"; + + FileInfo? fileLocation = config.DiffOverrideKnownPackagesLocation; + if (fileLocation == null) + { + await DownloadUriToFileAsync(uri, "non-packages.json", cancellationToken).ConfigureAwait(false); + fileLocation = new FileInfo("non-packages.json"); + } + Verbose.WriteLine($"Opening {fileLocation.FullName}"); + + using var fileStream = fileLocation.OpenRead(); + return JsonSerializer.Deserialize>(fileStream); + } + + private static async Task LoadExistingCacheAsync(CommandArgs config, CancellationToken cancellationToken) + { + Verbose.WriteLine($"Loading existing cache information."); + // aka.ms link should point to https://dotnet-templating-hrdkctdrgkacbyek.b01.azurefd.net/search/NuGetTemplateSearchInfoVer2.json or + // whatever the future absolute URL for the JSON file is. + const string uri = "https://aka.ms/dotnet/templating/searchcacheurl"; + + FileInfo? cacheFileLocation = config.DiffOverrideSearchCacheLocation; + + if (cacheFileLocation == null) + { + await DownloadUriToFileAsync(uri, "currentSearchCache.json", cancellationToken).ConfigureAwait(false); + cacheFileLocation = new FileInfo("currentSearchCache.json"); + } + Verbose.WriteLine($"Opening {cacheFileLocation.FullName}"); + using var fileStream = cacheFileLocation.OpenRead(); + JsonObject cacheObject = JExtensions.ParseJsonObject(new StreamReader(fileStream, System.Text.Encoding.UTF8, true).ReadToEnd()); + return TemplateSearchCache.FromJObject(cacheObject, NullLogger.Instance, new Dictionary>() { { CliHostSearchCacheData.DataName, CliHostSearchCacheData.Reader } }); + } + + private static async Task DownloadUriToFileAsync(string uri, string filePath, CancellationToken cancellationToken) + { + try + { + HttpClientHandler handler = new HttpClientHandler() + { + CheckCertificateRevocationList = true, + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }; + using HttpClient client = new HttpClient(handler); + using HttpResponseMessage response = await client.GetAsync(uri, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + string resultText = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + File.WriteAllText(filePath, resultText); + Verbose.WriteLine($"{uri} was successfully downloaded to {filePath}."); + return; + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception e) + { + Console.WriteLine("Failed to download {0}, details: {1}", uri, e); + throw; + } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NugetPackProvider.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NugetPackProvider.cs new file mode 100644 index 000000000000..fe47522b996b --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NugetPackProvider.cs @@ -0,0 +1,286 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.TemplateEngine; +using Microsoft.TemplateSearch.Common.Abstractions; +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; +using NuGet.Common; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.NuGet +{ + internal class NuGetPackProvider : IPackProvider + { + private const string NuGetOrgFeed = "https://api.nuget.org/v3/index.json"; + private const string DownloadPackageFileNameFormat = "{0}.{1}.nupkg"; + private const string DownloadedPacksDir = "DownloadedPacks"; + private readonly string _packageTempPath; + private readonly int _pageSize; + private readonly bool _runOnlyOnePage; + private readonly SourceRepository _repository; + private readonly SourceCacheContext _cacheContext = new SourceCacheContext(); + private readonly FindPackageByIdResource _downloadResource; + private readonly bool _includePreview; + private readonly string _searchUriFormat; + + internal NuGetPackProvider(string name, string query, DirectoryInfo packageTempBasePath, int pageSize, bool runOnlyOnePage, bool includePreviewPacks) + { + Name = name; + _pageSize = pageSize; + _runOnlyOnePage = runOnlyOnePage; + _packageTempPath = Path.GetFullPath(Path.Combine(packageTempBasePath.FullName, DownloadedPacksDir, Name)); + _repository = Repository.Factory.GetCoreV3(NuGetOrgFeed); + ServiceIndexResourceV3 indexResource = _repository.GetResource(); + IReadOnlyList searchResources = indexResource.GetServiceEntries("SearchQueryService"); + _downloadResource = _repository.GetResource(); + _includePreview = includePreviewPacks; + + if (!searchResources.Any()) + { + throw new Exception($"{NuGetOrgFeed} does not support search API (SearchQueryService)"); + } + + _searchUriFormat = $"{searchResources[0].Uri}?{query}&skip={{0}}&take={{1}}&prerelease={includePreviewPacks}&semVerLevel=2.0.0"; + + if (!Directory.Exists(_packageTempPath)) + { + Directory.CreateDirectory(_packageTempPath); + } + } + + public string Name { get; } + + public bool SupportsGetPackageInfoViaApi => true; + + public async IAsyncEnumerable GetCandidatePacksAsync([EnumeratorCancellation] CancellationToken token) + { + int skip = 0; + bool done = false; + int packCount = 0; + + int totalPackCount = 0; + int pageSize = _pageSize; + + do + { + //NuGet search API limit is 3000, so try to get all the packages exceeding the limit. + if (skip + pageSize > 3000) + { + //get all the packages up to 3000 + pageSize = skip + pageSize - 3000; + } + if (skip >= 3000) + { + //try to get all remaining packages + skip = 3000; + pageSize = totalPackCount - 3000; + + //pageSize limit is 1000 + //therefore max amount of packages that can be retrieved is 4000. + if (pageSize > 1000) + { + pageSize = 1000; + } + } + string queryString = string.Format(_searchUriFormat, skip, pageSize); + + Uri queryUri = new Uri(queryString); + using HttpClient client = new HttpClient(new HttpClientHandler() { CheckCertificateRevocationList = true }); + using HttpResponseMessage response = await client.GetAsync(queryUri, token).ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + string responseText = await response.Content.ReadAsStringAsync(token).ConfigureAwait(false); + + NuGetPackageSearchResult resultsForPage = NuGetPackageSearchResult.FromJObject(JExtensions.ParseJsonObject(responseText)); + totalPackCount = resultsForPage.TotalHits; + if (resultsForPage.Data.Count > 0) + { + skip += pageSize; + packCount += resultsForPage.Data.Count; + foreach (NuGetPackageSourceInfo sourceInfo in resultsForPage.Data) + { + yield return sourceInfo; + } + } + //4000 is NuGet limit, stop after 4000 is processed. + if (totalPackCount == packCount || (totalPackCount > 4000 && packCount == 4000)) + { + if (totalPackCount > 4000) + { + Console.WriteLine($"Warning: {totalPackCount} packages were found, but only first 4000 packages can be retrieved. Other packages will be skipped."); + } + done = true; + } + else if (skip > 3000 || skip >= totalPackCount) + { + Console.WriteLine($"Failed to get all search results from NuGet: expected {totalPackCount}, retrieved: {packCount}."); + throw new Exception("Failed to get search results from NuGet search API."); + } + + } + else + { + Console.WriteLine($"Unexpected response from NuGet: code {response.StatusCode}, details: {response}."); + throw new Exception("Failed to get search results from NuGet search API."); + } + } + while (!done && !_runOnlyOnePage); + } + + public async Task DownloadPackageAsync(ITemplatePackageInfo packinfo, CancellationToken token) + { + string packageFileName = string.Format(DownloadPackageFileNameFormat, packinfo.Name, packinfo.Version); + string outputPackageFileNameFullPath = Path.Combine(_packageTempPath, packageFileName); + + try + { + if (File.Exists(outputPackageFileNameFullPath)) + { + return new DownloadedPackInfo(packinfo, outputPackageFileNameFullPath); + } + else + { + using Stream packageStream = File.Create(outputPackageFileNameFullPath); + if (await _downloadResource.CopyNupkgToStreamAsync( + packinfo.Name, + new NuGetVersion(packinfo.Version!), + packageStream, + _cacheContext, + NullLogger.Instance, + token).ConfigureAwait(false)) + { + return new DownloadedPackInfo(packinfo, outputPackageFileNameFullPath); + } + else + { + throw new Exception($"Download failed: {nameof(_downloadResource.CopyNupkgToStreamAsync)} returned false."); + } + } + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception e) + { + Console.WriteLine($"Failed to download package {packinfo.Name} {packinfo.Version}, reason: {e}."); + throw; + } + } + + public async Task GetPackageCountAsync(CancellationToken token) + { + string queryString = string.Format(_searchUriFormat, 0, _pageSize); + Uri queryUri = new Uri(queryString); + using HttpClient client = new HttpClient(new HttpClientHandler() { CheckCertificateRevocationList = true }); + using HttpResponseMessage response = await client.GetAsync(queryUri, token).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + string responseText = await response.Content.ReadAsStringAsync(token).ConfigureAwait(false); + NuGetPackageSearchResult resultsForPage = NuGetPackageSearchResult.FromJObject(JExtensions.ParseJsonObject(responseText)); + return resultsForPage.TotalHits; + } + + public async Task DeleteDownloadedPacksAsync() + { + for (int i = 0; i < 5; i++) + { + try + { + Directory.Delete(_packageTempPath, true); + return; + } + catch (IOException) + { + Console.WriteLine($"Failed to remove {_packageTempPath}, retrying in 1 sec"); + } + await Task.Delay(1000).ConfigureAwait(false); + } + Console.WriteLine($"Failed to remove {_packageTempPath}, remove it manually."); + } + + public async Task<(ITemplatePackageInfo PackageInfo, bool Removed)> GetPackageInfoAsync(string packageIdentifier, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(packageIdentifier)) + { + throw new ArgumentException($"{nameof(packageIdentifier)} cannot be null or empty", nameof(packageIdentifier)); + } + + try + { + PackageMetadataResource resource = await _repository.GetResourceAsync(cancellationToken).ConfigureAwait(false); + IEnumerable foundPackages = await resource.GetMetadataAsync( + packageIdentifier, + includePrerelease: _includePreview, + includeUnlisted: true, + _cacheContext, + NullLogger.Instance, + cancellationToken).ConfigureAwait(false); + + if (!foundPackages.Any()) + { + Console.WriteLine($"Package {packageIdentifier} was not found."); + return default; + } + + if (foundPackages.Any(package => package.IsListed)) + { + IPackageSearchMetadata latestPackage = foundPackages + .Where(package => package.IsListed) + .Aggregate((max, current) => + { + return current.Identity.Version > max.Identity.Version ? current : max; + }); + return (new NuGetPackInfo(latestPackage), false); + } + + IPackageSearchMetadata latestUnlistedPackage = foundPackages + .Aggregate((max, current) => + { + return current.Identity.Version > max.Identity.Version ? current : max; + }); + + return (new NuGetPackInfo(latestUnlistedPackage), true); + + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception ex) + { + Console.WriteLine($"Failed to get information about package {packageIdentifier}, details: {ex}"); + return default; + } + } + + private class NuGetPackInfo : ITemplatePackageInfo + { + internal NuGetPackInfo(IPackageSearchMetadata packageSearchMetadata) + { + Name = packageSearchMetadata.Identity.Id; + Version = packageSearchMetadata.Identity.Version.ToString(); + TotalDownloads = packageSearchMetadata.DownloadCount ?? 0; + Reserved = packageSearchMetadata.PrefixReserved; + Description = packageSearchMetadata.Description; + IconUrl = packageSearchMetadata.IconUrl?.ToString(); + } + + public string Name { get; } + + public string? Version { get; } + + public long TotalDownloads { get; } + + public IReadOnlyList Owners => []; + + public bool Reserved { get; } + + public string? Description { get; } + + public string? IconUrl { get; } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NugetPackageSearchResult.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NugetPackageSearchResult.cs new file mode 100644 index 000000000000..021ccc35bcbf --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NugetPackageSearchResult.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.NuGet +{ + internal class NuGetPackageSearchResult + { + internal int TotalHits { get; private set; } + + internal List Data { get; } = new List(); + + //property names are explained here: https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource + internal static NuGetPackageSearchResult FromJObject(JsonObject entry) + { + NuGetPackageSearchResult searchResult = new NuGetPackageSearchResult + { + TotalHits = entry.ToInt32("totalHits") + }; + var dataArray = entry.Get("data"); + if (dataArray != null) + { + foreach (JsonNode? data in dataArray) + { + if (data is JsonObject dataObj) + { + searchResult.Data.Add(NuGetPackageSourceInfo.FromJObject(dataObj)); + } + } + + } + return searchResult; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NugetPackageSourceInfo.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NugetPackageSourceInfo.cs new file mode 100644 index 000000000000..e8834929810a --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/NuGet/NugetPackageSourceInfo.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine; +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.NuGet +{ + internal class NuGetPackageSourceInfo : ITemplatePackageInfo, IEquatable + { + internal NuGetPackageSourceInfo(string id, string version) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException($"'{nameof(id)}' cannot be null or whitespace.", nameof(id)); + } + + if (string.IsNullOrWhiteSpace(version)) + { + throw new ArgumentException($"'{nameof(version)}' cannot be null or whitespace.", nameof(version)); + } + + Name = id; + Version = version; + } + + public string Name { get; } + + public string Version { get; } + + public long TotalDownloads { get; private set; } + + public IReadOnlyList Owners { get; private set; } = []; + + public bool Reserved { get; private set; } + + public string? Description { get; private set; } + + public string? IconUrl { get; private set; } + + //property names are explained here: https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource + internal static NuGetPackageSourceInfo FromJObject(JsonObject entry) + { + string id = entry.ToString("id") ?? throw new ArgumentException($"{nameof(entry)} doesn't have \"id\" property.", nameof(entry)); + string version = entry.ToString("version") ?? throw new ArgumentException($"{nameof(entry)} doesn't have \"version\" property.", nameof(entry)); + NuGetPackageSourceInfo sourceInfo = new NuGetPackageSourceInfo(id, version) + { + TotalDownloads = entry.ToInt32("totalDownloads"), + Owners = entry.Get("owners").JTokenStringOrArrayToCollection([]), + Reserved = entry.ToBool("verified"), + Description = entry.ToString("description"), + IconUrl = entry.ToString("iconUrl") + }; + + return sourceInfo; + } + +#pragma warning disable SA1202 // Elements should be ordered by access + public override bool Equals(object? obj) +#pragma warning restore SA1202 // Elements should be ordered by access + { + if (obj is NuGetPackageSourceInfo info) + { + return Name.Equals(info.Name, StringComparison.OrdinalIgnoreCase) && Version.Equals(info.Version, StringComparison.OrdinalIgnoreCase); + } + return base.Equals(obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Name, Version); + } + + public bool Equals(ITemplatePackageInfo? other) + { + if (other == null) + { + return false; + } + + return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && Version.Equals(other.Version, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/DownloadedPackInfo.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/DownloadedPackInfo.cs new file mode 100644 index 000000000000..dbadba2797bf --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/DownloadedPackInfo.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.PackChecking +{ + public class DownloadedPackInfo : IDownloadedPackInfo + { + private readonly ITemplatePackageInfo _info; + + internal DownloadedPackInfo(ITemplatePackageInfo info, string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentException($"'{nameof(filePath)}' cannot be null or whitespace.", nameof(filePath)); + } + + _info = info ?? throw new ArgumentNullException(nameof(info)); + Path = filePath; + } + + public string Name => _info.Name; + + public string? Version => _info.Version; + + public string Path { get; } + + public long TotalDownloads => _info.TotalDownloads; + + public IReadOnlyList Owners => _info.Owners; + + public bool Reserved => _info.Reserved; + + public string? Description => _info.Description; + + public string? IconUrl => _info.IconUrl; + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/FilteredPackageInfo.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/FilteredPackageInfo.cs new file mode 100644 index 000000000000..2fe29018900c --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/FilteredPackageInfo.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.PackChecking +{ + [System.Diagnostics.DebuggerDisplay("{Name}@{Version} - {Reason}")] + internal class FilteredPackageInfo : ITemplatePackageInfo + { + internal FilteredPackageInfo(ITemplatePackageInfo info, string reason) + { + Reason = reason; + Name = info.Name; + Version = info.Version; + Owners = info.Owners; + TotalDownloads = info.TotalDownloads; + Reserved = info.Reserved; + Description = info.Description; + IconUrl = info.IconUrl; + } + + [System.Text.Json.Serialization.JsonConstructor] + private FilteredPackageInfo(string name, string reason) + { + Name = name; + Reason = reason; + } + + [JsonInclude] + public string Name { get; private set; } + + [JsonInclude] + public string? Version { get; private set; } + + [JsonInclude] + public string Reason { get; private set; } + + [JsonInclude] + public long TotalDownloads { get; private set; } + + [JsonInclude] + public IReadOnlyList Owners { get; private set; } = []; + + [JsonInclude] + public bool Reserved { get; private set; } + + [JsonIgnore] + public string? Description { get; } + + [JsonIgnore] + public string? IconUrl { get; } + } +} + diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/IDownloadedPackInfo.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/IDownloadedPackInfo.cs new file mode 100644 index 000000000000..12877c91c0cf --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/IDownloadedPackInfo.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.PackChecking +{ + internal interface IDownloadedPackInfo : ITemplatePackageInfo + { + /// + /// The path on disk for the pack. + /// + string Path { get; } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/IPackCheckerFactory.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/IPackCheckerFactory.cs new file mode 100644 index 000000000000..3405873860c0 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/IPackCheckerFactory.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateSearch.TemplateDiscovery.PackChecking +{ + internal interface IPackCheckerFactory + { + internal Task CreatePackSourceCheckerAsync(CommandArgs config, CancellationToken cancellationToken); + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/IPackProvider.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/IPackProvider.cs new file mode 100644 index 000000000000..381b289c99d2 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/IPackProvider.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.PackChecking +{ + internal interface IPackProvider + { + string Name { get; } + + IAsyncEnumerable GetCandidatePacksAsync(CancellationToken token); + + Task DownloadPackageAsync(ITemplatePackageInfo packinfo, CancellationToken token); + + Task GetPackageCountAsync(CancellationToken token); + + Task DeleteDownloadedPacksAsync(); + + Task<(ITemplatePackageInfo PackageInfo, bool Removed)> GetPackageInfoAsync(string packageIdentifier, CancellationToken cancellationToken); + + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackCheckResult.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackCheckResult.cs new file mode 100644 index 000000000000..b939110c8440 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackCheckResult.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.PackChecking +{ + internal class PackCheckResult + { + internal PackCheckResult(IDownloadedPackInfo packInfo, PreFilterResultList preFilterResults) + { + PackInfo = packInfo; + PreFilterResults = preFilterResults; + FoundTemplates = new List(); + } + + internal PackCheckResult(IDownloadedPackInfo packInfo, IReadOnlyList foundTemplates) + { + PackInfo = packInfo; + PreFilterResults = new PreFilterResultList(); + FoundTemplates = foundTemplates; + } + + internal IDownloadedPackInfo PackInfo { get; } + + internal PreFilterResultList PreFilterResults { get; } + + internal IReadOnlyList FoundTemplates { get; } + + internal bool AnyTemplates => FoundTemplates.Count > 0; + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackPrefilterer.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackPrefilterer.cs new file mode 100644 index 000000000000..7cc34cd2f1e8 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackPrefilterer.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateSearch.TemplateDiscovery.PackChecking +{ + internal class PackPreFilterer + { + private readonly IReadOnlyList> _preFilters; + + internal PackPreFilterer(IReadOnlyList> preFilters) + { + _preFilters = preFilters; + } + + internal PreFilterResultList FilterPack(IDownloadedPackInfo packInfo) + { + List resultList = new List(); + + foreach (Func filter in _preFilters) + { + PreFilterResult result = filter(packInfo); + resultList.Add(result); + } + + return new PreFilterResultList(resultList); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackSourceCheckResult.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackSourceCheckResult.cs new file mode 100644 index 000000000000..ad17afae71e5 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackSourceCheckResult.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateSearch.Common; +using Microsoft.TemplateSearch.TemplateDiscovery.AdditionalData; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.PackChecking +{ + internal class PackSourceCheckResult + { + internal PackSourceCheckResult(TemplateSearchCache generatedSearchCache, IReadOnlyList filteredPackages, IReadOnlyList additionalDataHandlers) + { + SearchCache = generatedSearchCache; + FilteredPackages = filteredPackages; + AdditionalDataProducers = additionalDataHandlers; + } + + internal TemplateSearchCache SearchCache { get; } + + internal IReadOnlyList FilteredPackages { get; private set; } + + internal IReadOnlyList AdditionalDataProducers { get; } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackSourceChecker.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackSourceChecker.cs new file mode 100644 index 000000000000..f1e6e8d5cb43 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PackSourceChecker.cs @@ -0,0 +1,522 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Edge; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.Utils; +using Microsoft.TemplateSearch.Common; +using Microsoft.TemplateSearch.Common.Abstractions; +using Microsoft.TemplateSearch.TemplateDiscovery.AdditionalData; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.PackChecking +{ + internal class PackSourceChecker + { + private const string HostIdentifierBase = "dotnetcli-discovery-"; + private readonly IEnumerable _packProviders; + private readonly PackPreFilterer _packPreFilterer; + private readonly IReadOnlyList _additionalDataProducers; + private readonly bool _saveCandidatePacks; + private readonly IReadOnlyDictionary _existingCache; + private readonly IReadOnlyDictionary _knownFilteredPackages; + private readonly bool _diffEnabled; + + internal PackSourceChecker( + IEnumerable packProviders, + PackPreFilterer packPreFilterer, + IReadOnlyList additionalDataProducers, + bool saveCandidatePacks, + TemplateSearchCache? existingData = null, + IEnumerable? knownFilteredPackages = null) + { + _packProviders = packProviders; + _packPreFilterer = packPreFilterer; + _additionalDataProducers = additionalDataProducers; + _saveCandidatePacks = saveCandidatePacks; + + _existingCache = existingData?.TemplatePackages.ToDictionary(p => p.Name) ?? new Dictionary(); + _knownFilteredPackages = knownFilteredPackages?.ToDictionary(item => item.Name) ?? new Dictionary(); + _diffEnabled = _existingCache.Any(); + } + + internal async Task CheckPackagesAsync(CancellationToken token) + { + Dictionary newCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary filteredPackages = new Dictionary(StringComparer.OrdinalIgnoreCase); + + ScanningStats scanningStats = new ScanningStats(); + + try + { + await ProcessTemplatePackagesUsingSearchAsync(newCache, filteredPackages, scanningStats, token).ConfigureAwait(false); + if (_diffEnabled) + { + await CheckRemovedPackagesAsync(newCache, filteredPackages, scanningStats, token).ConfigureAwait(false); + } + DisplayScanningStats(scanningStats); + return new PackSourceCheckResult(new TemplateSearchCache(newCache.Values.ToList()), filteredPackages.Values.ToList(), _additionalDataProducers); + } + finally + { + if (!_saveCandidatePacks) + { + try + { + Console.WriteLine("Removing downloaded packs"); + foreach (IPackProvider provider in _packProviders) + { + await provider.DeleteDownloadedPacksAsync().ConfigureAwait(false); + } + Console.WriteLine("Downloaded packs were removed"); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to remove downloaded packs, details: {ex}"); + } + } + } + } + + private async Task ProcessTemplatePackagesUsingSearchAsync( + Dictionary newCache, + Dictionary filteredPackages, + ScanningStats scanningStats, + CancellationToken cancellationToken) + { + foreach (IPackProvider packProvider in _packProviders) + { + cancellationToken.ThrowIfCancellationRequested(); + int count = -1; + Console.WriteLine($"Processing pack provider {packProvider.Name}:"); + Console.WriteLine($"{await packProvider.GetPackageCountAsync(cancellationToken).ConfigureAwait(false)} packs discovered, starting processing"); + + await foreach (ITemplatePackageInfo sourceInfo in packProvider.GetCandidatePacksAsync(cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + count = ProcessCount(count); + if (newCache.ContainsKey(sourceInfo.Name) || filteredPackages.ContainsKey(sourceInfo.Name)) + { + Verbose.WriteLine($"Package {sourceInfo.Name}@{sourceInfo.Version} is already processed."); + continue; + } + string? oldTemplateVersion = null; + string? oldNonTemplateVersion = null; + if (_diffEnabled) + { + if (CheckIfVersionIsKnownTemplatePackage(sourceInfo, newCache, scanningStats, out oldTemplateVersion)) + { + continue; + } + if (CheckIfVersionIsKnownNonTemplatePackage(sourceInfo, filteredPackages, scanningStats, out oldNonTemplateVersion)) + { + continue; + } + } + + IDownloadedPackInfo? packInfo = await packProvider.DownloadPackageAsync(sourceInfo, cancellationToken).ConfigureAwait(false); + if (CheckIfPackageIsFiltered(packInfo, filteredPackages, scanningStats, oldTemplateVersion, oldNonTemplateVersion)) + { + continue; + } + await ProcessTemplatePackageAsync(packInfo, newCache, filteredPackages, scanningStats, oldTemplateVersion, oldNonTemplateVersion, cancellationToken).ConfigureAwait(false); + } + ProcessCount(count); + Console.WriteLine($"All packs from pack provider {packProvider.Name} are processed"); + } + Console.WriteLine("All packs are processed"); + } + + private async Task CheckRemovedPackagesAsync( + Dictionary newCache, + Dictionary filteredPackages, + ScanningStats scanningStats, + CancellationToken cancellationToken) + { + IReadOnlyList removedTemplatePacks; + IReadOnlyList removedNonTemplatePacks; + + (removedTemplatePacks, removedNonTemplatePacks) = EvaluateRemovedPackages(newCache, filteredPackages); + if (!removedTemplatePacks.Any() && removedNonTemplatePacks.Any()) + { + return; + } + + var provider = _packProviders.FirstOrDefault(); + if (provider == null) + { + Console.WriteLine($"Providers do not support checking information about package, removed packages won't be verified and added to cache."); + return; + } + + Console.WriteLine("Checking template packages via API: "); + foreach (var package in removedTemplatePacks) + { + cancellationToken.ThrowIfCancellationRequested(); + var packageInfo = await provider.GetPackageInfoAsync(package.Name, cancellationToken).ConfigureAwait(false); + if (packageInfo == default) + { + Console.WriteLine($"Package {package.Name} no longer exists in the provider."); + scanningStats.RemovedTemplatePacks.Add(package); + } + else if (packageInfo.Removed) + { + Console.WriteLine($"Package {package.Name} was unlisted."); + scanningStats.RemovedTemplatePacks.Add(package); + } + else + { + Console.WriteLine($"Package {package.Name} was verified, adding to cache."); + if (CheckIfVersionIsKnownTemplatePackage(package, newCache, scanningStats, out string? oldVersion)) + { + continue; + } + + IDownloadedPackInfo? packInfo = await provider.DownloadPackageAsync(package, cancellationToken).ConfigureAwait(false); + if (CheckIfPackageIsFiltered(packInfo, filteredPackages, scanningStats, oldVersion, null)) + { + continue; + } + await ProcessTemplatePackageAsync(packInfo, newCache, filteredPackages, scanningStats, oldVersion, null, cancellationToken).ConfigureAwait(false); + } + } + + Verbose.WriteLine("Checking non template packages via API: "); + foreach (var package in removedNonTemplatePacks) + { + cancellationToken.ThrowIfCancellationRequested(); + var packageInfo = await provider.GetPackageInfoAsync(package.Name, cancellationToken).ConfigureAwait(false); + if (packageInfo == default) + { + Verbose.WriteLine($"Package {package.Name} cannot be verified."); + //it's ok to continue if the package was known non-package - ignore error + scanningStats.UnavailableNonTemplatePacksCount++; + continue; + } + if (packageInfo.Removed) + { + Verbose.WriteLine($"Package {package.Name} was unlisted."); + scanningStats.RemovedNonTemplatePacksCount++; + } + else + { + Verbose.WriteLine($"Package {package.Name} was verified."); + if (CheckIfVersionIsKnownNonTemplatePackage(package, filteredPackages, scanningStats, out string? oldNonTemplateVersion)) + { + continue; + } + + IDownloadedPackInfo? packInfo = await provider.DownloadPackageAsync(package, cancellationToken).ConfigureAwait(false); + if (packInfo == null) + { + Console.WriteLine($"[Error] Package {package.Name}@{package.Version} is not processed."); + continue; + } + if (CheckIfPackageIsFiltered(packInfo, filteredPackages, scanningStats, null, oldNonTemplateVersion)) + { + continue; + } + await ProcessTemplatePackageAsync(packInfo, newCache, filteredPackages, scanningStats, null, oldNonTemplateVersion, cancellationToken).ConfigureAwait(false); + } + } + + } + + private bool CheckIfVersionIsKnownTemplatePackage( + ITemplatePackageInfo sourceInfo, + Dictionary newCache, + ScanningStats scanningStats, + out string? currentVersion) + { + currentVersion = null; + if (_existingCache.TryGetValue(sourceInfo.Name, out TemplatePackageSearchData? existingInfo)) + { + if (sourceInfo.Version == existingInfo.Version) + { + Verbose.WriteLine($"Package {sourceInfo.Name}@{sourceInfo.Version} has not changed since last scan, updating metadata only."); + newCache[sourceInfo.Name] = new TemplatePackageSearchData(sourceInfo, existingInfo.Templates, sourceInfo.ProduceAdditionalData(_additionalDataProducers)); + + scanningStats.SameTemplatePacksCount++; + return true; + } + else + { + currentVersion = existingInfo.Version; + } + } + return false; + } + + private bool CheckIfVersionIsKnownNonTemplatePackage( + ITemplatePackageInfo sourceInfo, + Dictionary filteredPackages, + ScanningStats scanningStats, + out string? currentVersion) + { + currentVersion = null; + if (_knownFilteredPackages.TryGetValue(sourceInfo.Name, out FilteredPackageInfo? info)) + { + if (sourceInfo.Version == info.Version) + { + Verbose.WriteLine($"Package {sourceInfo.Name}@{sourceInfo.Version} has not changed since last scan, skipping as it was filtered last time."); + filteredPackages[sourceInfo.Name] = info; + scanningStats.SameNonTemplatePacksCount++; + return true; + } + else + { + currentVersion = info.Version; + } + } + return false; + } + + private bool CheckIfPackageIsFiltered( + IDownloadedPackInfo sourceInfo, + Dictionary filteredPackages, + ScanningStats scanningStats, + string? oldTemplatePackageVersion, + string? oldNonTemplatePackageVersion) + { + PreFilterResultList preFilterResult = _packPreFilterer.FilterPack(sourceInfo); + if (preFilterResult.ShouldBeFiltered) + { + ProcessNonTemplatePackage(sourceInfo, preFilterResult.Reason, filteredPackages, scanningStats, oldTemplatePackageVersion, oldNonTemplatePackageVersion); + } + return preFilterResult.ShouldBeFiltered; + } + + private async Task ProcessTemplatePackageAsync( + IDownloadedPackInfo sourceInfo, + Dictionary newCache, + Dictionary filteredPackages, + ScanningStats scanningStats, + string? oldTemplatePackageVersion, + string? oldNonTemplatePackageVersion, + CancellationToken cancellationToken) + { + IEnumerable foundTemplates = await TryGetTemplatesInPackAsync(sourceInfo, _additionalDataProducers, cancellationToken).ConfigureAwait(false); + Verbose.WriteLine($"{sourceInfo.Name}@{sourceInfo.Version} is processed"); + if (foundTemplates.Any()) + { + Verbose.WriteLine("Found templates:"); + foreach (TemplateSearchData template in foundTemplates) + { + string shortNames = string.Join(",", template.ShortNameList); + Verbose.WriteLine($" - {template.Identity} ({shortNames}) by {template.Author}, group: {(string.IsNullOrWhiteSpace(template.GroupIdentity) ? "" : template.GroupIdentity)}, precedence: {template.Precedence}"); + } + newCache[sourceInfo.Name] = new TemplatePackageSearchData(sourceInfo, foundTemplates, sourceInfo.ProduceAdditionalData(_additionalDataProducers)); + if (string.IsNullOrWhiteSpace(oldNonTemplatePackageVersion)) + { + if (string.IsNullOrWhiteSpace(oldTemplatePackageVersion)) + { + scanningStats.NewTemplatePacks.Add(newCache[sourceInfo.Name]); + } + else + { + scanningStats.UpdatedTemplatePacks.Add((newCache[sourceInfo.Name], oldTemplatePackageVersion)); + } + } + else + { + if (string.IsNullOrWhiteSpace(oldTemplatePackageVersion)) + { + scanningStats.BecameTemplatePacks.Add(newCache[sourceInfo.Name]); + } + } + } + else + { + ProcessNonTemplatePackage( + sourceInfo, + "Failed to scan the package for the templates, the package may contain invalid templates.", + filteredPackages, + scanningStats, + oldTemplatePackageVersion, + oldNonTemplatePackageVersion); + } + + } + + private void ProcessNonTemplatePackage( + IDownloadedPackInfo sourceInfo, + string filterReason, + Dictionary filteredPackages, + ScanningStats scanningStats, + string? oldTemplatePackageVersion, + string? oldNonTemplatePackageVersion) + { + Verbose.WriteLine($"{sourceInfo.Name}@{sourceInfo.Version} is skipped, {filterReason}"); + filteredPackages[sourceInfo.Name] = new FilteredPackageInfo(sourceInfo, filterReason); + if (string.IsNullOrWhiteSpace(oldNonTemplatePackageVersion)) + { + if (string.IsNullOrWhiteSpace(oldTemplatePackageVersion)) + { + scanningStats.NewNonTemplatePacksCount++; + } + else + { + scanningStats.BecameNonTemplatePacksCount++; + } + } + else + { + if (string.IsNullOrWhiteSpace(oldTemplatePackageVersion)) + { + scanningStats.UpdatedNonTemplatePacksCount++; + } + } + } + + private async Task> TryGetTemplatesInPackAsync(IDownloadedPackInfo packInfo, IReadOnlyList additionalDataProducers, CancellationToken cancellationToken) + { + ITemplateEngineHost host = TemplateEngineHostHelper.CreateHost(HostIdentifierBase + packInfo.Name); + using EngineEnvironmentSettings environmentSettings = new EngineEnvironmentSettings(host, virtualizeSettings: true); + Scanner scanner = new Scanner(environmentSettings); + try + { + using var scanResult = await scanner.ScanAsync(packInfo.Path, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + if (scanResult.Templates.Any()) + { + foreach (IAdditionalDataProducer dataProducer in additionalDataProducers) + { +#pragma warning disable CS0612 // Type or member is obsolete + dataProducer.CreateDataForTemplatePack(packInfo, scanResult.Templates, environmentSettings); +#pragma warning restore CS0612 // Type or member is obsolete + } + + return scanResult.Templates.Select(t => new TemplateSearchData(t.ToITemplateInfo(), t.ProduceAdditionalData(additionalDataProducers, environmentSettings))); + } + return []; + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception ex) + { + Console.WriteLine("Failed to read package {0}@{1}, details: {2}. The package will be skipped.", packInfo.Name, packInfo.Version, ex); + return []; + } + } + + private (IReadOnlyList, IReadOnlyList) EvaluateRemovedPackages( + Dictionary newCache, + Dictionary filteredPackages) + { + List removedTemplatePacks = new List(); + List removedNonTemplatePacks = new List(); + + foreach (var item in _existingCache) + { + if (!newCache.ContainsKey(item.Key)) + { + removedTemplatePacks.Add(item.Value); + } + } + + foreach (var item in _knownFilteredPackages) + { + if (!filteredPackages.ContainsKey(item.Key)) + { + removedNonTemplatePacks.Add(item.Value); + } + } + if (removedTemplatePacks.Any()) + { + Console.WriteLine($"[Error]: the following {removedTemplatePacks.Count} packages were removed"); + foreach (var package in removedTemplatePacks) + { + Console.WriteLine($" {package.Name}@{package.Version}"); + } + } + if (removedNonTemplatePacks.Any()) + { + Console.WriteLine($"[Error]: the following {removedNonTemplatePacks.Count} non template packages were removed"); + foreach (var package in removedNonTemplatePacks) + { + Console.WriteLine($" {package.Name}@{package.Version}"); + } + } + return (removedTemplatePacks, removedNonTemplatePacks); + } + + private void DisplayScanningStats(ScanningStats scanningStats) + { + Console.WriteLine("Run summary:"); + Console.WriteLine("Template packages:"); + Console.WriteLine($" new: {scanningStats.NewTemplatePacks.Count}"); + foreach (var package in scanningStats.NewTemplatePacks) + { + Console.WriteLine($" {package.Name}@{package.Version}"); + } + + Console.WriteLine($" updated: {scanningStats.UpdatedTemplatePacks.Count}"); + foreach (var (updatedPackage, oldVersion) in scanningStats.UpdatedTemplatePacks) + { + Console.WriteLine($" {updatedPackage.Name}, {oldVersion} --> {updatedPackage.Version}"); + } + + Console.WriteLine($" removed: {scanningStats.RemovedTemplatePacks.Count}"); + foreach (var package in scanningStats.RemovedTemplatePacks) + { + Console.WriteLine($" {package.Name}@{package.Version}"); + } + if (scanningStats.BecameTemplatePacks.Any()) + { + Console.WriteLine($" became template packages: {scanningStats.BecameTemplatePacks.Count}"); + foreach (var package in scanningStats.BecameTemplatePacks) + { + Console.WriteLine($" {package.Name}@{package.Version}"); + } + } + Console.WriteLine($" not changed: {scanningStats.SameTemplatePacksCount}"); + Console.WriteLine($"Non template packages:"); + Console.WriteLine($" new: {scanningStats.NewNonTemplatePacksCount}"); + Console.WriteLine($" updated: {scanningStats.UpdatedNonTemplatePacksCount}"); + Console.WriteLine($" removed: {scanningStats.RemovedNonTemplatePacksCount}"); + Console.WriteLine($" not changed: {scanningStats.SameNonTemplatePacksCount}"); + Console.WriteLine($" unavailable: {scanningStats.UnavailableNonTemplatePacksCount}"); + if (scanningStats.BecameNonTemplatePacksCount > 0) + { + Console.WriteLine($" became non template package: {scanningStats.BecameNonTemplatePacksCount}"); + } + } + + private int ProcessCount(int count) + { + count++; + if ((count % 10) == 0) + { + Console.WriteLine($"{count} packs are processed"); + } + return count; + } + + private class ScanningStats + { + internal List NewTemplatePacks { get; } = new List(); + + internal List BecameTemplatePacks { get; } = new List(); + + internal List RemovedTemplatePacks { get; } = new List(); + + internal List<(TemplatePackageSearchData Package, string OldVersion)> UpdatedTemplatePacks { get; } = new List<(TemplatePackageSearchData, string)>(); + + internal int SameTemplatePacksCount { get; set; } + + internal int BecameNonTemplatePacksCount { get; set; } + + internal int NewNonTemplatePacksCount { get; set; } + + internal int UpdatedNonTemplatePacksCount { get; set; } + + internal int SameNonTemplatePacksCount { get; set; } + + internal int RemovedNonTemplatePacksCount { get; set; } + + internal int UnavailableNonTemplatePacksCount { get; set; } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PreFilterResult.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PreFilterResult.cs new file mode 100644 index 000000000000..7762a59e7c28 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PreFilterResult.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateSearch.TemplateDiscovery.PackChecking +{ + internal class PreFilterResult + { + internal PreFilterResult(string filterId, bool isFiltered, string? reason = null) + { + if (string.IsNullOrWhiteSpace(filterId)) + { + throw new System.ArgumentException($"'{nameof(filterId)}' cannot be null or whitespace.", nameof(filterId)); + } + FilterId = filterId; + IsFiltered = isFiltered; + Reason = reason; + } + + internal string FilterId { get; private set; } + + internal bool IsFiltered { get; private set; } + + internal string? Reason { get; private set; } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PreFilterResultList.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PreFilterResultList.cs new file mode 100644 index 000000000000..a0f00e00b213 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/PackChecking/PreFilterResultList.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateSearch.TemplateDiscovery.PackChecking +{ + internal class PreFilterResultList + { + internal PreFilterResultList() + { + Results = new List(); + } + + internal PreFilterResultList(List results) + { + Results = results; + } + + internal IReadOnlyList Results { get; } + + // return true if any of the filter results have IsFiltered == true + internal bool ShouldBeFiltered => Results.Any(r => r.IsFiltered); + + internal string Reason => string.Join("; ", Results.Where(r => r.IsFiltered && !string.IsNullOrWhiteSpace(r.Reason)).Select(r => r.Reason)); + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Program.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Program.cs new file mode 100644 index 000000000000..50ffae769386 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Program.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; + +namespace Microsoft.TemplateSearch.TemplateDiscovery +{ + internal class Program + { + private static async Task Main(string[] args) + { + Command rootCommand = new TemplateDiscoveryCommand(); + return await rootCommand.Parse(args).InvokeAsync().ConfigureAwait(false); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/LegacyBlobTemplateInfo.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/LegacyBlobTemplateInfo.cs new file mode 100644 index 000000000000..53cccca10e54 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/LegacyBlobTemplateInfo.cs @@ -0,0 +1,220 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.Results +{ + [Obsolete] + internal class LegacyBlobTemplateInfo : ITemplateInfo + { + public LegacyBlobTemplateInfo(ITemplateInfo templateInfo) + { + Author = templateInfo.Author; + Classifications = templateInfo.Classifications; + Description = templateInfo.Description; + Identity = templateInfo.Identity; + GroupIdentity = templateInfo.GroupIdentity; + Precedence = templateInfo.Precedence; + Name = templateInfo.Name; + ShortNameList = templateInfo.ShortNameList; + BaselineInfo = templateInfo.BaselineInfo; + + //new properties - not written to json + ParameterDefinitions = templateInfo.ParameterDefinitions; + TagsCollection = templateInfo.TagsCollection; + PostActions = templateInfo.PostActions; + + //compatibility for old way to manage parameters + if (templateInfo.Tags.Any()) + { + Tags = templateInfo.Tags; + } + else + { + Dictionary tags = new Dictionary(); + foreach (KeyValuePair tag in TagsCollection) + { + Dictionary choices = new Dictionary() { { tag.Value, string.Empty } }; + tags[tag.Key] = new BlobLegacyCacheTag(null, choices, tag.Value, null); + } + foreach (ITemplateParameter choiceParam in ParameterDefinitions.Where(param => param.IsChoice())) + { + Dictionary choices = new Dictionary(); + if (choiceParam.Choices != null) + { + foreach (var choice in choiceParam.Choices) + { + choices.Add(choice.Key, choice.Value.Description ?? string.Empty); + } + } + tags[choiceParam.Name] = new BlobLegacyCacheTag(choiceParam.Description, choices, choiceParam.DefaultValue, choiceParam.DefaultIfOptionWithoutValue); + } + Tags = tags; + } + + if (templateInfo.CacheParameters.Any()) + { + CacheParameters = templateInfo.CacheParameters; + } + else + { + Dictionary cacheParameters = new Dictionary(); + foreach (ITemplateParameter param in ParameterDefinitions.Where(param => !param.IsChoice())) + { + cacheParameters[param.Name] = new BlobLegacyCacheParameter(param.Description, param.DataType, param.DefaultValue, param.DefaultIfOptionWithoutValue); + } + CacheParameters = cacheParameters; + } + } + + public Guid ConfigMountPointId => Guid.Empty; + + public string? Author { get; private set; } + + public IReadOnlyList Classifications { get; private set; } + + public string DefaultName => string.Empty; + + public string? Description { get; private set; } + + public string Identity { get; private set; } + + public Guid GeneratorId => Guid.Empty; + + public string? GroupIdentity { get; private set; } + + public int Precedence { get; private set; } + + public string Name { get; private set; } + + [JsonIgnore] + public bool PreferDefaultName { get; } + + public string ShortName + { + get + { + if (ShortNameList.Count > 0) + { + return ShortNameList[0]; + } + + return string.Empty; + } + + set + { + if (ShortNameList.Count > 0) + { + throw new Exception("Can't set the short name when the ShortNameList already has entries."); + } + + ShortNameList = new List() { value }; + } + } + + public IReadOnlyList ShortNameList { get; private set; } + + public string ConfigPlace => string.Empty; + + public Guid LocaleConfigMountPointId => Guid.Empty; + + public string LocaleConfigPlace => string.Empty; + + public Guid HostConfigMountPointId => Guid.Empty; + + public string HostConfigPlace => string.Empty; + + public string? ThirdPartyNotices { get; private set; } + + public IReadOnlyDictionary BaselineInfo { get; private set; } + + public bool HasScriptRunningPostActions { get; set; } + + public DateTime? ConfigTimestampUtc { get; private set; } + + [JsonIgnore] + public IReadOnlyDictionary TagsCollection { get; } + + [JsonIgnore] + public IParameterDefinitionSet ParameterDefinitions { get; } + + [JsonIgnore] + [Obsolete("Use ParameterDefinitionSet instead.")] + public IReadOnlyList Parameters => ParameterDefinitions; + + [JsonIgnore] + public string MountPointUri => string.Empty; + + public IReadOnlyDictionary Tags { get; private set; } = new Dictionary(); + + public IReadOnlyDictionary CacheParameters { get; private set; } = new Dictionary(); + + [JsonIgnore] + public IReadOnlyList PostActions { get; } + + [JsonIgnore] + IReadOnlyList ITemplateMetadata.Constraints => []; + + // ShortName should get deserialized when it exists, for backwards compat. + // But moving forward, ShortNameList should be the definitive source. + // It can still be ShortName in the template.json, but in the caches it'll be ShortNameList + public bool ShouldSerializeShortName() + { + return false; + } + + private class BlobLegacyCacheTag : ICacheTag + { + public BlobLegacyCacheTag(string? description, IReadOnlyDictionary choicesAndDescriptions, string? defaultValue, string? defaultIfOptionWithoutValue) + { + Description = description; + ChoicesAndDescriptions = choicesAndDescriptions; + DefaultValue = defaultValue; + DefaultIfOptionWithoutValue = defaultIfOptionWithoutValue; + } + + public string? Description { get; } + + public IReadOnlyDictionary ChoicesAndDescriptions { get; } + + public string? DefaultValue { get; } + + public string? DefaultIfOptionWithoutValue { get; } + + [JsonIgnore] + public string? DisplayName => null; + + [JsonIgnore] + public IReadOnlyDictionary Choices => new Dictionary(); + + } + + private class BlobLegacyCacheParameter : ICacheParameter + { + public BlobLegacyCacheParameter(string? description, string? dataType, string? defaultValue, string? defaultIfOptionWithoutValue) + { + Description = description; + DataType = dataType; + DefaultValue = defaultValue; + DefaultIfOptionWithoutValue = defaultIfOptionWithoutValue; + } + + public string? DataType { get; } + + public string? DefaultValue { get; } + + public string? Description { get; } + + public string? DefaultIfOptionWithoutValue { get; } + + [JsonIgnore] + public string DisplayName => string.Empty; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/LegacyMetadataWriter.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/LegacyMetadataWriter.cs new file mode 100644 index 000000000000..9752481561d7 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/LegacyMetadataWriter.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateSearch.Common; +using Microsoft.TemplateSearch.TemplateDiscovery.AdditionalData; +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.Results +{ + [Obsolete] + internal partial class LegacyMetadataWriter + { + internal static string WriteLegacySearchMetadata(PackSourceCheckResult packSourceCheckResults, string outputFileName) + { + var searchMetadata = CreateLegacySearchMetadata(packSourceCheckResults); + File.WriteAllText(outputFileName, searchMetadata.ToJObject().ToJsonString()); + Console.WriteLine($"Legacy search cache file created: {outputFileName}"); + return outputFileName; + } + + private static TemplateDiscoveryMetadata CreateLegacySearchMetadata(PackSourceCheckResult packSourceCheckResults) + { + List templateCache = packSourceCheckResults.SearchCache.TemplatePackages.SelectMany(p => p.Templates) + .Distinct(new TemplateIdentityEqualityComparer()) + .Select(t => (ITemplateInfo)new LegacyBlobTemplateInfo(t)) + .ToList(); + + Dictionary packToTemplateMap = packSourceCheckResults.SearchCache.TemplatePackages + .ToDictionary( + r => r.Name, + r => + { + PackToTemplateEntry packToTemplateEntry = new PackToTemplateEntry( + r.Version ?? string.Empty, + r.Templates.Select(t => new TemplateIdentificationEntry(t.Identity, t.GroupIdentity)).ToList()) + { + TotalDownloads = r.TotalDownloads, + Owners = r.Owners, + Reserved = r.Reserved + }; + return packToTemplateEntry; + }); + + Dictionary additionalData = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (IAdditionalDataProducer dataProducer in packSourceCheckResults.AdditionalDataProducers) + { + if (dataProducer.Data != null) + { + additionalData[dataProducer.DataUniqueName] = dataProducer.Data; + } + } + + return new TemplateDiscoveryMetadata("1.0.0.3", templateCache, packToTemplateMap, additionalData); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/TemplateIdentityEqualityComparer.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/TemplateIdentityEqualityComparer.cs new file mode 100644 index 000000000000..7de3b57347e1 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/TemplateIdentityEqualityComparer.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.Results +{ + internal class TemplateIdentityEqualityComparer : IEqualityComparer + { + public bool Equals(ITemplateInfo? x, ITemplateInfo? y) + { + if (x == null && y == null) + { + return true; + } + if (x == null || y == null) + { + return false; + } + return string.Equals(x.Identity, y.Identity, StringComparison.Ordinal); + } + + public int GetHashCode(ITemplateInfo obj) + { + return obj.Identity.GetHashCode(); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/UnifiedPackCheckResultReportWriter.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/UnifiedPackCheckResultReportWriter.cs new file mode 100644 index 000000000000..d2c967db607e --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Results/UnifiedPackCheckResultReportWriter.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Microsoft.TemplateSearch.Common; +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.Results +{ + internal static class PackCheckResultReportWriter + { + internal const string CacheContentDirectory = "SearchCache"; + // All the metadata needed for searching from dotnet new. + internal const string SearchMetadataFilename = "NuGetTemplateSearchInfo.json"; + internal const string SearchMetadataFilenameVer2 = "NuGetTemplateSearchInfoVer2.json"; + + // Metadata for the scraper to skip packs known to not contain templates. + internal const string NonTemplatePacksFileName = "nonTemplatePacks.json"; + + internal static (string MetadataPath, string LegacyMetadataPath) WriteResults(DirectoryInfo outputBasePath, PackSourceCheckResult packSourceCheckResults) + { + string reportPath = Path.Combine(outputBasePath.FullName, CacheContentDirectory); + + if (!Directory.Exists(reportPath)) + { + Directory.CreateDirectory(reportPath); + Console.WriteLine($"Created directory:{reportPath}"); + } + + string legacyMetadataFilePath = Path.GetFullPath(Path.Combine(reportPath, SearchMetadataFilename)); + string metadataFilePath = Path.GetFullPath(Path.Combine(reportPath, SearchMetadataFilenameVer2)); + + WriteNonTemplatePackList(reportPath, packSourceCheckResults.FilteredPackages); +#pragma warning disable CS0612 // Type or member is obsolete + LegacyMetadataWriter.WriteLegacySearchMetadata(packSourceCheckResults, legacyMetadataFilePath); +#pragma warning restore CS0612 // Type or member is obsolete + WriteSearchMetadata(packSourceCheckResults, metadataFilePath); + return (metadataFilePath, legacyMetadataFilePath); + + } + + private static void WriteSearchMetadata(PackSourceCheckResult packSourceCheckResults, string outputFileName) + { + TemplateSearchCache searchMetadata = packSourceCheckResults.SearchCache; + File.WriteAllText(outputFileName, searchMetadata.ToJObject().ToJsonString()); + Console.WriteLine($"Search cache file created: {outputFileName}"); + } + + private static void WriteNonTemplatePackList(string reportPath, IReadOnlyList packCheckResults) + { + var orderedFilters = packCheckResults.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase).ToArray(); + string serializedContent = JsonSerializer.Serialize(orderedFilters); + string outputFileName = Path.Combine(reportPath, NonTemplatePacksFileName); + File.WriteAllText(outputFileName, serializedContent); + Console.WriteLine($"Non template pack list was created: {outputFileName}"); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TemplateDiscoveryCommand.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TemplateDiscoveryCommand.cs new file mode 100644 index 000000000000..c3741b35f9ad --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TemplateDiscoveryCommand.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using Microsoft.TemplateSearch.TemplateDiscovery.NuGet; +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; +using Microsoft.TemplateSearch.TemplateDiscovery.Results; +using Microsoft.TemplateSearch.TemplateDiscovery.Test; + +namespace Microsoft.TemplateSearch.TemplateDiscovery +{ + internal class TemplateDiscoveryCommand : Command + { + private const int DefaultPageSize = 100; + + private readonly Option _basePathOption = new("--basePath") + { + Arity = ArgumentArity.ExactlyOne, + Description = "The root dir for output for this run.", + Required = true + }; + + private readonly Option _allowPreviewPacksOption = new("--allowPreviewPacks") + { + Description = "Include preview packs in the results (by default, preview packs are ignored and the latest stable pack is used.", + }; + + private readonly Option _pageSizeOption = new("--pageSize") + { + Description = "(debugging) The chunk size for interactions with the source.", + DefaultValueFactory = (r) => DefaultPageSize, + }; + + private readonly Option _onePageOption = new("--onePage") + { + Description = "(debugging) Only process one page of template packs.", + }; + + private readonly Option _savePacksOption = new("--savePacks") + { + Description = "Don't delete downloaded candidate packs (by default, they're deleted at the end of a run).", + }; + + private readonly Option _noTemplateJsonFilterOption = new("--noTemplateJsonFilter") + { + Description = "Don't prefilter packs that don't contain any template.json files (this filter is applied by default).", + }; + + private readonly Option _verboseOption = new("--verbose", "-v") + { + Description = "Verbose output for template processing.", + }; + + private readonly Option _testOption = new("--test", "-t") + { + Description = "Run tests on generated metadata files.", + }; + + private readonly Option _queriesOption = new("--queries") + { + Arity = ArgumentArity.OneOrMore, + Description = $"The list of providers to run. Supported providers: {string.Join(",", Enum.GetValues())}.", + AllowMultipleArgumentsPerToken = true, + }; + + private readonly Option _packagesPathOption = new Option("--packagesPath") + { + Description = "Path to pre-downloaded packages. If specified, the packages won't be downloaded from NuGet.org." + }.AcceptExistingOnly(); + + private readonly Option _diffOption = new("--diff") + { + Description = "The list of packages will be compared with previous run, and if package version is not changed, the package won't be rescanned.", + DefaultValueFactory = (r) => true, + }; + + private readonly Option _diffOverrideCacheOption = new Option("--diff-override-cache") + { + Description = "Location of current search cache (local path only).", + }.AcceptExistingOnly(); + + private readonly Option _diffOverrideNonPackagesOption = new Option("--diff-override-non-packages") + { + Description = "Location of the list of packages known not to be a valid package (local path only).", + }.AcceptExistingOnly(); + + public TemplateDiscoveryCommand() : base("template-discovery", "Generates the template package search cache file based on the packages available on NuGet.org.") + { + _basePathOption.AcceptLegalFilePathsOnly(); + _queriesOption.AcceptOnlyFromAmong(Enum.GetValues().Select(e => e.ToString()).ToArray()); + + Options.Add(_basePathOption); + Options.Add(_allowPreviewPacksOption); + Options.Add(_pageSizeOption); + Options.Add(_onePageOption); + Options.Add(_savePacksOption); + Options.Add(_noTemplateJsonFilterOption); + Options.Add(_testOption); + Options.Add(_queriesOption); + Options.Add(_packagesPathOption); + Options.Add(_verboseOption); + Options.Add(_diffOption); + Options.Add(_diffOverrideCacheOption); + Options.Add(_diffOverrideNonPackagesOption); + + TreatUnmatchedTokensAsErrors = true; + SetAction(async (parseResult, cancellationToken) => + { + var config = new CommandArgs(parseResult.GetValue(_basePathOption) ?? throw new Exception("Output path is not set")) + { + LocalPackagePath = parseResult.GetValue(_packagesPathOption), + PageSize = parseResult.GetValue(_pageSizeOption), + SaveCandidatePacks = parseResult.GetValue(_savePacksOption), + RunOnlyOnePage = parseResult.GetValue(_onePageOption), + IncludePreviewPacks = parseResult.GetValue(_allowPreviewPacksOption), + DontFilterOnTemplateJson = parseResult.GetValue(_noTemplateJsonFilterOption), + Verbose = parseResult.GetValue(_verboseOption), + TestEnabled = parseResult.GetValue(_testOption), + Queries = parseResult.GetValue(_queriesOption) ?? [], + DiffMode = parseResult.GetValue(_diffOption), + DiffOverrideSearchCacheLocation = parseResult.GetValue(_diffOverrideCacheOption), + DiffOverrideKnownPackagesLocation = parseResult.GetValue(_diffOverrideNonPackagesOption), + }; + + await ExecuteAsync(config, cancellationToken).ConfigureAwait(false); + + return 0; + }); + } + + private static async Task ExecuteAsync(CommandArgs config, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + Verbose.IsEnabled = config.Verbose; + IPackCheckerFactory factory = config.LocalPackagePath == null ? new NuGetPackSourceCheckerFactory() : new TestPackCheckerFactory(); + PackSourceChecker packSourceChecker = await factory.CreatePackSourceCheckerAsync(config, cancellationToken).ConfigureAwait(false); + PackSourceCheckResult checkResults = await packSourceChecker.CheckPackagesAsync(cancellationToken).ConfigureAwait(false); + (string metadataPath, string legacyMetadataPath) = PackCheckResultReportWriter.WriteResults(config.OutputPath, checkResults); + if (config.TestEnabled) + { + CacheFileTests.RunTests(metadataPath, legacyMetadataPath); + } + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TemplateEngineHostHelper.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TemplateEngineHostHelper.cs new file mode 100644 index 000000000000..c69b5b08a07d --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TemplateEngineHostHelper.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Edge; + +namespace Microsoft.TemplateSearch.TemplateDiscovery +{ + internal static class TemplateEngineHostHelper + { + private const string DefaultHostVersion = "1.0.0"; + + private static readonly Dictionary DefaultPreferences = new Dictionary + { + { "prefs:language", "C#" } + }; + + internal static DefaultTemplateEngineHost CreateHost(string hostIdentifier, string? hostVersion = null, Dictionary? preferences = null) + { + if (string.IsNullOrEmpty(hostIdentifier)) + { + throw new ArgumentException("hostIdentifier cannot be null"); + } + + if (string.IsNullOrEmpty(hostVersion)) + { + hostVersion = DefaultHostVersion; + } + + preferences ??= DefaultPreferences; + + var builtIns = new List<(Type, IIdentifiedComponent)>(); + builtIns.AddRange(Components.AllComponents); + builtIns.AddRange(TemplateEngine.Orchestrator.RunnableProjects.Components.AllComponents); + + // use "dotnetcli" as a fallback host so the correct host specific files are read. + DefaultTemplateEngineHost host = new DefaultTemplateEngineHost(hostIdentifier, hostVersion, preferences, builtIns, new[] { "dotnetcli" }); + return host; + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Test/CacheFileTests.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Test/CacheFileTests.cs new file mode 100644 index 000000000000..d2e2759385c7 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Test/CacheFileTests.cs @@ -0,0 +1,191 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.TemplateEngine.CommandUtils; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.Test +{ + internal static class CacheFileTests + { + internal static void RunTests(string metadataPath, string legacyMetadataPath) + { + //6.0.100 + string sdkVersion = "6.0.100"; + string workingDirectory = TestUtils.CreateTemporaryFolder(sdkVersion); + UseSdkVersion(workingDirectory, requestedSdkVersion: sdkVersion, resolvedVersionPattern: "6.0.1", rollForward: "latestPatch"); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {legacyMetadataPath}."); + CanSearch(workingDirectory, legacyMetadataPath); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {metadataPath}."); + CanSearch(workingDirectory, metadataPath); + + //6.0.300 + sdkVersion = "6.0.300"; + workingDirectory = TestUtils.CreateTemporaryFolder(sdkVersion); + UseSdkVersion(workingDirectory, requestedSdkVersion: sdkVersion, resolvedVersionPattern: "6.0.3", rollForward: "latestPatch"); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {legacyMetadataPath}."); + CanSearch(workingDirectory, legacyMetadataPath); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {metadataPath}."); + CanSearch(workingDirectory, metadataPath); + + //6.0.400 + sdkVersion = "6.0.400"; + workingDirectory = TestUtils.CreateTemporaryFolder(sdkVersion); + UseSdkVersion(workingDirectory, requestedSdkVersion: sdkVersion, resolvedVersionPattern: "6.0.4", rollForward: "latestPatch"); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {legacyMetadataPath}."); + CanSearch(workingDirectory, legacyMetadataPath); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {metadataPath}."); + CanSearch(workingDirectory, metadataPath); + + //7.0.100 + sdkVersion = "7.0.100"; + workingDirectory = TestUtils.CreateTemporaryFolder(sdkVersion); + UseSdkVersion(workingDirectory, requestedSdkVersion: sdkVersion, resolvedVersionPattern: "7.0.1", rollForward: "latestPatch"); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {legacyMetadataPath}."); + CanSearch(workingDirectory, legacyMetadataPath); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {metadataPath}."); + CanSearch(workingDirectory, metadataPath); + + //7.0.200 + sdkVersion = "7.0.200"; + workingDirectory = TestUtils.CreateTemporaryFolder(sdkVersion); + UseSdkVersion(workingDirectory, requestedSdkVersion: sdkVersion, resolvedVersionPattern: "7.0.2", rollForward: "latestPatch"); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {legacyMetadataPath}."); + CanSearch(workingDirectory, legacyMetadataPath); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {metadataPath}."); + CanSearch(workingDirectory, metadataPath); + + //7.0.300 + sdkVersion = "7.0.300"; + workingDirectory = TestUtils.CreateTemporaryFolder(sdkVersion); + UseSdkVersion(workingDirectory, requestedSdkVersion: sdkVersion, resolvedVersionPattern: "7.0.3", rollForward: "latestPatch"); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {legacyMetadataPath}."); + CanSearch(workingDirectory, legacyMetadataPath); + Console.WriteLine($"Running tests on .NET {sdkVersion} for: {metadataPath}."); + CanSearch(workingDirectory, metadataPath); + + //latest + workingDirectory = TestUtils.CreateTemporaryFolder("latest"); + //print the version + new DotnetCommand(TestOutputLogger.Instance, "--version") + .WithoutTelemetry() + .WithWorkingDirectory(workingDirectory) + .Execute() + .Should() + .ExitWith(0); + Console.WriteLine($"Running tests on latest .NET for: {legacyMetadataPath}."); + CanSearch(workingDirectory, legacyMetadataPath); + Console.WriteLine($"Running tests on latest .NET for: {metadataPath}."); + CanSearch(workingDirectory, metadataPath); + } + + private static void UseSdkVersion(string workingDirectory, string requestedSdkVersion, string resolvedVersionPattern, string rollForward = "latestMinor", bool allowPrerelease = false) + { + CreateGlobalJson(workingDirectory, requestedSdkVersion, rollForward, allowPrerelease); + + new DotnetCommand(TestOutputLogger.Instance, "--version") + .WithoutTelemetry() + .WithWorkingDirectory(workingDirectory) + .Execute() + .Should() + .ExitWith(0) + .And + .NotHaveStdErr() + .And.HaveStdOutContaining(resolvedVersionPattern); + } + + private static void CanSearchWhileInstantiating(string workingDirectory, string cacheFilePath) + { + var settingsPath = TestUtils.CreateTemporaryFolder(); + new DotnetNewCommand(TestOutputLogger.Instance, "func") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithWorkingDirectory(workingDirectory) + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .Execute() + .Should() + .ExitWith(0) + .And.HaveStdOutContaining("Couldn't find an installed template that matches the input, searching online for one that does...") + .And.HaveStdOutContaining("Template name \"Azure Functions\" (func) from author \"Microsoft\" in pack Microsoft.Azure.WebJobs.ProjectTemplates"); + } + + private static void CanCheckUpdates(string workingDirectory, string cacheFilePath) + { + var settingsPath = TestUtils.CreateTemporaryFolder(); + new DotnetNewCommand(TestOutputLogger.Instance, "--install", "Microsoft.Azure.WebJobs.ItemTemplates::2.1.1785") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithWorkingDirectory(workingDirectory) + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr(); + + new DotnetNewCommand(TestOutputLogger.Instance, "--update-check") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithWorkingDirectory(workingDirectory) + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr() + .And.NotHaveStdOutContaining("Exception") + .And.HaveStdOutContaining("Updates are available for the following:") + .And.HaveStdOutContaining("Microsoft.Azure.WebJobs.ItemTemplates::2.1.1785"); + } + + private static void CanUpdate(string workingDirectory, string cacheFilePath) + { + var settingsPath = TestUtils.CreateTemporaryFolder(); + new DotnetNewCommand(TestOutputLogger.Instance, "--install", "Microsoft.Azure.WebJobs.ItemTemplates::2.1.1785") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithWorkingDirectory(workingDirectory) + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr(); + + new DotnetNewCommand(TestOutputLogger.Instance, "--update-apply") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithWorkingDirectory(workingDirectory) + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr() + .And.NotHaveStdOutContaining("Exception") + .And.HaveStdOutMatching("Update succeeded") + .And.HaveStdOutContaining("Microsoft.Azure.WebJobs.ItemTemplates::2.1.1785"); + } + + private static void CanSearch(string workingDirectory, string cacheFilePath) + { + var settingsPath = TestUtils.CreateTemporaryFolder(); + new DotnetNewCommand(TestOutputLogger.Instance, "func", "--search") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithWorkingDirectory(workingDirectory) + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr() + .And.NotHaveStdOutContaining("Exception") + .And.HaveStdOutContaining("Microsoft.Azure.Functions.Worker.ProjectTemplates"); + } + + private static void CreateGlobalJson(string directory, string sdkVersion, string rollForward = "latestMinor", bool allowPrerelease = false) + { + Console.WriteLine($"set {sdkVersion} in global.json under {directory}."); + string prereleaseSection = allowPrerelease ? @", ""allowPrerelease"": ""true""" : string.Empty; + string jsonContent = $@"{{ ""sdk"": {{ ""version"": ""{sdkVersion}"", ""rollForward"": ""{rollForward}"" {prereleaseSection}}} }}"; + File.WriteAllText(Path.Combine(directory, "global.json"), jsonContent); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Test/TestLogger.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Test/TestLogger.cs new file mode 100644 index 000000000000..3b20d743cd08 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/Test/TestLogger.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Abstractions; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.Test +{ + internal class TestOutputLogger : ITestOutputHelper + { + public static readonly TestOutputLogger Instance = new TestOutputLogger(); + + public void WriteLine(string message) => Console.WriteLine(message); + + public void WriteLine(string format, params object[] args) => Console.WriteLine(format, args); + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TestProvider/TestPackCheckerFactory.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TestProvider/TestPackCheckerFactory.cs new file mode 100644 index 000000000000..06b6d6fa5040 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TestProvider/TestPackCheckerFactory.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.TemplateEngine; +using Microsoft.TemplateSearch.Common; +using Microsoft.TemplateSearch.TemplateDiscovery.AdditionalData; +using Microsoft.TemplateSearch.TemplateDiscovery.Filters; +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.Test +{ + internal class TestPackCheckerFactory : IPackCheckerFactory + { + public Task CreatePackSourceCheckerAsync(CommandArgs config, CancellationToken cancellationToken) + { + List providers = new List() { new TestPackProvider(config.LocalPackagePath ?? throw new ArgumentNullException(nameof(config.LocalPackagePath))) }; + + List> preFilterList = new List>(); + + if (!config.DontFilterOnTemplateJson) + { + preFilterList.Add(TemplateJsonExistencePackFilter.SetupPackFilter()); + } + preFilterList.Add(SkipTemplatePacksFilter.SetupPackFilter()); + preFilterList.Add(FilterNonMicrosoftAuthors.SetupPackFilter()); + + PackPreFilterer preFilterer = new PackPreFilterer(preFilterList); + + IReadOnlyList additionalDataProducers = new List() + { + new CliHostDataProducer() + }; + + TemplateSearchCache? existingCache = LoadExistingCache(config); + IEnumerable? knownNonTemplatePackages = LoadKnownPackagesList(config); + return Task.FromResult(new PackSourceChecker(providers, preFilterer, additionalDataProducers, config.SaveCandidatePacks, existingCache, knownNonTemplatePackages)); + } + + private static IEnumerable? LoadKnownPackagesList(CommandArgs config) + { + if (!config.DiffMode || config.DiffOverrideKnownPackagesLocation == null) + { + return null; + } + + FileInfo? fileLocation = config.DiffOverrideKnownPackagesLocation; + Verbose.WriteLine($"Opening {fileLocation.FullName}"); + + using var fileStream = fileLocation.OpenRead(); + return JsonSerializer.Deserialize>(fileStream); + } + + private static TemplateSearchCache? LoadExistingCache(CommandArgs config) + { + if (!config.DiffMode || config.DiffOverrideSearchCacheLocation == null) + { + return null; + } + + FileInfo? cacheFileLocation = config.DiffOverrideSearchCacheLocation; + Verbose.WriteLine($"Opening {cacheFileLocation.FullName}"); + using var fileStream = cacheFileLocation.OpenRead(); + string content = new StreamReader(fileStream, System.Text.Encoding.UTF8, true).ReadToEnd(); + JsonObject cacheObject = JExtensions.ParseJsonObject(content); + return TemplateSearchCache.FromJObject(cacheObject, NullLogger.Instance, new Dictionary>() { { CliHostSearchCacheData.DataName, CliHostSearchCacheData.Reader } }); + } + } +} diff --git a/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TestProvider/TestPackProvider.cs b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TestProvider/TestPackProvider.cs new file mode 100644 index 000000000000..f2fd334ac6b6 --- /dev/null +++ b/src/TemplateEngine/Tools/Microsoft.TemplateSearch.TemplateDiscovery/TestProvider/TestPackProvider.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.TemplateSearch.Common.Abstractions; +using Microsoft.TemplateSearch.TemplateDiscovery.PackChecking; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.Test +{ + internal class TestPackProvider : IPackProvider + { + private readonly DirectoryInfo _folder; + + internal TestPackProvider(DirectoryInfo folder) + { + _folder = folder; + } + + public string Name => "TestFolderProvider"; + + public Task DeleteDownloadedPacksAsync() + { + //do nothing - do not remove test packs + return Task.FromResult(0); + } + + public Task DownloadPackageAsync(ITemplatePackageInfo packinfo, CancellationToken token) + { + return Task.FromResult((IDownloadedPackInfo)packinfo); + } + + public async IAsyncEnumerable GetCandidatePacksAsync([EnumeratorCancellation] CancellationToken token) + { + foreach (FileInfo package in _folder.EnumerateFiles("*.nupkg", SearchOption.AllDirectories)) + { + yield return new TestPackInfo(package.FullName); + } + + await Task.CompletedTask.ConfigureAwait(false); + } + + public Task GetPackageCountAsync(CancellationToken token) + { + return Task.FromResult(_folder.EnumerateFiles("*.nupkg", SearchOption.AllDirectories).Count()); + } + + public Task<(ITemplatePackageInfo PackageInfo, bool Removed)> GetPackageInfoAsync(string packageIdentifier, CancellationToken cancellationToken) + { + foreach (FileInfo package in _folder.EnumerateFiles("*.nupkg", SearchOption.AllDirectories)) + { + if (package.Name.Contains(packageIdentifier, StringComparison.OrdinalIgnoreCase)) + { + return Task.FromResult(((ITemplatePackageInfo)new TestPackInfo(package.FullName), false)); + } + } + return Task.FromResult(((ITemplatePackageInfo)new TestPackInfo(packageIdentifier), true)); + } + + private class TestPackInfo : ITemplatePackageInfo, IDownloadedPackInfo + { + internal TestPackInfo(string path) + { + Path = path; + + string filename = System.IO.Path.GetFileNameWithoutExtension(path); + + //for testing purposes NuGet versioned packages should be named as ##.nupkg + if (filename.Contains("##")) + { + string[] split = filename.Split("##"); + Name = split[0]; + Version = split[1]; + } + else + { + Name = System.IO.Path.GetFileNameWithoutExtension(path); + Version = "1.0"; + } + } + + public string Name { get; } + + public string Version { get; } + + public long TotalDownloads => 0; + + public string Path { get; } + + public IReadOnlyList Owners => new[] { "TestAuthor" }; + + public bool Reserved => false; + + public string Description => "description"; + + public string IconUrl => "https://icon"; + } + } +} diff --git a/src/TemplateEngine/Tools/README.md b/src/TemplateEngine/Tools/README.md new file mode 100644 index 000000000000..15639a83ea47 --- /dev/null +++ b/src/TemplateEngine/Tools/README.md @@ -0,0 +1,27 @@ +# Authoring tools + +This is the home for template engine tools. + +The following tools are distributed publicly as part of [authoring tools](../docs/authoring-tools/Authoring-Tools.md). + +|Package name|Description|Documentation|Available since| +|---|---|---|---| +| [`Microsoft.TemplateEngine.Authoring.Tasks`](https://www.nuget.org/packages/Microsoft.TemplateEngine.Authoring.Tasks) | MSBuild tasks for template authoring. These tasks are supposed to be added on template package build. The following tasks are available:
- `Localize` task - creates the localization files for the templates on the package build.
- `Validate` task ([planned](https://github.com/dotnet/templating/issues/2623)) - validates the templates for errors and warnings. | [Localization](../docs/authoring-tools/Localization.md) | .NET SDK 7.0.200 | +| [`Microsoft.TemplateEngine.Authoring.CLI`](https://www.nuget.org/packages/Microsoft.TemplateEngine.Authoring.CLI) | `dotnet CLI` [tool](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools) with utilities for template authoring. Offers the following commands:
- `localize` - creates or updates the localization files for the templates.
- `verify` - allows to test the templates and compare them with expected output (snapshot).
- `validate` ([planned](https://github.com/dotnet/templating/issues/2623)) - validates the template(s) for errors and warnings |[Localization](../docs/authoring-tools/Localization.md)
[Template testing](../docs/authoring-tools/Templates-Testing-Tooling.md#cli)| .NET SDK 7.0.200 | +| [`Microsoft.TemplateEngine.Authoring.TemplateVerifier`](https://www.nuget.org/packages/Microsoft.TemplateEngine.Authoring.TemplateVerifier) | The class library containing [snapshot testing framework](../docs/authoring-tools/Templates-Testing-Tooling.md) for the templates. Facilitates writing the tests for templates using Xunit test framework.|[Template testing](../docs/authoring-tools/Templates-Testing-Tooling.md#api)| .NET SDK 7.0.200 | + +The following tools are only used internally: + +|Package name|Description|Documentation|Available since| +|---|---|---|---| +| `Microsoft.TemplateEngine.Authoring.TemplateApiVerifier` | The class library containing the basic template engine host that can be used with [snapshot testing framework](../docs/authoring-tools/Templates-Testing-Tooling.md) to test the templates with using `Microsoft.TemplateEngine.Edge` only. |[Test examples](https://github.com/dotnet/templating/blob/main/test/Microsoft.TemplateEngine.IDE.IntegrationTests/SnapshotTests.cs)| .NET SDK 7.0.200 | +| `Microsoft.TemplateEngine.TemplateDiscovery` | The CLI tool to generate the search cache for users of `Microsoft.TemplateSearch.Common` and `dotnet new search` command || .NET SDK 3.1 | + +The unit and integration tests for the tools are in [`test`](https://github.com/dotnet/templating/tree/main/test) folder. + +## Notes + +Prior to .NET SDK 7.0.2xx, `Microsoft.TemplateEngine.Authoring.Tasks` package was distributed as [`Microsoft.TemplateEngine.Tasks`](https://www.nuget.org/packages/Microsoft.TemplateEngine.Tasks). If you are using old package, please consider switching to new package. + +Prior to .NET SDK 7.0.2xx, the `dotnet CLI` tool for localization was distributed as [`Microsoft.TemplateEngine.TemplateLocalizer`](https://www.nuget.org/packages/Microsoft.TemplateEngine.TemplateLocalizer) package. +If you are using `Microsoft.TemplateEngine.TemplateLocalizer` tool, please consider switching to `Microsoft.TemplateEngine.Authoring.CLI` tool. \ No newline at end of file diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/ArgumentEscaper.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/ArgumentEscaper.cs new file mode 100644 index 000000000000..eb0d24b40452 --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/ArgumentEscaper.cs @@ -0,0 +1,199 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal static class ArgumentEscaper + { + /// + /// Undo the processing which took place to create string[] args in Main, + /// so that the next process will receive the same string[] args + /// See here for more info: + /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx . + /// + /// + /// + internal static string EscapeAndConcatenateArgArrayForProcessStart(IEnumerable args) + { + IEnumerable escaped = EscapeArgArray(args); + return string.Join(" ", escaped); + } + + /// + /// Undo the processing which took place to create string[] args in Main, + /// so that the next process will receive the same string[] args + /// See here for more info: + /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx . + /// + /// + /// + internal static string EscapeAndConcatenateArgArrayForCmdProcessStart(IEnumerable args) + { + IEnumerable escaped = EscapeArgArrayForCmd(args); + return string.Join(" ", escaped); + } + + internal static string EscapeSingleArg(string arg) + { + var sb = new StringBuilder(); + + var length = arg.Length; + var needsQuotes = length == 0 || ShouldSurroundWithQuotes(arg); + var isQuoted = needsQuotes || IsSurroundedWithQuotes(arg); + + if (needsQuotes) + { + sb.Append('"'); + } + + for (int i = 0; i < length; ++i) + { + var backslashCount = 0; + + // Consume All Backslashes + while (i < arg.Length && arg[i] == '\\') + { + backslashCount++; + i++; + } + + // Escape any backslashes at the end of the arg + // when the argument is also quoted. + // This ensures the outside quote is interpreted as + // an argument delimiter + if (i == arg.Length && isQuoted) + { + sb.Append('\\', 2 * backslashCount); + } + + // At then end of the arg, which isn't quoted, + // just add the backslashes, no need to escape + else if (i == arg.Length) + { + sb.Append('\\', backslashCount); + } + + // Escape any preceding backslashes and the quote + else if (arg[i] == '"') + { + sb.Append('\\', (2 * backslashCount) + 1); + sb.Append('"'); + } + + // Output any consumed backslashes and the character + else + { + sb.Append('\\', backslashCount); + sb.Append(arg[i]); + } + } + + if (needsQuotes) + { + sb.Append('"'); + } + + return sb.ToString(); + } + + internal static bool ShouldSurroundWithQuotes(string argument) + { + // Only quote if whitespace exists in the string + return ArgumentContainsWhitespace(argument); + } + + internal static bool IsSurroundedWithQuotes(string argument) + { + return argument.StartsWith('"') && + argument.EndsWith('"'); + } + + internal static bool ArgumentContainsWhitespace(string argument) + { + return argument.Contains(' ') || argument.Contains('\t') || argument.Contains('\n'); + } + + /// + /// Prepare as single argument to + /// roundtrip properly through cmd. + /// This prefixes every character with the '^' character to force cmd to + /// interpret the argument string literally. An alternative option would + /// be to do this only for cmd metacharacters. + /// See here for more info: + /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx . + /// + /// + /// + private static string EscapeArgForCmd(string argument) + { + var sb = new StringBuilder(); + + var quoted = ShouldSurroundWithQuotes(argument); + + if (quoted) + { + sb.Append("^\""); + } + + // Prepend every character with ^ + // This is harmless when passing through cmd + // and ensures cmd metacharacters are not interpreted + // as such + foreach (var character in argument) + { + sb.Append('^'); + sb.Append(character); + } + + if (quoted) + { + sb.Append("^\""); + } + + return sb.ToString(); + } + + /// + /// Undo the processing which took place to create string[] args in Main, + /// so that the next process will receive the same string[] args + /// See here for more info: + /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx . + /// + /// + /// + private static IEnumerable EscapeArgArray(IEnumerable args) + { + var escapedArgs = new List(); + + foreach (var arg in args) + { + escapedArgs.Add(EscapeSingleArg(arg)); + } + + return escapedArgs; + } + + /// + /// This prefixes every character with the '^' character to force cmd to + /// interpret the argument string literally. An alternative option would + /// be to do this only for cmd metacharacters. + /// See here for more info: + /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx. + /// + /// + /// + private static IEnumerable EscapeArgArrayForCmd(IEnumerable arguments) + { + var escapedArgs = new List(); + + foreach (var arg in arguments) + { + escapedArgs.Add(EscapeArgForCmd(arg)); + } + + return escapedArgs; + } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/BasicCommand.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/BasicCommand.cs new file mode 100644 index 000000000000..5b0179e8cdd2 --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/BasicCommand.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +#if !XUNIT_V3 +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal class BasicCommand : TestCommand + { + private readonly string _processName; + + internal BasicCommand(ITestOutputHelper log, string processName, params string[] args) : base(log) + { + _processName = processName; + Arguments.AddRange(args.Where(a => !string.IsNullOrWhiteSpace(a))); + } + + internal BasicCommand(ILogger log, string processName, params string[] args) : base(log) + { + _processName = processName; + Arguments.AddRange(args.Where(a => !string.IsNullOrWhiteSpace(a))); + } + + private protected override SdkCommandSpec CreateCommand(IEnumerable args) + { + var sdkCommandSpec = new SdkCommandSpec() + { + FileName = _processName, + Arguments = args.ToList(), + WorkingDirectory = WorkingDirectory + }; + return sdkCommandSpec; + } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/Command.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/Command.cs new file mode 100644 index 000000000000..89b343244688 --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/Command.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal class Command + { + private readonly Process _process; + + private readonly bool _trimTrailingNewlines; + + private StreamForwarder? _stdOut; + private StreamForwarder? _stdErr; + private bool _running; + + public Command(Process process, bool trimtrailingNewlines = false) + { + _trimTrailingNewlines = trimtrailingNewlines; + _process = process ?? throw new ArgumentNullException(nameof(process)); + } + + public string CommandName => _process.StartInfo.FileName; + + public string CommandArgs => _process.StartInfo.Arguments; + + public CommandResult Execute() + { + return Execute(_ => { }); + } + + public CommandResult Execute(Action? processStarted) + { + Console.WriteLine($"Running {_process.StartInfo.FileName} {_process.StartInfo.Arguments}"); + ThrowIfRunning(); + + _running = true; + + _process.EnableRaisingEvents = true; + +#if DEBUG + var sw = Stopwatch.StartNew(); + + Console.WriteLine($"> {FormatProcessInfo(_process.StartInfo)}"); +#endif + using (var reaper = new ProcessReaper(_process)) + { + _process.Start(); + processStarted?.Invoke(_process); + reaper.NotifyProcessStarted(); + + Console.WriteLine($"Process ID: {_process.Id}"); + + var taskOut = _stdOut?.BeginRead(_process.StandardOutput); + var taskErr = _stdErr?.BeginRead(_process.StandardError); + _process.WaitForExit(); + + taskOut?.Wait(); + taskErr?.Wait(); + } + + var exitCode = _process.ExitCode; + +#if DEBUG + var message = string.Format($"< {FormatProcessInfo(_process.StartInfo)} exited with {exitCode} in {sw.ElapsedMilliseconds} ms"); + if (exitCode == 0) + { + Console.WriteLine(message); + } + else + { + Console.WriteLine(message); + } +#endif + + return new CommandResult( + _process.StartInfo, + exitCode, + _stdOut?.CapturedOutput, + _stdErr?.CapturedOutput); + } + + public Command WorkingDirectory(string projectDirectory) + { + _process.StartInfo.WorkingDirectory = projectDirectory; + return this; + } + + public Command EnvironmentVariable(string name, string value) + { + _process.StartInfo.Environment[name] = value; + return this; + } + + public Command CaptureStdOut() + { + ThrowIfRunning(); + EnsureStdOut(); + _stdOut!.Capture(_trimTrailingNewlines); + return this; + } + + public Command CaptureStdErr() + { + ThrowIfRunning(); + EnsureStdErr(); + _stdErr!.Capture(_trimTrailingNewlines); + return this; + } + + public Command ForwardStdOut(TextWriter? to = null) + { + ThrowIfRunning(); + EnsureStdOut(); + + if (to == null) + { + _stdOut!.ForwardTo(writeLine: Console.Out.WriteLine); + } + else + { + _stdOut!.ForwardTo(writeLine: to.WriteLine); + } + + return this; + } + + public Command ForwardStdErr(TextWriter? to = null) + { + ThrowIfRunning(); + + EnsureStdErr(); + + if (to == null) + { + _stdErr!.ForwardTo(writeLine: Console.Error.WriteLine); + } + else + { + _stdErr!.ForwardTo(writeLine: to.WriteLine); + } + + return this; + } + + public Command OnOutputLine(Action handler) + { + ThrowIfRunning(); + EnsureStdOut(); + + _stdOut!.ForwardTo(writeLine: handler); + return this; + } + + public Command OnErrorLine(Action handler) + { + ThrowIfRunning(); + EnsureStdErr(); + + _stdErr!.ForwardTo(writeLine: handler); + return this; + } + + public Command SetCommandArgs(string commandArgs) + { + _process.StartInfo.Arguments = commandArgs; + return this; + } + + private static string FormatProcessInfo(ProcessStartInfo info) + { + if (string.IsNullOrWhiteSpace(info.Arguments)) + { + return info.FileName; + } + + return info.FileName + " " + info.Arguments; + } + + private void EnsureStdOut() + { + _stdOut ??= new StreamForwarder(); + _process.StartInfo.RedirectStandardOutput = true; + } + + private void EnsureStdErr() + { + _stdErr ??= new StreamForwarder(); + _process.StartInfo.RedirectStandardError = true; + } + + private void ThrowIfRunning([CallerMemberName] string? memberName = null) + { + if (_running) + { + throw new InvalidOperationException($"Unable to invoke {memberName} after the command has been run."); + } + } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/CommandResult.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/CommandResult.cs new file mode 100644 index 000000000000..94f26b4dc75d --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/CommandResult.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal readonly struct CommandResult + { + internal static readonly CommandResult Empty; + + internal CommandResult(ProcessStartInfo startInfo, int exitCode, string? stdOut, string? stdErr) + { + StartInfo = startInfo; + ExitCode = exitCode; + StdOut = stdOut; + StdErr = stdErr; + } + + internal ProcessStartInfo StartInfo { get; } + + internal int ExitCode { get; } + + internal string? StdOut { get; } + + internal string? StdErr { get; } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/CommandResultAssertions.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/CommandResultAssertions.cs new file mode 100644 index 000000000000..8ca719cb507b --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/CommandResultAssertions.cs @@ -0,0 +1,175 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal class CommandResultAssertions + { + private readonly CommandResult _commandResult; + + internal CommandResultAssertions(CommandResult commandResult) + { + _commandResult = commandResult; + } + + internal CommandResultAssertions And => this; + + internal CommandResultAssertions ExitWith(int expectedExitCode) + { + Assert.True(expectedExitCode == _commandResult.ExitCode, AppendDiagnosticsTo($"Expected command to exit with {expectedExitCode} but it did not.")); + return this; + } + + internal CommandResultAssertions Pass() + { + Assert.True(_commandResult.ExitCode == 0, AppendDiagnosticsTo("Expected command to pass but it did not.")); + return this; + } + + internal CommandResultAssertions Fail() + { + Assert.False(_commandResult.ExitCode == 0, AppendDiagnosticsTo("Expected command to fail but it passed.")); + return this; + } + + internal CommandResultAssertions HaveStdOut() + { + Assert.False(string.IsNullOrEmpty(_commandResult.StdOut), AppendDiagnosticsTo("Expected command to have standard output but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdOut(string expectedOutput) + { + Assert.NotNull(_commandResult.StdOut); + Assert.True(expectedOutput == _commandResult.StdOut, AppendDiagnosticsTo($"Expected standard output to be '{expectedOutput}' but it was not.")); + return this; + } + + internal CommandResultAssertions HaveStdOutContaining(string pattern) + { + Assert.NotNull(_commandResult.StdOut); + Assert.True(_commandResult.StdOut.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to contain '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdOutContaining(Func predicate, string description = "") + { + Assert.NotNull(_commandResult.StdOut); + Assert.True(predicate(_commandResult.StdOut), $"The command output did not contain expected result: {description} {Environment.NewLine}"); + return this; + } + + internal CommandResultAssertions NotHaveStdOutContaining(string pattern) + { + Assert.NotNull(_commandResult.StdOut); + Assert.True(!_commandResult.StdOut.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to not contain '{pattern}' but it did.")); + return this; + } + + internal CommandResultAssertions HaveStdOutContainingIgnoreSpaces(string pattern) + { + Assert.NotNull(_commandResult.StdOut); + string commandResultNoSpaces = _commandResult.StdOut.Replace(" ", string.Empty); + Assert.True(commandResultNoSpaces.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to contain '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdOutContainingIgnoreCase(string pattern) + { + Assert.NotNull(_commandResult.StdOut); + Assert.True(_commandResult.StdOut.Contains(pattern, StringComparison.OrdinalIgnoreCase), AppendDiagnosticsTo($"Expected standard output to contain '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdOutMatching(string pattern, RegexOptions options = RegexOptions.None) + { + Assert.NotNull(_commandResult.StdOut); + Assert.True(Regex.Match(_commandResult.StdOut, pattern, options).Success, AppendDiagnosticsTo($"Expected standard output to match pattern '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions NotHaveStdOutMatching(string pattern, RegexOptions options = RegexOptions.None) + { + Assert.NotNull(_commandResult.StdOut); + Assert.True(!Regex.Match(_commandResult.StdOut, pattern, options).Success, AppendDiagnosticsTo($"Expected standard output to not match pattern '{pattern}' but it did.")); + return this; + } + + internal CommandResultAssertions HaveStdErr() + { + Assert.NotNull(_commandResult.StdErr); + Assert.False(string.IsNullOrEmpty(_commandResult.StdErr), AppendDiagnosticsTo("Expected command to have standard error but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdErr(string expectedOutput) + { + Assert.NotNull(_commandResult.StdErr); + Assert.True(expectedOutput == _commandResult.StdErr, AppendDiagnosticsTo($"Expected standard error to be '{expectedOutput}' but it was not.")); + return this; + } + + internal CommandResultAssertions HaveStdErrContaining(string pattern) + { + Assert.NotNull(_commandResult.StdErr); + Assert.True(_commandResult.StdErr.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard error to contain '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions NotHaveStdErrContaining(string pattern) + { + Assert.NotNull(_commandResult.StdErr); + Assert.True(!_commandResult.StdErr.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard error to contain '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions HaveStdErrMatching(string pattern, RegexOptions options = RegexOptions.None) + { + Assert.NotNull(_commandResult.StdErr); + Assert.True(Regex.Match(_commandResult.StdErr, pattern, options).Success, AppendDiagnosticsTo($"Expected standard error to match pattern '{pattern}' but it did not.")); + return this; + } + + internal CommandResultAssertions NotHaveStdOut() + { + Assert.NotNull(_commandResult.StdOut); + Assert.True(string.IsNullOrEmpty(_commandResult.StdOut), AppendDiagnosticsTo("Expected command to not have standard output but it did.")); + return this; + } + + internal CommandResultAssertions NotHaveStdErr() + { + Assert.NotNull(_commandResult.StdErr); + Assert.True(string.IsNullOrEmpty(_commandResult.StdErr), AppendDiagnosticsTo("Expected command to not have standard error but it did.")); + return this; + } + + internal CommandResultAssertions HaveSkippedProjectCompilation(string skippedProject, string frameworkFullName) + { + Assert.NotNull(_commandResult.StdOut); + Assert.True(_commandResult.StdOut.Contains($"Project {skippedProject} ({frameworkFullName}) was previously compiled. Skipping compilation.", StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to contain 'Project {skippedProject} ({frameworkFullName}) was previously compiled. Skipping compilation.' but it did not.")); + return this; + } + + internal CommandResultAssertions HaveCompiledProject(string compiledProject, string frameworkFullName) + { + Assert.NotNull(_commandResult.StdOut); + Assert.True(_commandResult.StdOut.Contains($"Project {compiledProject} ({frameworkFullName}) will be compiled", StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to contain 'Project {compiledProject} ({frameworkFullName}) will be compiled' but it did not.")); + return this; + } + + private string AppendDiagnosticsTo(string s) + { + return (s + $"{Environment.NewLine}" + + $"File Name: {_commandResult.StartInfo.FileName}{Environment.NewLine}" + + $"Arguments: {_commandResult.StartInfo.Arguments}{Environment.NewLine}" + + $"Exit Code: {_commandResult.ExitCode}{Environment.NewLine}" + + $"StdOut:{Environment.NewLine}{_commandResult.StdOut}{Environment.NewLine}" + + $"StdErr:{Environment.NewLine}{_commandResult.StdErr}{Environment.NewLine}") + //escape curly braces for String.Format + .Replace("{", "{{").Replace("}", "}}"); + } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/CommandResultExtensions.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/CommandResultExtensions.cs new file mode 100644 index 000000000000..775e0d339e05 --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/CommandResultExtensions.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal static class CommandResultExtensions + { + internal static CommandResultAssertions Should(this CommandResult commandResult) + { + return new CommandResultAssertions(commandResult); + } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/DotnetCommand.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/DotnetCommand.cs new file mode 100644 index 000000000000..1f9984a0fae5 --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/DotnetCommand.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +#if !XUNIT_V3 +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal class DotnetCommand : TestCommand + { + private string _executableFilePath = "dotnet"; + + internal DotnetCommand(ILogger log, string subcommand, params string[] args) : base(log) + { + Arguments.Add(subcommand); + Arguments.AddRange(args); + } + + internal DotnetCommand(ITestOutputHelper log, string subcommand, params string[] args) : base(log) + { + Arguments.Add(subcommand); + Arguments.AddRange(args); + } + + internal DotnetCommand WithoutTelemetry() + { + WithEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT", "true"); + return this; + } + + internal DotnetCommand WithCustomExecutablePath(string? executableFilePath) + { + if (!string.IsNullOrEmpty(executableFilePath)) + { + _executableFilePath = executableFilePath; + } + return this; + } + + private protected override SdkCommandSpec CreateCommand(IEnumerable args) + { + var sdkCommandSpec = new SdkCommandSpec() + { + FileName = _executableFilePath, + Arguments = args.ToList(), + WorkingDirectory = WorkingDirectory + }; + return sdkCommandSpec; + } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/DotnetNewCommand.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/DotnetNewCommand.cs new file mode 100644 index 000000000000..ae71ad46be61 --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/DotnetNewCommand.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +#if !XUNIT_V3 +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal class DotnetNewCommand : DotnetCommand + { + private bool _hiveSet; + + internal DotnetNewCommand(ILogger log, params string[] args) : base(log, "new", args) + { + } + + internal DotnetNewCommand(ITestOutputHelper log, params string[] args) : base(log, "new", args) + { + } + + internal DotnetNewCommand WithVirtualHive() + { + Arguments.Add("--debug:ephemeral-hive"); + _hiveSet = true; + return this; + } + + internal DotnetNewCommand WithCustomHive(string path) + { + Arguments.Add("--debug:custom-hive"); + Arguments.Add(path); + _hiveSet = true; + return this; + } + + internal DotnetNewCommand WithoutCustomHive() + { + _hiveSet = true; + return this; + } + + internal DotnetNewCommand WithoutBuiltInTemplates() + { + Arguments.Add("--debug:disable-sdk-templates"); + return this; + } + + internal DotnetNewCommand WithDebug() + { + WithEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE", "true"); + return this; + } + + private protected override SdkCommandSpec CreateCommand(IEnumerable args) + { + if (!_hiveSet) + { + throw new Exception($"\"--debug:custom-hive\" is not set, call {nameof(WithCustomHive)} to set it or {nameof(WithoutCustomHive)} if it is intentional."); + } + + return base.CreateCommand(args); + } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/NativeMethods.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/NativeMethods.cs new file mode 100644 index 000000000000..83d3d50cbcd4 --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/NativeMethods.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal static class NativeMethods + { + internal static class Windows + { + internal const int ProcessBasicInformation = 0; + + internal enum JobObjectInfoClass : uint + { + JobObjectExtendedLimitInformation = 9, + } + + [Flags] + internal enum JobObjectLimitFlags : uint + { + JobObjectLimitKillOnJobClose = 0x2000, + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern SafeWaitHandle CreateJobObjectW(IntPtr lpJobAttributes, string? lpName); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoClass jobObjectInformationClass, IntPtr lpJobObjectInformation, uint cbJobObjectInformationLength); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + internal static extern IntPtr GetCommandLine(); + + [StructLayout(LayoutKind.Sequential)] + internal struct JobObjectBasicLimitInformation + { + public long PerProcessUserTimeLimit; + public long PerJobUserTimeLimit; + public JobObjectLimitFlags LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public uint ActiveProcessLimit; + public UIntPtr Affinity; + public uint PriorityClass; + public uint SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct IoCounters + { + public ulong ReadOperationCount; + public ulong WriteOperationCount; + public ulong OtherOperationCount; + public ulong ReadTransferCount; + public ulong WriteTransferCount; + public ulong OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct JobObjectExtendedLimitInformation + { + public JobObjectBasicLimitInformation BasicLimitInformation; + public IoCounters IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } + + } + + internal static class Posix + { + internal const int SIGTERM = 15; + + [DllImport("libc", SetLastError = true)] + internal static extern int kill(int pid, int sig); + } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/ProcessReaper.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/ProcessReaper.cs new file mode 100644 index 000000000000..ac2ece665d53 --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/ProcessReaper.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.TemplateEngine.CommandUtils +{ + /// + /// Responsible for reaping a target process if the current process terminates. + /// + /// + /// On Windows, a job object will be used to ensure the termination of the target + /// process (and its tree) even if the current process is rudely terminated. + /// + /// On POSIX systems, the reaper will handle SIGTERM and attempt to forward the + /// signal to the target process only. + /// + /// The reaper also suppresses SIGINT in the current process to allow the target + /// process to handle the signal. + /// + internal class ProcessReaper : IDisposable + { + private readonly Process _process; + private SafeWaitHandle? _job; + private Mutex? _shutdownMutex; + + /// + /// Creates a new process reaper. + /// + /// The target process to reap if the current process terminates. The process should not yet be started. + public ProcessReaper(Process process) + { + _process = process; + + // The tests need the event handlers registered prior to spawning the child to prevent a race + // where the child writes output the test expects before the intermediate dotnet process + // has registered the event handlers to handle the signals the tests will generate. + Console.CancelKeyPress += HandleCancelKeyPress; + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _shutdownMutex = new Mutex(); + AppDomain.CurrentDomain.ProcessExit += HandleProcessExit; + } + } + + /// + /// Call to notify the reaper that the process has started. + /// + public void NotifyProcessStarted() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Limit the use of job objects to versions of Windows that support nested jobs (i.e. Windows 8/2012 or later). + // Ideally, we would check for some new API export or OS feature instead of the OS version, + // but nested jobs are transparently implemented with respect to the Job Objects API. + // Note: Windows 8.1 and later may report as Windows 8 (see https://docs.microsoft.com/en-us/windows/desktop/sysinfo/operating-system-version). + // However, for the purpose of this check that is still sufficient. + if (Environment.OSVersion.Version.Major > 6 || + (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor >= 2)) + { + _job = AssignProcessToJobObject(_process.Handle); + } + } + } + + public void Dispose() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (_job != null) + { + // Clear the kill on close flag because the child process terminated successfully + // If this fails, then we have no choice but to terminate any remaining processes in the job + SetKillOnJobClose(_job.DangerousGetHandle(), false); + + _job.Dispose(); + _job = null; + } + } + else + { + AppDomain.CurrentDomain.ProcessExit -= HandleProcessExit; + + // If there's been a shutdown via the process exit handler, + // this will block the current thread so we don't race with the CLR shutdown + // from the signal handler. + if (_shutdownMutex != null) + { + _shutdownMutex.WaitOne(); + _shutdownMutex.ReleaseMutex(); + _shutdownMutex.Dispose(); + _shutdownMutex = null; + } + } + + Console.CancelKeyPress -= HandleCancelKeyPress; + } + + private static void HandleCancelKeyPress(object? sender, ConsoleCancelEventArgs e) + { + // Ignore SIGINT/SIGQUIT so that the process can handle the signal + e.Cancel = true; + } + + private static SafeWaitHandle? AssignProcessToJobObject(IntPtr process) + { + var job = NativeMethods.Windows.CreateJobObjectW(IntPtr.Zero, null); + if (job == null || job.IsInvalid) + { + return null; + } + + if (!SetKillOnJobClose(job.DangerousGetHandle(), true)) + { + job.Dispose(); + return null; + } + + if (!NativeMethods.Windows.AssignProcessToJobObject(job.DangerousGetHandle(), process)) + { + job.Dispose(); + return null; + } + + return job; + } + + private static bool SetKillOnJobClose(IntPtr job, bool value) + { + var information = new NativeMethods.Windows.JobObjectExtendedLimitInformation + { + BasicLimitInformation = new NativeMethods.Windows.JobObjectBasicLimitInformation + { + LimitFlags = value ? NativeMethods.Windows.JobObjectLimitFlags.JobObjectLimitKillOnJobClose : 0 + } + }; + + var length = Marshal.SizeOf(typeof(NativeMethods.Windows.JobObjectExtendedLimitInformation)); + var informationPtr = Marshal.AllocHGlobal(length); + + try + { + Marshal.StructureToPtr(information, informationPtr, false); + + if (!NativeMethods.Windows.SetInformationJobObject( + job, + NativeMethods.Windows.JobObjectInfoClass.JobObjectExtendedLimitInformation, + informationPtr, + (uint)length)) + { + return false; + } + + return true; + } + finally + { + Marshal.FreeHGlobal(informationPtr); + } + } + + private void HandleProcessExit(object? sender, EventArgs args) + { + int processId; + try + { + processId = _process.Id; + } + catch (InvalidOperationException) + { + // The process hasn't started yet; nothing to signal + return; + } + + // Take ownership of the shutdown mutex; this will ensure that the other + // thread also waiting on the process to exit won't complete CLR shutdown before + // this one does. + _shutdownMutex?.WaitOne(); + + if (!_process.WaitForExit(0) && NativeMethods.Posix.kill(processId, NativeMethods.Posix.SIGTERM) != 0) + { + // Couldn't send the signal, don't wait + return; + } + + // If SIGTERM was ignored by the target, then we'll still wait + _process.WaitForExit(); + + Environment.ExitCode = _process.ExitCode; + } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/SdkCommandSpec.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/SdkCommandSpec.cs new file mode 100644 index 000000000000..91617042a405 --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/SdkCommandSpec.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal class SdkCommandSpec + { + public string? FileName { get; set; } + + public List Arguments { get; set; } = new List(); + + public Dictionary Environment { get; set; } = new Dictionary(); + + public List EnvironmentToRemove { get; } = new List(); + + public string? WorkingDirectory { get; set; } + + public Command ToCommand() + { + var process = new Process() + { + StartInfo = ToProcessStartInfo() + }; + var ret = new Command(process, trimtrailingNewlines: true); + return ret; + } + + public ProcessStartInfo ToProcessStartInfo() + { + var ret = new ProcessStartInfo + { + FileName = FileName, + Arguments = EscapeArgs(), + UseShellExecute = false + }; + foreach (var kvp in Environment) + { + ret.Environment[kvp.Key] = kvp.Value; + } + foreach (var envToRemove in EnvironmentToRemove) + { + ret.Environment.Remove(envToRemove); + } + + if (WorkingDirectory != null) + { + ret.WorkingDirectory = WorkingDirectory; + } + + return ret; + } + + private string EscapeArgs() + { + // Note: this doesn't handle invoking .cmd files via "cmd /c" on Windows, which probably won't be necessary here + // If it is, refer to the code in WindowsExePreferredCommandSpecFactory in Microsoft.DotNet.Cli.Utils + return ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(Arguments); + } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/StreamForwarder.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/StreamForwarder.cs new file mode 100644 index 000000000000..dfe1ff6567fc --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/StreamForwarder.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal sealed class StreamForwarder + { + private const char FlushBuilderCharacter = '\n'; + private const char CarriageReturn = '\r'; + + private StringBuilder? _builder; + private StringWriter? _capture; + private Action? _writeLine; + private bool _trimTrailingCapturedNewline; + + public string? CapturedOutput + { + get + { + string? capture = _capture?.GetStringBuilder()?.ToString(); + if (_trimTrailingCapturedNewline) + { + capture = capture?.TrimEnd('\r', '\n'); + } + return capture; + } + } + + public StreamForwarder Capture(bool trimTrailingNewline = false) + { + ThrowIfCaptureSet(); + + _capture = new StringWriter(); + _trimTrailingCapturedNewline = trimTrailingNewline; + + return this; + } + + public StreamForwarder ForwardTo(Action writeLine) + { + ThrowIfNull(writeLine); + + ThrowIfForwarderSet(); + + _writeLine = writeLine; + + return this; + } + + public Task BeginRead(TextReader reader) => Task.Run(() => Read(reader)); + + public void Read(TextReader reader) + { + int bufferSize = 1; + char currentCharacter; + + char[] buffer = new char[bufferSize]; + _builder = new StringBuilder(); + + // Using Read with buffer size 1 to prevent looping endlessly + // like we would when using Read() with no buffer + while ((_ = reader.Read(buffer, 0, bufferSize)) > 0) + { + currentCharacter = buffer[0]; + + if (currentCharacter == FlushBuilderCharacter) + { + WriteBuilder(); + } + else if (currentCharacter != CarriageReturn) + { + _ = _builder.Append(currentCharacter); + } + } + + // Flush anything else when the stream is closed + // Which should only happen if someone used console.Write + if (_builder.Length > 0) + { + WriteBuilder(); + } + } + + private void WriteBuilder() + { + WriteLine(_builder?.ToString()); + _ = (_builder?.Clear()); + } + + private void WriteLine(string? str) + { + _capture?.WriteLine(str); + + if (_writeLine != null && str != null) + { + _writeLine(str); + } + } + + private void ThrowIfNull(object obj) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + } + + private void ThrowIfForwarderSet() + { + if (_writeLine != null) + { + throw new InvalidOperationException("WriteLine forwarder set previously"); + } + } + + private void ThrowIfCaptureSet() + { + if (_capture != null) + { + throw new InvalidOperationException("Already capturing stream!"); + } + } + } +} diff --git a/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/TestCommand.cs b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/TestCommand.cs new file mode 100644 index 000000000000..14682c46cfcd --- /dev/null +++ b/src/TemplateEngine/Tools/Shared/Microsoft.TemplateEngine.CommandUtils/TestCommand.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Utils; +#if !XUNIT_V3 +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.CommandUtils +{ + internal abstract class TestCommand + { + private readonly LoggerWrapper _loggerWrapper; + + protected TestCommand(ITestOutputHelper log) + { + _loggerWrapper = new LoggerWrapper(log); + } + + protected TestCommand(ILogger log) + { + _loggerWrapper = new LoggerWrapper(log); + } + + internal string? WorkingDirectory { get; set; } + + internal List Arguments { get; set; } = new List(); + + internal List EnvironmentToRemove { get; } = new List(); + + // These only work via Execute(), not when using GetProcessStartInfo() + internal Action? CommandOutputHandler { get; set; } + + internal Action? ProcessStartedHandler { get; set; } + + protected Dictionary Environment { get; set; } = new Dictionary(); + + internal TestCommand WithEnvironmentVariable(string name, string value) + { + Environment[name] = value; + return this; + } + + internal TestCommand WithEnvironmentVariables(IReadOnlyDictionary? variables) + { + if (variables != null) + { + Environment.Merge(variables); + } + return this; + } + + internal TestCommand WithWorkingDirectory(string workingDirectory) + { + WorkingDirectory = workingDirectory; + return this; + } + + internal TestCommand WithNoUpdateCheck() + { + Arguments.Add("--no-update-check"); + return this; + } + + internal ProcessStartInfo GetProcessStartInfo(params string[] args) + { + SdkCommandSpec commandSpec = CreateCommandSpec(args); + + var psi = commandSpec.ToProcessStartInfo(); + + return psi; + } + + internal CommandResult Execute(params string[] args) + { + IEnumerable enumerableArgs = args; + return Execute(enumerableArgs); + } + + internal virtual CommandResult Execute(IEnumerable args) + { + Command command = CreateCommandSpec(args) + .ToCommand() + .CaptureStdOut() + .CaptureStdErr(); + + if (CommandOutputHandler != null) + { + command.OnOutputLine(CommandOutputHandler); + } + + var result = command.Execute(ProcessStartedHandler); + + _loggerWrapper.WriteLine($"> {result.StartInfo.FileName} {result.StartInfo.Arguments}"); + _loggerWrapper.WriteLine(result.StdOut); + + if (!string.IsNullOrEmpty(result.StdErr)) + { + _loggerWrapper.WriteLine(string.Empty); + _loggerWrapper.WriteLine("StdErr:"); + _loggerWrapper.WriteLine(result.StdErr); + } + + if (result.ExitCode != 0) + { + _loggerWrapper.WriteLine($"Exit Code: {result.ExitCode}"); + } + + return result; + } + + private protected abstract SdkCommandSpec CreateCommand(IEnumerable args); + + private SdkCommandSpec CreateCommandSpec(IEnumerable args) + { + var commandSpec = CreateCommand(args); + foreach (var kvp in Environment) + { + commandSpec.Environment[kvp.Key] = kvp.Value; + } + + foreach (var envToRemove in EnvironmentToRemove) + { + commandSpec.EnvironmentToRemove.Add(envToRemove); + } + + if (WorkingDirectory != null) + { + commandSpec.WorkingDirectory = WorkingDirectory; + } + + if (Arguments.Any()) + { + commandSpec.Arguments = Arguments.Concat(commandSpec.Arguments).ToList(); + } + + return commandSpec; + } + + private class LoggerWrapper + { + private readonly ILogger? _logger; + private readonly ITestOutputHelper? _testHelper; + + internal LoggerWrapper(ILogger logger) + { + _logger = logger; + } + + internal LoggerWrapper(ITestOutputHelper logger) + { + _testHelper = logger; + } + + internal void WriteLine(string? message) + { + if (message is null) + { + return; + } + _logger?.Log(LogLevel.Information, message); + _testHelper?.WriteLine(message); + } + } + } +} diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/Microsoft.TemplateEngine.Authoring.Templates.csproj b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/Microsoft.TemplateEngine.Authoring.Templates.csproj new file mode 100644 index 000000000000..0ecf01b3fe44 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/Microsoft.TemplateEngine.Authoring.Templates.csproj @@ -0,0 +1,32 @@ + + + + $(NetCurrent) + False + False + False + $(ArtifactsTmpDir) + False + true + true + true + true + $(NoWarn);2008;NU5105 + true + Microsoft.TemplateEngine.Authoring.Templates + Microsoft + The templates for useful items and projects for template authoring. + en-US + https://github.com/dotnet/templating + Template + True + true + + + + + content + + + + diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/README.md b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/README.md new file mode 100644 index 000000000000..eedbd5366ab1 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/README.md @@ -0,0 +1,10 @@ +## The templates for template authoring + +The package contains the templates useful for the template authoring: +| Template name | Short name | Description| +|---|---|---| +|Template Package|`templatepack`|A project for creating template package containing .NET templates.| +|`template.json` configuration file|`template.json`|A template for template.json configuration file for .NET template.| + +The package is available for download from nuget.org. +Please feel to contribute or provide the feedback in discussions or via opening the issue in dotnet/templating repo. diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/dotnetcli.host.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/dotnetcli.host.json new file mode 100644 index 000000000000..adb957225fac --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/dotnetcli.host.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "CreateTemplateConfigFolder": { + "longName": "create-template-config-folder", + "shortName": "" + }, + "TemplateName": { + "longName": "template-name", + "shortName": "tn" + }, + "TemplateShortName": { + "longName": "short-name", + "shortName": "sn" + }, + "TemplateIdentity": { + "longName": "template-identity", + "shortName": "ti" + } + } +} diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.cs.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.cs.json new file mode 100644 index 000000000000..2f47aa56f96a --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.cs.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "konfigurační soubor template.json", + "description": "Šablona pro konfigurační soubor template.json pro šablonu .NET Úplný popis konfigurace najdete v https://aka.ms/template-json-reference.", + "symbols/CreateTemplateConfigFolder/description": "True, pokud se má soubor template.json vytvořit ve složce .template.config.", + "symbols/CreateTemplateConfigFolder/displayName": "Vytvořit složku .template.config", + "symbols/TemplateName/description": "Název šablony.", + "symbols/TemplateName/displayName": "Název šablony", + "symbols/TemplateShortName/description": "Krátký název šablony.", + "symbols/TemplateShortName/displayName": "Krátký název šablony", + "symbols/TemplateIdentity/description": "Identita šablony (musí být jedinečná)", + "symbols/TemplateIdentity/displayName": "Identita šablony", + "postActions/instructions/description": "Vyžadují se ruční akce", + "postActions/instructions/manualInstructions/default/text": "Otevřete soubor template.json v editoru a dokončete konfiguraci." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.de.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.de.json new file mode 100644 index 000000000000..69daa2cfd2ab --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.de.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "Konfigurationsdatei template.json", + "description": "Eine Vorlage für die template.json-Konfigurationsdatei für die .NET-Vorlage. Eine vollständige Konfigurationsbeschreibung finden Sie unter https://aka.ms/template-json-reference.", + "symbols/CreateTemplateConfigFolder/description": "WAHR, wenn template.json im Ordner .template.config erstellt werden soll.", + "symbols/CreateTemplateConfigFolder/displayName": "Erstellen des Ordners .template.config", + "symbols/TemplateName/description": "Der Vorlagenname.", + "symbols/TemplateName/displayName": "Vorlagenname", + "symbols/TemplateShortName/description": "Der Kurzname der Vorlage.", + "symbols/TemplateShortName/displayName": "Kurzname der Vorlage", + "symbols/TemplateIdentity/description": "Die Vorlagenidentität (muss eindeutig sein).", + "symbols/TemplateIdentity/displayName": "Vorlagenidentität", + "postActions/instructions/description": "Manuelle Aktionen erforderlich", + "postActions/instructions/manualInstructions/default/text": "Öffnen Sie template.json im Editor, und vervollständigen Sie die Konfiguration." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.en.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.en.json new file mode 100644 index 000000000000..27c1db4a3747 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.en.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "template.json configuration file", + "description": "A template for template.json configuration file for .NET template. See https://aka.ms/template-json-reference for complete configuration description.", + "symbols/CreateTemplateConfigFolder/description": "True when template.json should be created in .template.config folder.", + "symbols/CreateTemplateConfigFolder/displayName": "Create .template.config folder", + "symbols/TemplateName/description": "The template name.", + "symbols/TemplateName/displayName": "Template name", + "symbols/TemplateShortName/description": "The template short name.", + "symbols/TemplateShortName/displayName": "Template short name", + "symbols/TemplateIdentity/description": "The template identity (should be unique).", + "symbols/TemplateIdentity/displayName": "Template identity", + "postActions/instructions/description": "Manual actions required", + "postActions/instructions/manualInstructions/default/text": "Open template.json in the editor and complete the configuration." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.es.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.es.json new file mode 100644 index 000000000000..d34d0f5897d1 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.es.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "Archivo de configuración plantilla.json", + "description": "Plantilla para el archivo de configuración template.json para la plantilla de .NET. Consulte https://aka.ms/template-json-reference para obtener una descripción completa de la configuración.", + "symbols/CreateTemplateConfigFolder/description": "Es verdadero cuando template.json debe crearse en la carpeta .template.config.", + "symbols/CreateTemplateConfigFolder/displayName": "Crear carpeta .template.config", + "symbols/TemplateName/description": "Nombre de la plantilla.", + "symbols/TemplateName/displayName": "Nombre de plantilla", + "symbols/TemplateShortName/description": "Nombre corto de la plantilla.", + "symbols/TemplateShortName/displayName": "Nombre corto de la plantilla", + "symbols/TemplateIdentity/description": "Identidad de la plantilla (debe ser única).", + "symbols/TemplateIdentity/displayName": "Identidad de plantilla", + "postActions/instructions/description": "No se requieren acciones manuales", + "postActions/instructions/manualInstructions/default/text": "Abra template.json en el editor y complete la configuración." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.fr.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.fr.json new file mode 100644 index 000000000000..cf642c8f2d13 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.fr.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "Fichier de configuration template.json", + "description": "Un modèle pour le fichier de configuration template.json pour le modèle .NET. Consultez https://aka.ms/template-json-reference pour une description complète de la configuration.", + "symbols/CreateTemplateConfigFolder/description": "La valeur est true quand template.json doit être créé dans le dossier .template.config.", + "symbols/CreateTemplateConfigFolder/displayName": "Créer un dossier .template.config", + "symbols/TemplateName/description": "Nom du modèle.", + "symbols/TemplateName/displayName": "Nom du modèle", + "symbols/TemplateShortName/description": "Nom court du modèle.", + "symbols/TemplateShortName/displayName": "Nom court du modèle", + "symbols/TemplateIdentity/description": "Identité du modèle (doit être unique).", + "symbols/TemplateIdentity/displayName": "Identité du modèle", + "postActions/instructions/description": "Actions annuelles requises", + "postActions/instructions/manualInstructions/default/text": "Ouvrez template.json dans l’éditeur et terminez la configuration." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.it.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.it.json new file mode 100644 index 000000000000..cfb361df3f33 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.it.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "File di configurazione template.json", + "description": "Il modello per il file di configurazione template.json per il modello .NET. Per una descrizione completa della configurazione, vedere https://aka.ms/template-json-reference.", + "symbols/CreateTemplateConfigFolder/description": "È true quando template.json deve essere creato nella cartella .template.config.", + "symbols/CreateTemplateConfigFolder/displayName": "Creare la cartella .template.config", + "symbols/TemplateName/description": "Il nome del modello.", + "symbols/TemplateName/displayName": "Nome modello", + "symbols/TemplateShortName/description": "Il nome breve del modello.", + "symbols/TemplateShortName/displayName": "Nome breve modello", + "symbols/TemplateIdentity/description": "L'identità del modello (deve essere univoca).", + "symbols/TemplateIdentity/displayName": "Identità del modello", + "postActions/instructions/description": "Azioni manuali necessarie", + "postActions/instructions/manualInstructions/default/text": "Aprire template.json nell'editor e completare la configurazione." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.ja.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.ja.json new file mode 100644 index 000000000000..8035cc618a29 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.ja.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "template.json configuration file", + "description": ".NET テンプレート用の template.json 構成ファイルのテンプレート。構成の詳細については、「https://aka.ms/template-json-reference」を参照してください。", + "symbols/CreateTemplateConfigFolder/description": ".template.config フォルダーに template.json を作成する必要がある場合は True。", + "symbols/CreateTemplateConfigFolder/displayName": "Create .template.config folder", + "symbols/TemplateName/description": "テンプレート名。", + "symbols/TemplateName/displayName": "テンプレート名", + "symbols/TemplateShortName/description": "テンプレートの短い名前。", + "symbols/TemplateShortName/displayName": "テンプレートの短い名前", + "symbols/TemplateIdentity/description": "テンプレート ID (一意である必要があります)。", + "symbols/TemplateIdentity/displayName": "テンプレート ID", + "postActions/instructions/description": "手動操作が必要です", + "postActions/instructions/manualInstructions/default/text": "エディターで template.json を開き、構成を完了します。" +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.ko.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.ko.json new file mode 100644 index 000000000000..bacdbff28cac --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.ko.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "template.json 구성 파일", + "description": ".NET 템플릿용 template.json 구성 파일의 템플릿입니다. 전체 구성 설명은 https://aka.ms/template-json-reference를 참조하요.", + "symbols/CreateTemplateConfigFolder/description": ".template.config 폴더에 template.json을 만들어야 하는 경우 True.", + "symbols/CreateTemplateConfigFolder/displayName": ".template.config 폴더 만들기", + "symbols/TemplateName/description": "템플릿 이름입니다.", + "symbols/TemplateName/displayName": "템플릿 이름", + "symbols/TemplateShortName/description": "템플릿 약식 이름입니다.", + "symbols/TemplateShortName/displayName": "템플릿 약식 이름", + "symbols/TemplateIdentity/description": "템플릿 ID입니다(고유해야 함).", + "symbols/TemplateIdentity/displayName": "템플릿 ID", + "postActions/instructions/description": "수동 작업 필요", + "postActions/instructions/manualInstructions/default/text": "편집기에서 template.json을 열고 구성을 완료합니다." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.pl.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.pl.json new file mode 100644 index 000000000000..7c1eff2a4d7a --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.pl.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "plik konfiguracji template.json", + "description": "Szablon pliku konfiguracji template.json dla szablonu platformy .NET. Aby uzyskać pełny opis konfiguracji, zobacz https://aka.ms/template-json-reference.", + "symbols/CreateTemplateConfigFolder/description": "Wartość True podczas tworzenia pliku template.json w folderze .template.config.", + "symbols/CreateTemplateConfigFolder/displayName": "Utwórz folder .template.config", + "symbols/TemplateName/description": "Nazwa szablonu.", + "symbols/TemplateName/displayName": "Nazwa szablonu", + "symbols/TemplateShortName/description": "Krótka nazwa szablonu.", + "symbols/TemplateShortName/displayName": "Krótka nazwa szablonu", + "symbols/TemplateIdentity/description": "Tożsamość szablonu (powinna być unikatowa).", + "symbols/TemplateIdentity/displayName": "Tożsamość szablonu", + "postActions/instructions/description": "Wymagane akcje ręczne", + "postActions/instructions/manualInstructions/default/text": "Otwórz plik template.json w edytorze i ukończ konfigurację." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.pt-BR.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.pt-BR.json new file mode 100644 index 000000000000..4d74d9799e98 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.pt-BR.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "Arquivo de configuração template.json", + "description": "Um modelo para arquivo de configuração template.json para modelo .NET. Veja https://aka.ms/template-json-reference para descrição completa da configuração.", + "symbols/CreateTemplateConfigFolder/description": "Verdadeiro quando template.json dever ser criado na pasta .template.config.", + "symbols/CreateTemplateConfigFolder/displayName": "Criar pasta .template.config", + "symbols/TemplateName/description": "O nome do modelo.", + "symbols/TemplateName/displayName": "Nome do modelo", + "symbols/TemplateShortName/description": "O nome curto do modelo.", + "symbols/TemplateShortName/displayName": "Nome curto do modelo", + "symbols/TemplateIdentity/description": "A identidade do modelo (deve ser exclusiva).", + "symbols/TemplateIdentity/displayName": "Identidade do modelo", + "postActions/instructions/description": "Ações manuais necessárias", + "postActions/instructions/manualInstructions/default/text": "Abra template.json no editor e conclua a configuração." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.ru.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.ru.json new file mode 100644 index 000000000000..7b6163ef136c --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.ru.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "Файл конфигурации template.json", + "description": "Шаблон для файла конфигурации template.json для шаблона .NET. Полное описание конфигурации см. на странице https://aka.ms/template-json-reference.", + "symbols/CreateTemplateConfigFolder/description": "Принимает значение true, если template.json необходимо создать в папке .template.config.", + "symbols/CreateTemplateConfigFolder/displayName": "Создать папку .template.config", + "symbols/TemplateName/description": "Имя шаблона.", + "symbols/TemplateName/displayName": "Имя шаблона", + "symbols/TemplateShortName/description": "Короткое имя шаблона.", + "symbols/TemplateShortName/displayName": "Короткое имя шаблона", + "symbols/TemplateIdentity/description": "Удостоверение шаблона (должно быть уникальным).", + "symbols/TemplateIdentity/displayName": "Удостоверение шаблона", + "postActions/instructions/description": "Необходимо выполнить действия вручную", + "postActions/instructions/manualInstructions/default/text": "Откройте template.json в редакторе и заполните конфигурацию." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.tr.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.tr.json new file mode 100644 index 000000000000..1daff5d65964 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.tr.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "template.json yapılandırma dosyası", + "description": ".NET şablonunun template.json yapılandırma dosyası için şablon. Tam yapılandırma açıklaması için https://aka.ms/template-json-reference sayfasına bakın.", + "symbols/CreateTemplateConfigFolder/description": ".template.config klasöründe template.json dosyası oluşturulması gerekiyorsa “Doğru” değerini alır.", + "symbols/CreateTemplateConfigFolder/displayName": ".template.config klasörü oluşturun", + "symbols/TemplateName/description": "Şablonun adı.", + "symbols/TemplateName/displayName": "Şablon adı", + "symbols/TemplateShortName/description": "Şablonun kısa adı.", + "symbols/TemplateShortName/displayName": "Şablon kısa adı", + "symbols/TemplateIdentity/description": "Şablonun kimliği (benzersiz olmalıdır).", + "symbols/TemplateIdentity/displayName": "Şablon kimliği", + "postActions/instructions/description": "El ile işlem yapılması gerekiyor", + "postActions/instructions/manualInstructions/default/text": "Template.json dosyasını düzenleyicide açın ve yapılandırmayı tamamlayın." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.zh-Hans.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.zh-Hans.json new file mode 100644 index 000000000000..d923cddc2e79 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.zh-Hans.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "template.json configuration file", + "description": ".NET 模板的 template.json 配置文件的模板。有关完整的配置说明,请参阅 https://aka.ms/template-json-reference。", + "symbols/CreateTemplateConfigFolder/description": "如果应在 .template.config 文件夹中创建 template.json,则为 true。", + "symbols/CreateTemplateConfigFolder/displayName": "创建 .template.config 文件夹", + "symbols/TemplateName/description": "模板名称。", + "symbols/TemplateName/displayName": "模板名称", + "symbols/TemplateShortName/description": "模板短名称。", + "symbols/TemplateShortName/displayName": "模板短名称", + "symbols/TemplateIdentity/description": "模板标识(应是唯一的)。", + "symbols/TemplateIdentity/displayName": "模板标识", + "postActions/instructions/description": "需要手动操作", + "postActions/instructions/manualInstructions/default/text": "在编辑器中打开 template.json 并完成配置。" +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.zh-Hant.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.zh-Hant.json new file mode 100644 index 000000000000..730b976788d1 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/localize/templatestrings.zh-Hant.json @@ -0,0 +1,15 @@ +{ + "author": "Microsoft", + "name": "template.json 設定檔", + "description": ".NET 範本的 template.json 設定檔案範本。如需完整的設定描述,請參閱 https://github.com/dotnet/templating/wiki/Reference-for-template.json。", + "symbols/CreateTemplateConfigFolder/description": "當應該在 .template.config 資料夾中建立 template.json 時為 true。", + "symbols/CreateTemplateConfigFolder/displayName": "建立 .template.config 資料夾", + "symbols/TemplateName/description": "範本名稱。", + "symbols/TemplateName/displayName": "範本名稱", + "symbols/TemplateShortName/description": "範本簡短名稱。", + "symbols/TemplateShortName/displayName": "範本簡短名稱", + "symbols/TemplateIdentity/description": "範本身分識別 (必須是唯一的)。", + "symbols/TemplateIdentity/displayName": "範本身分識別", + "postActions/instructions/description": "需要手動動作", + "postActions/instructions/manualInstructions/default/text": "在編輯器中開啟 template.json 並完成設定。" +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/template.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/template.json new file mode 100644 index 000000000000..c26d918de8ea --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/.template.config/template.json @@ -0,0 +1,90 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ + "Template Authoring" + ], + "name": "template.json configuration file", + "generatorVersions": "[1.0.0.0-*)", + "description": "A template for template.json configuration file for .NET template. See https://aka.ms/template-json-reference for complete configuration description.", + "groupIdentity": "Microsoft.TemplateEngine.Authoring.TemplateJson", + "precedence": "0", + "identity": "Microsoft.TemplateEngine.Authoring.TemplateJson.1.0", + "shortName": "template.json", + "tags": { + "language": "JSON", + "type": "item" + }, + "symbols": { + "CreateTemplateConfigFolder": { + "type": "parameter", + "datatype": "bool", + "description": "True when template.json should be created in .template.config folder.", + "defaultValue": "true", + "displayName": "Create .template.config folder" + }, + "TemplateName": { + "type": "parameter", + "datatype": "string", + "description": "The template name.", + "defaultValue": "New Template", + "replaces": "New Template", + "displayName": "Template name" + }, + "TemplateShortName": { + "type": "parameter", + "datatype": "string", + "description": "The template short name.", + "defaultValue": "new-template", + "replaces": "new-template", + "displayName": "Template short name" + }, + "TemplateIdentity": { + "type": "parameter", + "datatype": "string", + "description": "The template identity (should be unique).", + "defaultValue": "New.Template.Identity", + "replaces": "New.Template.Identity", + "displayName": "Template identity" + } + }, + "sources": [ + { + "condition": "CreateTemplateConfigFolder == true", + "target": ".template.config/", + "rename": { + "_template.json": "template.json" + } + }, + { + "condition": "CreateTemplateConfigFolder == false", + "target": "./", + "rename": { + "_template.json": "template.json" + } + } + ], + "primaryOutputs": [ + { + "condition": "CreateTemplateConfigFolder == true", + "path": ".template.config/template.json" + }, + { + "condition": "CreateTemplateConfigFolder != true", + "path": "template.json" + } + ], + "postActions": [ + { + "id": "instructions", + "description": "Manual actions required", + "manualInstructions": [ + { + "text": "Open template.json in the editor and complete the configuration." + } + ], + "actionId": "AC1156F7-BB77-4DB8-B28F-24EEBCCA1E5C", + "continueOnError": true + } + ] +} diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/_template.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/_template.json new file mode 100644 index 000000000000..eb638e573051 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplateJson/_template.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json.schemastore.org/template", + "$comment": "See https://aka.ms/template-json-reference for complete configuration description. Complete TODOs below and remove the $comment properties. It is recommended to use the JSON editor that supports schema hints to get more information about defined JSON properties and their description.", + "author": "TODO: fill author name here.", + "classifications": [ + "TODO: fill classification here. Common classifications are: Library, Test, Web etc. Each classification should be a separate element of the array. For more details, see https://aka.ms/template-json-reference#classifications." + ], + "name": "New Template", + "description": "TODO: fill the template description here.", + "precedence": "0", + "identity": "New.Template.Identity", + "shortName": "new-template", + "tags": { + "$comment": "TODO: fill the language and type below. Common types are: project, item, solution.", + "language": "C#", + "type": "project" + }, + "$comment_sourceName": "TODO: Source name should follow these rules: https://aka.ms/template-json-source-name. Source name may be removed if replacement for name is not required.", + "sourceName": "New.Template.1", + "preferNameDirectory": true, + "symbols": { + "ExampleParameter": { + "$comment": "TODO: The symbols section defines variables and their values. For more details, see https://aka.ms/template-json-reference#symbols.", + "type": "parameter", + "datatype": "string", + "defaultValue": "example", + "replaces": "valueToReplace", + "isEnabled": false + } + }, + "sources": [ + { + "$comment": "TODO: Sources control the paths for source and target content. For more details see https://aka.ms/template-json-reference#symbols#source-definition. If source definition is not required, remove the property.", + "condition": "false", + "source": "./source-files", + "target": "./target" + } + ], + "constraints": { + "SampleConstraint": { + "$comment": "TODO: The template may define the constraints all of which must be met in order for the template to be used. For more details see https://aka.ms/template-json-reference#symbols#template-constraints. If constraints are not required, remove the property.", + "type": "os", + "args": [ "Linux", "OSX", "Windows" ] + } + }, + "primaryOutputs": [ + { + "$comment": "TODO: Primary outputs define the list of template files for further processing, usually post actions. If primary outputs are not required, remove the property.", + "path": "New.Template.1.cs", + "condition": "false" + } + ], + "postActions": [ + { + "$comment": "TODO: Enables actions to be performed after the project is created. For more details see https://aka.ms/template-json-post-actions. If post actions are not required, remove the property.", + "actionId": "84C0DA21-51C8-4541-9940-6CA19AF04EE6", + "condition": "false", + "id": "sample-post-action", + "manualInstructions": [ + { + "text": "Open file in New.Template.1.cs in the editor." + } + ], + "args": { + "files": "0" + } + } + ] +} diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/dotnetcli.host.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/dotnetcli.host.json new file mode 100644 index 000000000000..f42c76ecb7dd --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/dotnetcli.host.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "EnableMSBuildTasks": { + "longName": "enable-msbuild-tasks", + "shortName": "" + } + } +} diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.cs.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.cs.json new file mode 100644 index 000000000000..ad1da2a216eb --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.cs.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Balíček šablony", + "description": "Projekt pro balíček šablony obsahující šablony .NET.", + "postActions/instructions/description": "Vyžadují se ruční akce", + "postActions/instructions/manualInstructions/default/text": "Otevřete soubor *.csproj v editoru a dokončete konfiguraci metadat balíčku. Zkopírujte šablony do složky content. Vyplňte README.md." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.de.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.de.json new file mode 100644 index 000000000000..8b6825d9fefd --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.de.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Vorlagenpaket", + "description": "Ein Projekt für ein Vorlagenpaket, das .NET-Vorlagen enthält.", + "postActions/instructions/description": "Manuelle Aktionen erforderlich", + "postActions/instructions/manualInstructions/default/text": "Öffnen Sie *.csproj im Editor, und vervollständigen Sie die Konfiguration der Paketmetadaten. Kopieren Sie die Vorlagen in den Ordner „Inhalt“. Füllen Sie README.md aus." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.en.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.en.json new file mode 100644 index 000000000000..dfc74ac6792d --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.en.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Template Package", + "description": "A project for a template package containing .NET templates.", + "postActions/instructions/description": "Manual actions required", + "postActions/instructions/manualInstructions/default/text": "Open *.csproj in the editor and complete the package metadata configuration. Copy the templates to 'content' folder. Fill in README.md." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.es.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.es.json new file mode 100644 index 000000000000..98494eb55e17 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.es.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Paquete de plantillas", + "description": "Proyecto para un paquete de plantillas que contiene plantillas .NET.", + "postActions/instructions/description": "No se requieren acciones manuales", + "postActions/instructions/manualInstructions/default/text": "Abra *.csproj en el editor y complete la configuración de metadatos del paquete. Copie las plantillas en la carpeta \"content\". Rellene en README.md." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.fr.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.fr.json new file mode 100644 index 000000000000..f318c85ce9bc --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.fr.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Package de modèles", + "description": "Projet pour un package de modèles contenant des modèles .NET.", + "postActions/instructions/description": "Actions annuelles requises", + "postActions/instructions/manualInstructions/default/text": "Ouvrez *.csproj dans l’éditeur et terminez la configuration des métadonnées du package. Copiez les modèles dans le dossier « content ». Renseignez README.md." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.it.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.it.json new file mode 100644 index 000000000000..6c934281b463 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.it.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Pacchetto modello", + "description": "Progetto per un pacchetto di modelli contenente i modelli .NET.", + "postActions/instructions/description": "Azioni manuali necessarie", + "postActions/instructions/manualInstructions/default/text": "Aprire *.csproj nell'editor e completare la configurazione dei metadati del pacchetto. Copiare i modelli nella cartella 'content'. Compilare README.md." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.ja.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.ja.json new file mode 100644 index 000000000000..2a08c206257b --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.ja.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "テンプレート パッケージ", + "description": ".NET テンプレートを含むテンプレート パッケージのプロジェクト。", + "postActions/instructions/description": "手動操作が必要です", + "postActions/instructions/manualInstructions/default/text": "エディターで *.csproj を開き、パッケージ メタデータの構成を完了します。テンプレートを 'content' フォルダーにコピーします。README.md を入力します。" +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.ko.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.ko.json new file mode 100644 index 000000000000..d76d9b8ad85f --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.ko.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "템플릿 패키지", + "description": ".NET 템플릿을 포함하는 템플릿 패키지에 대한 프로젝트입니다.", + "postActions/instructions/description": "수동 작업 필요", + "postActions/instructions/manualInstructions/default/text": "편집기에서 *.csproj를 열고 패키지 메타데이터 구성을 완료합니다. 템플릿을 'content' 폴더에 복사합니다. README.md를 입력합니다." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.pl.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.pl.json new file mode 100644 index 000000000000..27ed03831f8c --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.pl.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Pakiet szablonów", + "description": "Projekt dla pakietu szablonu zawierającego szablony platformy .NET.", + "postActions/instructions/description": "Wymagane akcje ręczne", + "postActions/instructions/manualInstructions/default/text": "Otwórz plik *.csproj w edytorze i ukończ konfigurację metadanych pakietu. Skopiuj szablony do folderu „content”. Wypełnij plik README.md." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.pt-BR.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.pt-BR.json new file mode 100644 index 000000000000..6ba0b51873d0 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.pt-BR.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Pacote de Modelo", + "description": "Um projeto para um pacote de modelo que contém modelos do .NET.", + "postActions/instructions/description": "Ações manuais necessárias", + "postActions/instructions/manualInstructions/default/text": "Abra *.csproj no editor e conclua a configuração de metadados do pacote. Copie os modelos para a pasta \"conteúdo\". Preencha o README.md." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.ru.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.ru.json new file mode 100644 index 000000000000..0ba759d0bcb4 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.ru.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Пакет шаблонов", + "description": "Проект для пакета шаблонов .NET.", + "postActions/instructions/description": "Необходимо выполнить действия вручную", + "postActions/instructions/manualInstructions/default/text": "Откройте *.csproj в редакторе и заполните конфигурацию метаданных пакета. Скопируйте шаблоны в папку \"content\". Заполните файл README.md." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.tr.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.tr.json new file mode 100644 index 000000000000..1241314331df --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.tr.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Şablon Paketi", + "description": ".NET şablonlarını içeren şablon paketine ilişkin proje.", + "postActions/instructions/description": "El ile işlem yapılması gerekiyor", + "postActions/instructions/manualInstructions/default/text": "*.csproj dosyasını düzenleyicide açın ve paket meta verilerini yapılandırmayı tamamlayın. Şablonları 'content' klasörüne kopyalayın. README.md dosyasını doldurun." +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.zh-Hans.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.zh-Hans.json new file mode 100644 index 000000000000..f567aa7416f1 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.zh-Hans.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "模板包", + "description": "包含 .NET 模板的模板包的项目。", + "postActions/instructions/description": "需要手动操作", + "postActions/instructions/manualInstructions/default/text": "在编辑器中打开 *.csproj 并完成包元数据配置。将模板复制到 \"content\" 文件夹。填写 README.md。" +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.zh-Hant.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.zh-Hant.json new file mode 100644 index 000000000000..49c85ecdbd79 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/localize/templatestrings.zh-Hant.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "範本套件", + "description": "包含 .NET 範本之範本套件的專案。", + "postActions/instructions/description": "需要手動動作", + "postActions/instructions/manualInstructions/default/text": "在編輯器中開啟 *.csproj,並完成套件中繼資料設定。將範本複製到 'content' 資料夾。填寫 README.md。" +} \ No newline at end of file diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/template.json b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/template.json new file mode 100644 index 000000000000..d4c10ba06fba --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/.template.config/template.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ + "Template Authoring" + ], + "name": "Template Package", + "generatorVersions": "[1.0.0.0-*)", + "description": "A project for a template package containing .NET templates.", + "groupIdentity": "Microsoft.TemplateEngine.Authoring.TemplatePackage", + "precedence": "0", + "identity": "Microsoft.TemplateEngine.Authoring.TemplatePackage.1.0", + "shortName": "templatepack", + "tags": { + "type": "project" + }, + "sourceName": "New.Template.Package", + "symbols": { + "EnableMSBuildTasks": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "true" + } + }, + "primaryOutputs": [ + { + "path": "New.Template.Package.csproj" + } + ], + "postActions": [ + { + "id": "instructions", + "description": "Manual actions required", + "manualInstructions": [ + { + "text": "Open *.csproj in the editor and complete the package metadata configuration. Copy the templates to 'content' folder. Fill in README.md." + } + ], + "actionId": "AC1156F7-BB77-4DB8-B28F-24EEBCCA1E5C", + "continueOnError": true + } + ] +} diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/New.Template.Package.csproj b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/New.Template.Package.csproj new file mode 100644 index 000000000000..74e81091e407 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/New.Template.Package.csproj @@ -0,0 +1,44 @@ + + + + + + New.Template.Package + 1.0 + TODO: fill the package name here + TODO: fill the author name or organization here + TODO: fill the package description here + TODO: fill the tags here + TODO: include a link to an associated project, repository, or company website + + + Template + $(BundledNETCoreAppTargetFramework) + true + false + content + $(NoWarn);NU5128 + true + README.md + + + + + false + + + + + + + + + + + + + + + + + diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/README.md b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/README.md new file mode 100644 index 000000000000..93d46c8c99b7 --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/README.md @@ -0,0 +1,2 @@ + diff --git a/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/content/SampleTemplate/placeholder.txt b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/content/SampleTemplate/placeholder.txt new file mode 100644 index 000000000000..6f0c602d38eb --- /dev/null +++ b/template_feed/Microsoft.TemplateEngine.Authoring.Templates/content/TemplatePackage/content/SampleTemplate/placeholder.txt @@ -0,0 +1 @@ +Place your templates to this folder. diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt index c66a73d20e6c..349efc69b180 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt @@ -125,6 +125,20 @@ InsertText: slnf, Documentation: Create a solution filter file that references a parent solution }, + { + Label: template.json, + Kind: Value, + SortText: template.json, + InsertText: template.json, + Documentation: A template for template.json configuration file for .NET template. See https://aka.ms/template-json-reference for complete configuration description. + }, + { + Label: templatepack, + Kind: Value, + SortText: templatepack, + InsertText: templatepack, + Documentation: A project for a template package containing .NET templates. + }, { Label: tool-manifest, Kind: Value, diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt index 6d16601942e3..29395effaeab 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt @@ -125,6 +125,20 @@ InsertText: slnf, Documentation: Create a solution filter file that references a parent solution }, + { + Label: template.json, + Kind: Value, + SortText: template.json, + InsertText: template.json, + Documentation: A template for template.json configuration file for .NET template. See https://aka.ms/template-json-reference for complete configuration description. + }, + { + Label: templatepack, + Kind: Value, + SortText: templatepack, + InsertText: templatepack, + Documentation: A project for a template package containing .NET templates. + }, { Label: tool-manifest, Kind: Value, diff --git a/test/TemplateEngine/Directory.Build.props b/test/TemplateEngine/Directory.Build.props new file mode 100644 index 000000000000..e4fe79a83479 --- /dev/null +++ b/test/TemplateEngine/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + true + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/ExportCommandFailureTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/ExportCommandFailureTests.cs new file mode 100644 index 000000000000..e2a94b9a7637 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/ExportCommandFailureTests.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.TemplateEngine.CommandUtils; +using Xunit.Abstractions; + +namespace Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests +{ + public class ExportCommandFailureTests : IDisposable + { + private readonly ITestOutputHelper _log; + + private readonly string _workingDirectory; + + public ExportCommandFailureTests(ITestOutputHelper log) + { + _log = log; + + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; + + _workingDirectory = Path.Combine(Path.GetTempPath(), "Microsoft.TemplateEngine.TemplateLocalizer.IntegrationTests", Path.GetRandomFileName()); + Directory.CreateDirectory(_workingDirectory); + } + + public void Dispose() + { + Directory.Delete(_workingDirectory, true); + } + + [Fact] + public async Task PostActionsShouldHaveIds() + { + string json = @"{ + ""postActions"": [ + { + }, + ] +}"; + (await CreateTemplateAndExport(json)) + .Execute() + .Should() + .ExitWith(1) + .And.HaveStdOutContaining("Generating localization files for a template.json has failed.") + .And.HaveStdOutContaining("Json element 'postActions/0' must have a member 'id'."); + } + + [Fact] + public async Task PostActionIdsAreUnique() + { + string json = @"{ + ""postActions"": [ + { + ""id"": ""postAction1"" + }, + { + ""id"": ""postAction1"" + } + ] +}"; + (await CreateTemplateAndExport(json)) + .Execute() + .Should() + .ExitWith(1) + .And.HaveStdOutContaining("Generating localization files for a template.json has failed.") + .And.HaveStdOutContaining(@"Each child of '//postActions' should have a unique id. Currently, the id 'postActions/postAction1' is shared by multiple children."); + } + + [Fact] + public async Task SingleManualInstructionDoesntNeedId() + { + string json = @"{ + ""postActions"": [ + { + ""id"": ""postActionId"", + ""manualInstructions"": [ + { + ""text"": ""some text"" + } + ] + } + ] +}"; + (await CreateTemplateAndExport(json)) + .Execute() + .Should() + .ExitWith(0) + .And.HaveStdOutContaining("Localization files were successfully generated"); + } + + [Fact] + public async Task MultipleManualInstructionShouldHaveIds() + { + string json = @"{ + ""postActions"": [ + { + ""id"": ""postActionId"", + ""manualInstructions"": [ + { + ""text"": ""some text"" + }, + { + ""text"": ""some other text"" + } + ] + } + ] +}"; + (await CreateTemplateAndExport(json)) + .Execute() + .Should() + .ExitWith(1) + .And.HaveStdOutContaining("Generating localization files for a template.json has failed.") + .And.HaveStdOutContaining("Json element 'manualInstructions/0' must have a member 'id'."); + } + + [Fact] + public async Task ManualInstructionIdsAreUnique() + { + string json = @"{ + ""postActions"": [ + { + ""id"": ""postActionId"", + ""manualInstructions"": [ + { + ""id"": ""mi"" + }, + { + ""id"": ""mi"" + }, + ] + } + ] +}"; + (await CreateTemplateAndExport(json)) + .Execute() + .Should() + .ExitWith(1) + .And.HaveStdOutContaining("Generating localization files for a template.json has failed.") + .And.HaveStdOutContaining("Each child of '//postActions/0/manualInstructions' should have a unique id. Currently, the id 'manualInstructions/mi' is shared by multiple children."); + } + + private async Task CreateTemplateAndExport(string templateJsonContent) + { + string filePath = Path.Combine(_workingDirectory, Path.GetRandomFileName() + ".json"); + await File.WriteAllTextAsync(filePath, templateJsonContent); + return new BasicCommand(_log, "dotnet", Path.GetFullPath("Microsoft.TemplateEngine.Authoring.CLI.dll"), "localize", "export", filePath); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/ExportCommandTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/ExportCommandTests.cs new file mode 100644 index 000000000000..4e945be749cf --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/ExportCommandTests.cs @@ -0,0 +1,265 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; + +namespace Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests +{ + public class ExportCommandTests : TestBase, IDisposable + { + private static readonly JsonDocumentOptions DocOptions = new() { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }; + private readonly string _workingDirectory; + + public ExportCommandTests() + { + _workingDirectory = Path.Combine(Path.GetTempPath(), "Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests", Path.GetRandomFileName()); + Directory.CreateDirectory(_workingDirectory); + } + + public void Dispose() + { + Directory.Delete(_workingDirectory, true); + } + + [Fact] + public async Task LocFilesAreExported() + { + string[] exportedFiles = await RunTemplateLocalizer( + GetTestTemplateJsonContent(), + _workingDirectory, + args: new string[] { "localize", "export", _workingDirectory }); + + Assert.True(exportedFiles.Length > 0); + Assert.All(exportedFiles, p => + { + Assert.StartsWith("templatestrings.", Path.GetFileName(p)); + Assert.EndsWith(".json", p); + }); + } + + [Fact] + public async Task LocFilesAreExportedFirstTime() + { + string testTemplate = GetTestTemplateInTempDir("TemplateWithSourceName"); + int runResult = await Program.Main(new[] { "localize", "export", testTemplate }); + Assert.Equal(0, runResult); + string[] exportedFiles; + string expectedExportDirectory = Path.Combine(testTemplate, ".template.config", "localize"); + try + { + exportedFiles = Directory.GetFiles(expectedExportDirectory); + } + catch (DirectoryNotFoundException) + { + // Since no templates were created, it is normal that no directory was created. + exportedFiles = []; + } + Assert.True(exportedFiles.Length > 0); + Assert.All(exportedFiles, p => + { + Assert.StartsWith("templatestrings.", Path.GetFileName(p)); + Assert.EndsWith(".json", p); + }); + } + + [Fact] + public async Task EnglishLocFilesAreOverwritten() + { + string testTemplate = GetTestTemplateInTempDir("TemplateWithSourceName"); + int runResult = await Program.Main(new[] { "localize", "export", testTemplate }); + Assert.Equal(0, runResult); + string expectedExportDirectory = Path.Combine(testTemplate, ".template.config", "localize"); + string enLocFile = Path.Combine(expectedExportDirectory, "templatestrings.en.json"); + string deLocFile = Path.Combine(expectedExportDirectory, "templatestrings.de.json"); + Assert.True(File.Exists(enLocFile)); + Assert.True(File.Exists(deLocFile)); + var engJsonContent = JsonNode.Parse(File.ReadAllText(enLocFile), documentOptions: DocOptions)!.AsObject(); + var deJsonContent = JsonNode.Parse(File.ReadAllText(deLocFile), documentOptions: DocOptions)!.AsObject(); + Assert.Equal("Test Asset", engJsonContent["author"]?.ToString()); + Assert.Equal("Test Asset", deJsonContent["author"]?.ToString()); + + //modify author property + string baseConfig = Path.Combine(testTemplate, ".template.config", "template.json"); + var templateJsonContent = JsonNode.Parse(File.ReadAllText(baseConfig), documentOptions: DocOptions)!.AsObject(); + Assert.NotNull(templateJsonContent["author"]); + templateJsonContent["author"] = "New Author"; + File.WriteAllText(baseConfig, templateJsonContent.ToJsonString()); + + runResult = await Program.Main(new[] { "localize", "export", testTemplate }); + Assert.Equal(0, runResult); + Assert.True(File.Exists(enLocFile)); + Assert.True(File.Exists(deLocFile)); + engJsonContent = JsonNode.Parse(File.ReadAllText(enLocFile), documentOptions: DocOptions)!.AsObject(); + deJsonContent = JsonNode.Parse(File.ReadAllText(deLocFile), documentOptions: DocOptions)!.AsObject(); + Assert.Equal("New Author", engJsonContent["author"]?.ToString()); + Assert.Equal("Test Asset", deJsonContent["author"]?.ToString()); + } + + [Fact] + public async Task TemplateLanguageLocFilesAreOverwritten() + { + string testTemplate = GetTestTemplateInTempDir("TemplateWithSourceName"); + string baseConfig = Path.Combine(testTemplate, ".template.config", "template.json"); + var templateJsonContent = JsonNode.Parse(File.ReadAllText(baseConfig), documentOptions: DocOptions)!.AsObject(); + templateJsonContent.Insert(0, "authoringLanguage", "de"); + File.WriteAllText(baseConfig, templateJsonContent.ToJsonString()); + + int runResult = await Program.Main(new[] { "localize", "export", testTemplate }); + Assert.Equal(0, runResult); + string expectedExportDirectory = Path.Combine(testTemplate, ".template.config", "localize"); + string enLocFile = Path.Combine(expectedExportDirectory, "templatestrings.en.json"); + string deLocFile = Path.Combine(expectedExportDirectory, "templatestrings.de.json"); + Assert.True(File.Exists(enLocFile)); + Assert.True(File.Exists(deLocFile)); + var engJsonContent = JsonNode.Parse(File.ReadAllText(enLocFile), documentOptions: DocOptions)!.AsObject(); + var deJsonContent = JsonNode.Parse(File.ReadAllText(deLocFile), documentOptions: DocOptions)!.AsObject(); + Assert.Equal("Test Asset", engJsonContent["author"]?.ToString()); + Assert.Equal("Test Asset", deJsonContent["author"]?.ToString()); + + //modify author property + templateJsonContent = JsonNode.Parse(File.ReadAllText(baseConfig), documentOptions: DocOptions)!.AsObject(); + Assert.NotNull(templateJsonContent["author"]); + templateJsonContent["author"] = "New Author"; + File.WriteAllText(baseConfig, templateJsonContent.ToJsonString()); + + runResult = await Program.Main(new[] { "localize", "export", testTemplate }); + Assert.Equal(0, runResult); + Assert.True(File.Exists(enLocFile)); + Assert.True(File.Exists(deLocFile)); + engJsonContent = JsonNode.Parse(File.ReadAllText(enLocFile), documentOptions: DocOptions)!.AsObject(); + deJsonContent = JsonNode.Parse(File.ReadAllText(deLocFile), documentOptions: DocOptions)!.AsObject(); + Assert.Equal("New Author", deJsonContent["author"]?.ToString()); + Assert.Equal("Test Asset", engJsonContent["author"]?.ToString()); + } + + [Fact] + public async Task LocFilesAreNotExportedWithDryRun() + { + string[] exportedFiles = await RunTemplateLocalizer( + GetTestTemplateJsonContent(), + _workingDirectory, + args: new string[] { "localize", "export", _workingDirectory, "--dry-run" }); + + Assert.Empty(exportedFiles); + } + + [Fact] + public async Task LanguagesCanBeOverriden() + { + string[] exportedFiles = await RunTemplateLocalizer( + GetTestTemplateJsonContent(), + _workingDirectory, + args: new string[] { "localize", "export", _workingDirectory, "--language", "tr", "de" }); + + Assert.Equal(2, exportedFiles.Length); + Assert.True(File.Exists(Path.Combine(_workingDirectory, "localize", "templatestrings.tr.json"))); + Assert.True(File.Exists(Path.Combine(_workingDirectory, "localize", "templatestrings.de.json"))); + Assert.False(File.Exists(Path.Combine(_workingDirectory, "localize", "templatestrings.es.json"))); + } + + [Fact] + public async Task SubdirectoriesAreNotSearchedByDefault() + { + Directory.CreateDirectory(Path.Combine(_workingDirectory, "subdir")); + Directory.CreateDirectory(Path.Combine(_workingDirectory, "subdir2")); + + string templateJson = GetTestTemplateJsonContent(); + await File.WriteAllTextAsync(Path.Combine(_workingDirectory, "subdir", "template.json"), templateJson); + await File.WriteAllTextAsync(Path.Combine(_workingDirectory, "subdir2", "template.json"), templateJson); + + int runResult = await Program.Main(new string[] { "localize", "export", _workingDirectory, "--language", "es" }); + // Error: no templates found under the given folder. + Assert.NotEqual(0, runResult); + + Assert.False(File.Exists(Path.Combine(_workingDirectory, "subdir", "localize", "es.templatestrings.json"))); + Assert.False(File.Exists(Path.Combine(_workingDirectory, "subdir2", "localize", "es.templatestrings.json"))); + } + + [Fact] + public async Task SubdirectoriesCanBeSearched() + { + Directory.CreateDirectory(Path.Combine(_workingDirectory, "subdir", ".template.config")); + Directory.CreateDirectory(Path.Combine(_workingDirectory, ".template.config")); + + string templateJson = GetTestTemplateJsonContent(); + await File.WriteAllTextAsync(Path.Combine(_workingDirectory, "subdir", ".template.config", "template.json"), templateJson); + await File.WriteAllTextAsync(Path.Combine(_workingDirectory, ".template.config", "template.json"), templateJson); + + int runResult = await Program.Main(new string[] { "localize", "export", _workingDirectory, "--language", "es", "--recursive" }); + Assert.Equal(0, runResult); + + Assert.True(File.Exists(Path.Combine(_workingDirectory, "subdir", ".template.config", "localize", "templatestrings.es.json"))); + Assert.True(File.Exists(Path.Combine(_workingDirectory, ".template.config", "localize", "templatestrings.es.json"))); + } + + [Fact] + public async Task SubdirectoriesWithoutTemplateConfigFileAreNotSearched() + { + Directory.CreateDirectory(Path.Combine(_workingDirectory, "subdir")); + + await File.WriteAllTextAsync(Path.Combine(_workingDirectory, "subdir", "template.json"), GetTestTemplateJsonContent()); + + int runResult = await Program.Main(new string[] { "localize", "export", _workingDirectory, "--language", "es", "--recursive" }); + // Error: no templates found under the given folder. + Assert.NotEqual(0, runResult); + + Assert.False(File.Exists(Path.Combine(_workingDirectory, "subdir", "localize", "es.templatestrings.json"))); + } + + /// + /// Creates a template.json file with given content in the given directory. + /// Runs the template localizer tool with given arguments. + /// Returns all the files found under "localize" folder. + /// + private static async Task RunTemplateLocalizer(string jsonContent, string directory, params string[] args) + { + await File.WriteAllTextAsync(Path.Combine(directory, "template.json"), jsonContent); + + int runResult = await Program.Main(args); + Assert.Equal(0, runResult); + + string expectedExportDirectory = Path.Combine(directory, "localize"); + try + { + return Directory.GetFiles(expectedExportDirectory); + } + catch (DirectoryNotFoundException) + { + // Since no templates were created, it is normal that no directory was created. + return []; + } + } + + private static string GetTestTemplateInTempDir(string templateName) + { + string templateLocation = GetTestTemplateLocation(templateName); + string tmpLocation = TestUtils.CreateTemporaryFolder(); + TestUtils.DirectoryCopy(templateLocation, tmpLocation, true); + return tmpLocation; + } + + private static string GetTestTemplateJsonContent() + { + string? thisDir = Path.GetDirectoryName(typeof(ExportCommandTests).Assembly.Location); + Assert.NotNull(thisDir); + string templateJsonPath = Path.GetFullPath(Path.Combine( + thisDir!, + "..", + "..", + "..", + "..", + "..", + "test", + "Microsoft.TemplateEngine.TestTemplates", + "test_templates", + "TemplateWithLocalization", + ".template.config", + "template.json")); + + return File.ReadAllText(templateJsonPath); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests.csproj new file mode 100644 index 000000000000..257fabe0ffd2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests.csproj @@ -0,0 +1,23 @@ + + + + $(NetCurrent) + + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Snapshots/ValidateCommandTests.ValidateCommand_BasicTest.Linux.verified.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Snapshots/ValidateCommandTests.ValidateCommand_BasicTest.Linux.verified.txt new file mode 100644 index 000000000000..f31885155d40 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Snapshots/ValidateCommandTests.ValidateCommand_BasicTest.Linux.verified.txt @@ -0,0 +1,57 @@ +StdErr: + +StdOut: +info: validate[0] + Scanning location '%TEMPLATE_LOCATION%' for the templates.. +fail: Microsoft.TemplateEngine.Orchestrator.RunnableProjects.RunnableProjectGenerator[0] + Failed to load template from %TEMPLATE_LOCATION%/MissingIdentity/.template.config/template.json. + Details: 'identity' is missing or is an empty string. +warn: Template Engine: %scrubbed% +info: validate[0] + Scanning completed +info: validate[0] + Location '%TEMPLATE_LOCATION%': found 6 templates. +info: validate[0] + Found template 'Invalid.SameShortName.TemplateA' (Invalid.SameShortName.TemplateA): + + Template 'Invalid.SameShortName.TemplateA' (Invalid.SameShortName.TemplateA): the template is valid. + +info: validate[0] + Found template 'Invalid.SameShortName.TemplateB' (Invalid.SameShortName.TemplateB): + + Template 'Invalid.SameShortName.TemplateB' (Invalid.SameShortName.TemplateB): the template is valid. + +info: validate[0] + Found template '' (MissingConfigTest): + [Error][MV002] Missing 'name'. + [Error][MV003] Missing 'shortName'. + [Info][MV005] Missing 'sourceName'. + [Info][MV006] Missing 'author'. + [Info][MV007] Missing 'groupIdentity'. + [Info][MV008] Missing 'generatorVersions'. + [Info][MV009] Missing 'precedence'. + [Info][MV010] Missing 'classifications'. + Template '' (MissingConfigTest): the template is not valid. + +info: validate[0] + Found template 'TestAssets.Invalid.InvalidHostData' (TestAssets.Invalid.InvalidHostData): + [Info][MV011] One or more postActions have a malformed or missing manualInstructions value. + Template 'TestAssets.Invalid.InvalidHostData' (TestAssets.Invalid.InvalidHostData): the template is valid. + +info: validate[0] + Found template 'name in base configuration' (TestAssets.Invalid.Localization.InvalidFormat): + + Template 'name in base configuration' (TestAssets.Invalid.Localization.InvalidFormat): the template is valid. + +info: validate[0] + Found template 'name' (TestAssets.Invalid.Localization.ValidationFailure): + [Warning][CONFIG0201] Id of the post action 'pa2' at index '3' is not unique. Only the first post action that uses this id will be localized. + Found localization 'de-DE' for template 'name' (TestAssets.Invalid.Localization.ValidationFailure): + [Error][LOC001] In localization file under the post action with id 'pa1', there are localized strings for manual instruction(s) with ids 'do-not-exist'. These manual instructions do not exist in the template.json file and should be removed from localization file. + [Error][LOC002] Post action(s) with id(s) 'pa0' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + Found localization 'tr' for template 'name' (TestAssets.Invalid.Localization.ValidationFailure): + [Error][LOC002] Post action(s) with id(s) 'pa6' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + Template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the template is valid. + 'de-DE' localization for the template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the localization file is not valid. The localization will be skipped. + 'tr' localization for the template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the localization file is not valid. The localization will be skipped. + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Snapshots/ValidateCommandTests.ValidateCommand_BasicTest.OSX.verified.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Snapshots/ValidateCommandTests.ValidateCommand_BasicTest.OSX.verified.txt new file mode 100644 index 000000000000..f31885155d40 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Snapshots/ValidateCommandTests.ValidateCommand_BasicTest.OSX.verified.txt @@ -0,0 +1,57 @@ +StdErr: + +StdOut: +info: validate[0] + Scanning location '%TEMPLATE_LOCATION%' for the templates.. +fail: Microsoft.TemplateEngine.Orchestrator.RunnableProjects.RunnableProjectGenerator[0] + Failed to load template from %TEMPLATE_LOCATION%/MissingIdentity/.template.config/template.json. + Details: 'identity' is missing or is an empty string. +warn: Template Engine: %scrubbed% +info: validate[0] + Scanning completed +info: validate[0] + Location '%TEMPLATE_LOCATION%': found 6 templates. +info: validate[0] + Found template 'Invalid.SameShortName.TemplateA' (Invalid.SameShortName.TemplateA): + + Template 'Invalid.SameShortName.TemplateA' (Invalid.SameShortName.TemplateA): the template is valid. + +info: validate[0] + Found template 'Invalid.SameShortName.TemplateB' (Invalid.SameShortName.TemplateB): + + Template 'Invalid.SameShortName.TemplateB' (Invalid.SameShortName.TemplateB): the template is valid. + +info: validate[0] + Found template '' (MissingConfigTest): + [Error][MV002] Missing 'name'. + [Error][MV003] Missing 'shortName'. + [Info][MV005] Missing 'sourceName'. + [Info][MV006] Missing 'author'. + [Info][MV007] Missing 'groupIdentity'. + [Info][MV008] Missing 'generatorVersions'. + [Info][MV009] Missing 'precedence'. + [Info][MV010] Missing 'classifications'. + Template '' (MissingConfigTest): the template is not valid. + +info: validate[0] + Found template 'TestAssets.Invalid.InvalidHostData' (TestAssets.Invalid.InvalidHostData): + [Info][MV011] One or more postActions have a malformed or missing manualInstructions value. + Template 'TestAssets.Invalid.InvalidHostData' (TestAssets.Invalid.InvalidHostData): the template is valid. + +info: validate[0] + Found template 'name in base configuration' (TestAssets.Invalid.Localization.InvalidFormat): + + Template 'name in base configuration' (TestAssets.Invalid.Localization.InvalidFormat): the template is valid. + +info: validate[0] + Found template 'name' (TestAssets.Invalid.Localization.ValidationFailure): + [Warning][CONFIG0201] Id of the post action 'pa2' at index '3' is not unique. Only the first post action that uses this id will be localized. + Found localization 'de-DE' for template 'name' (TestAssets.Invalid.Localization.ValidationFailure): + [Error][LOC001] In localization file under the post action with id 'pa1', there are localized strings for manual instruction(s) with ids 'do-not-exist'. These manual instructions do not exist in the template.json file and should be removed from localization file. + [Error][LOC002] Post action(s) with id(s) 'pa0' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + Found localization 'tr' for template 'name' (TestAssets.Invalid.Localization.ValidationFailure): + [Error][LOC002] Post action(s) with id(s) 'pa6' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + Template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the template is valid. + 'de-DE' localization for the template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the localization file is not valid. The localization will be skipped. + 'tr' localization for the template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the localization file is not valid. The localization will be skipped. + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Snapshots/ValidateCommandTests.ValidateCommand_BasicTest.Windows.verified.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Snapshots/ValidateCommandTests.ValidateCommand_BasicTest.Windows.verified.txt new file mode 100644 index 000000000000..f38e561b3444 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/Snapshots/ValidateCommandTests.ValidateCommand_BasicTest.Windows.verified.txt @@ -0,0 +1,57 @@ +StdErr: + +StdOut: +info: validate[0] + Scanning location '%TEMPLATE_LOCATION%' for the templates.. +fail: Microsoft.TemplateEngine.Orchestrator.RunnableProjects.RunnableProjectGenerator[0] + Failed to load template from %TEMPLATE_LOCATION%\MissingIdentity/.template.config/template.json. + Details: 'identity' is missing or is an empty string. +warn: Template Engine: %scrubbed% +info: validate[0] + Scanning completed +info: validate[0] + Location '%TEMPLATE_LOCATION%': found 6 templates. +info: validate[0] + Found template 'Invalid.SameShortName.TemplateA' (Invalid.SameShortName.TemplateA): + + Template 'Invalid.SameShortName.TemplateA' (Invalid.SameShortName.TemplateA): the template is valid. + +info: validate[0] + Found template 'Invalid.SameShortName.TemplateB' (Invalid.SameShortName.TemplateB): + + Template 'Invalid.SameShortName.TemplateB' (Invalid.SameShortName.TemplateB): the template is valid. + +info: validate[0] + Found template '' (MissingConfigTest): + [Error][MV002] Missing 'name'. + [Error][MV003] Missing 'shortName'. + [Info][MV005] Missing 'sourceName'. + [Info][MV006] Missing 'author'. + [Info][MV007] Missing 'groupIdentity'. + [Info][MV008] Missing 'generatorVersions'. + [Info][MV009] Missing 'precedence'. + [Info][MV010] Missing 'classifications'. + Template '' (MissingConfigTest): the template is not valid. + +info: validate[0] + Found template 'TestAssets.Invalid.InvalidHostData' (TestAssets.Invalid.InvalidHostData): + [Info][MV011] One or more postActions have a malformed or missing manualInstructions value. + Template 'TestAssets.Invalid.InvalidHostData' (TestAssets.Invalid.InvalidHostData): the template is valid. + +info: validate[0] + Found template 'name in base configuration' (TestAssets.Invalid.Localization.InvalidFormat): + + Template 'name in base configuration' (TestAssets.Invalid.Localization.InvalidFormat): the template is valid. + +info: validate[0] + Found template 'name' (TestAssets.Invalid.Localization.ValidationFailure): + [Warning][CONFIG0201] Id of the post action 'pa2' at index '3' is not unique. Only the first post action that uses this id will be localized. + Found localization 'de-DE' for template 'name' (TestAssets.Invalid.Localization.ValidationFailure): + [Error][LOC001] In localization file under the post action with id 'pa1', there are localized strings for manual instruction(s) with ids 'do-not-exist'. These manual instructions do not exist in the template.json file and should be removed from localization file. + [Error][LOC002] Post action(s) with id(s) 'pa0' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + Found localization 'tr' for template 'name' (TestAssets.Invalid.Localization.ValidationFailure): + [Error][LOC002] Post action(s) with id(s) 'pa6' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + Template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the template is valid. + 'de-DE' localization for the template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the localization file is not valid. The localization will be skipped. + 'tr' localization for the template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the localization file is not valid. The localization will be skipped. + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/ValidateCommandTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/ValidateCommandTests.cs new file mode 100644 index 000000000000..33309dd69823 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/ValidateCommandTests.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.TemplateEngine.CommandUtils; +using Microsoft.TemplateEngine.Tests; +using Xunit.Abstractions; + +namespace Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests +{ + [Collection("Verify Tests")] + public class ValidateCommandTests : TestBase + { + private readonly ITestOutputHelper _log; + + public ValidateCommandTests(ITestOutputHelper log) + { + _log = log; + } + + [Fact] + public Task ValidateCommand_BasicTest() + { + CommandResult commandResult = new BasicCommand( + _log, + "dotnet", + Path.GetFullPath("Microsoft.TemplateEngine.Authoring.CLI.dll"), + "validate", + Path.Combine(TestTemplatesLocation, "Invalid")) + .Execute(); + + commandResult + .Should() + .Fail(); + + return Verify(FormatOutputStreams(commandResult)) + .UniqueForOSPlatform() + .AddScrubber(text => text.Replace(Path.Combine(TestTemplatesLocation, "Invalid"), "%TEMPLATE_LOCATION%")) + //warning can appear in a different order, therefore scrubbing them + .AddScrubber(text => ScrubByRegex(text, "(warn: Template Engine\\[0\\]\\r?\\n)([^\\r\\n]*)", $"warn: Template Engine: %scrubbed%", RegexOptions.Multiline)); + } + + private static string FormatOutputStreams(CommandResult commandResult) + { + return "StdErr:" + Environment.NewLine + commandResult.StdErr + Environment.NewLine + "StdOut:" + Environment.NewLine + commandResult.StdOut; + } + + private static void ScrubByRegex(StringBuilder output, string pattern, string replacement, RegexOptions regexOptions = RegexOptions.None) + { + string finalOutput = Regex.Replace(output.ToString(), pattern, replacement, regexOptions); + output.Clear(); + output.Append(finalOutput); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/VerifyCommandTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/VerifyCommandTests.cs new file mode 100644 index 000000000000..644cf2cddc2a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/VerifyCommandTests.cs @@ -0,0 +1,212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier; +using Microsoft.TemplateEngine.CommandUtils; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Xunit.Abstractions; + +namespace Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests +{ + public class VerifyCommandTests : TestBase + { + private readonly ITestOutputHelper _log; + + public VerifyCommandTests(ITestOutputHelper log) + { + _log = log; + } + + [Fact] + public void VerifyCommandFullDevLoop() + { + // dots issue https://github.com/VerifyTests/Verify/issues/658 + string workingDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string snapshotsDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string templateOutputDir = "path with spaces"; + + var cmd = new BasicCommand( + _log, + "dotnet", + Path.GetFullPath("Microsoft.TemplateEngine.Authoring.CLI.dll"), + "verify", + "console", + "--template-args", + "--use-program-main -o \"" + templateOutputDir + "\" --no-restore", + "--verify-std", + "-o", + workingDir, + "--snapshots-directory", + snapshotsDir, + "--disable-diff-tool", + "--unique-for", + "architecture", + "--unique-for", + "RuntimeAndVersion"); + + cmd.Execute() + .Should() + .ExitWith((int)TemplateVerificationErrorCode.VerificationFailed) + .And.HaveStdOutContaining("Verification Failed."); + + // Assert template created + Directory.Exists(Path.Combine(workingDir, templateOutputDir)).Should().BeTrue(); + File.Exists(Path.Combine(workingDir, templateOutputDir, "console.csproj")).Should().BeTrue(); + File.Exists(Path.Combine(workingDir, templateOutputDir, "Program.cs")).Should().BeTrue(); + + // Assert verification files created + Directory.Exists(snapshotsDir).Should().BeTrue(); + Directory.GetDirectories(snapshotsDir).Length.Should().Be(2); + //for simplicity move to the created dir + snapshotsDir = Directory.GetDirectories(snapshotsDir).Single(d => d.EndsWith(".received", StringComparison.Ordinal)); + File.Exists(Path.Combine(snapshotsDir, templateOutputDir, "console.csproj")).Should().BeTrue(); + File.Exists(Path.Combine(snapshotsDir, templateOutputDir, "Program.cs")).Should().BeTrue(); + File.Exists(Path.Combine(snapshotsDir, "std-streams", "stdout.txt")).Should().BeTrue(); + File.Exists(Path.Combine(snapshotsDir, "std-streams", "stderr.txt")).Should().BeTrue(); + Directory.GetFiles(snapshotsDir, "*", SearchOption.AllDirectories).Length.Should().Be(4); + // .verified files are only created when diff tool is used - that is however turned off in CI + //File.Exists(Path.Combine(snapshotsDir, "console.console.csproj.verified.csproj")).Should().BeTrue(); + //File.Exists(Path.Combine(snapshotsDir, "console.Program.cs.verified.cs")).Should().BeTrue(); + //File.Exists(Path.Combine(snapshotsDir, "console.StdOut.verified.txt")).Should().BeTrue(); + //File.Exists(Path.Combine(snapshotsDir, "console.StdErr.verified.txt")).Should().BeTrue(); + + // .verified files are only created when diff tool is used - that is however turned off in CI + //File.ReadAllText(Path.Combine(snapshotsDir, "console.console.csproj.verified.csproj")).Should().BeEmpty(); + //File.ReadAllText(Path.Combine(snapshotsDir, "console.Program.cs.verified.cs")).Should().BeEmpty(); + //File.ReadAllText(Path.Combine(snapshotsDir, "console.StdOut.verified.txt")).Should().BeEmpty(); + //File.ReadAllText(Path.Combine(snapshotsDir, "console.StdErr.verified.txt")).Should().BeEmpty(); + File.ReadAllText(Path.Combine(snapshotsDir, templateOutputDir, "console.csproj").UnixifyLineBreaks()).Should() + .BeEquivalentTo(File.ReadAllText(Path.Combine(workingDir, templateOutputDir, "console.csproj")).UnixifyLineBreaks()); + File.ReadAllText(Path.Combine(snapshotsDir, templateOutputDir, "Program.cs").UnixifyLineBreaks()).Should() + .BeEquivalentTo(File.ReadAllText(Path.Combine(workingDir, templateOutputDir, "Program.cs")).UnixifyLineBreaks()); + + // Accept changes + string verifiedDir = snapshotsDir.Replace(".received", ".verified", StringComparison.Ordinal); + Directory.Delete(verifiedDir, false); + Directory.Move(snapshotsDir, verifiedDir); + + //reset the expectations dir to where it was before previous run + snapshotsDir = Path.GetDirectoryName(snapshotsDir)!; + + // And run again same scenario - verification should succeed now + string workingDir2 = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var cmd2 = new BasicCommand( + _log, + "dotnet", + Path.GetFullPath("Microsoft.TemplateEngine.Authoring.CLI.dll"), + "verify", + "console", + "--template-args", + "--use-program-main -o \"" + templateOutputDir + "\" --no-restore", + "--verify-std", + "-o", + workingDir2, + "--snapshots-directory", + snapshotsDir, + "--unique-for", + "architecture", + "--unique-for", + "RuntimeAndVersion"); + + cmd2.Execute() + .Should() + .Pass() + .And.HaveStdOutContaining("Running the verification of console.") + .And.NotHaveStdErr(); + + Directory.Delete(workingDir, true); + Directory.Delete(workingDir2, true); + Directory.Delete(snapshotsDir, true); + } + + [Fact] + public void VerifyCommandFullDevLoopWithNotInstalledTemplate() + { + // dots issue https://github.com/VerifyTests/Verify/issues/658 + string workingDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string snapshotsDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string templateShortName = "TestAssets.SampleTestTemplate"; + string templateOutputDir = templateShortName; + + //get the template location + string templateLocation = Path.Combine(TestTemplatesLocation, "TestTemplate"); + + var cmd = new BasicCommand( + _log, + "dotnet", + Path.GetFullPath("Microsoft.TemplateEngine.Authoring.CLI.dll"), + "verify", + templateShortName, + "--template-path", + templateLocation, + "--template-args", + "--paramB true", + "--verify-std", + "-o", + workingDir, + "--snapshots-directory", + snapshotsDir, + "--disable-diff-tool"); + + cmd.Execute() + .Should() + .ExitWith((int)TemplateVerificationErrorCode.VerificationFailed) + .And.HaveStdOutContaining("Verification Failed."); + + // Assert template created + Directory.Exists(Path.Combine(workingDir, templateOutputDir)).Should().BeTrue(); + File.Exists(Path.Combine(workingDir, templateOutputDir, "Test.cs")).Should().BeTrue(); + + // Assert verification files created + Directory.Exists(snapshotsDir).Should().BeTrue(); + Directory.GetDirectories(snapshotsDir).Length.Should().Be(2); + //for simplicity move to the created dir + snapshotsDir = Directory.GetDirectories(snapshotsDir).Single(d => d.EndsWith(".received", StringComparison.Ordinal)); + File.Exists(Path.Combine(snapshotsDir, templateOutputDir, "Test.cs")).Should().BeTrue(); + File.Exists(Path.Combine(snapshotsDir, "std-streams", "stdout.txt")).Should().BeTrue(); + File.Exists(Path.Combine(snapshotsDir, "std-streams", "stderr.txt")).Should().BeTrue(); + Directory.GetFiles(snapshotsDir, "*", SearchOption.AllDirectories).Length.Should().Be(3); + + File.ReadAllText(Path.Combine(snapshotsDir, templateOutputDir, "Test.cs").UnixifyLineBreaks()).Should() + .BeEquivalentTo(File.ReadAllText(Path.Combine(workingDir, templateOutputDir, "Test.cs")).UnixifyLineBreaks()); + + // Accept changes + string verifiedDir = snapshotsDir.Replace(".received", ".verified", StringComparison.Ordinal); + Directory.Delete(verifiedDir, false); + Directory.Move(snapshotsDir, verifiedDir); + + //reset the expectations dir to where it was before previous run + snapshotsDir = Path.GetDirectoryName(snapshotsDir)!; + + // And run again same scenario - verification should succeed now + string workingDir2 = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var cmd2 = new BasicCommand( + _log, + "dotnet", + Path.GetFullPath("Microsoft.TemplateEngine.Authoring.CLI.dll"), + "verify", + templateShortName, + "--template-path", + templateLocation, + "--template-args", + "--paramB true", + "--verify-std", + "-o", + workingDir2, + "--snapshots-directory", + snapshotsDir); + + cmd2.Execute() + .Should() + .Pass() + .And.HaveStdOutContaining(string.Format("Running the verification of {0}.", templateShortName)) + .And.NotHaveStdErr(); + + Directory.Delete(workingDir, true); + Directory.Delete(workingDir2, true); + Directory.Delete(snapshotsDir, true); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/VerifySettingsFixture.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/VerifySettingsFixture.cs new file mode 100644 index 000000000000..79757a88da50 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/VerifySettingsFixture.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using VerifyTests.DiffPlex; + +namespace Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests +{ + public class VerifySettingsFixture : IDisposable + { + private static bool s_called; + + public VerifySettingsFixture() + { + if (s_called) + { + return; + } + s_called = true; + DerivePathInfo( + (_, _, type, method) => new( + directory: "Snapshots", + typeName: type.Name, + methodName: method.Name)); + + // Customize diff output of verifier + VerifyDiffPlex.Initialize(OutputType.Compact); + } + + public void Dispose() { } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/VerifyTestCollection.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/VerifyTestCollection.cs new file mode 100644 index 000000000000..fb63227639d3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests/VerifyTestCollection.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Authoring.CLI.IntegrationTests +{ + [CollectionDefinition("Verify Tests")] + public class VerifyTestCollection : IClassFixture + { + //intentionally empty + //defines test class collection to share the fixture + //usage [Collection("Verify Tests")] on the test class. + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/Microsoft.TemplateEngine.Authoring.CLI.UnitTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/Microsoft.TemplateEngine.Authoring.CLI.UnitTests.csproj new file mode 100644 index 000000000000..462969e7e66a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/Microsoft.TemplateEngine.Authoring.CLI.UnitTests.csproj @@ -0,0 +1,16 @@ + + + + $(NetCurrent) + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/ValidateCommandTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/ValidateCommandTests.cs new file mode 100644 index 000000000000..7d17777be84c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/ValidateCommandTests.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using Microsoft.TemplateEngine.Authoring.CLI.Commands; +using Microsoft.TemplateEngine.Tests; + +namespace Microsoft.TemplateEngine.Authoring.CLI.UnitTests +{ + public class ValidateCommandTests : TestBase + { + [Fact] + public async Task ValidateCommand_BasicTest_InvalidTemplate() + { + RootCommand root = new() + { + new ValidateCommand() + }; + + int result = await root.Parse(new[] { "validate", Path.Combine(TestTemplatesLocation, "Invalid") }).InvokeAsync(); + + //there are some invalid templates in location "Invalid" + Assert.Equal(1, result); + } + + [Fact] + public async Task ValidateCommand_BasicTest_ValidTemplate() + { + RootCommand root = new() + { + new ValidateCommand() + }; + + int result = await root.Parse(new[] { "validate", Path.Combine(TestTemplatesLocation, "TemplateWithSourceName") }).InvokeAsync(); + + Assert.Equal(0, result); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/VerifyCommandArgsTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/VerifyCommandArgsTests.cs new file mode 100644 index 000000000000..8017674a0335 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/VerifyCommandArgsTests.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.TemplateEngine.Authoring.CLI.Commands.Verify; + +namespace Microsoft.TemplateEngine.Authoring.CLI.UnitTests +{ + public class VerifyCommandArgsTests + { + [Theory] + [InlineData(null, new string[] { })] + [InlineData(" ", new string[] { })] + [InlineData(" a b c", new string[] { "a", "b", "c" })] + [InlineData(" abc ", new string[] { "abc" })] + [InlineData("a \"b c \" d ", new string[] { "a", "b c ", "d" })] + [InlineData("aa \" bb cc \"dd", new string[] { "aa", " bb cc ", "dd" })] + [InlineData("aa q= 'bb cc'dd", new string[] { "aa", "q=", "bb cc", "dd" })] + public void OnTokenizeJoinedArgsResultIsExpected(string? input, IEnumerable expectedOutput) + { + var result = VerifyCommandArgs.TokenizeJoinedArgs(input); + result.Should().BeEquivalentTo(expectedOutput, options => options.WithStrictOrdering()); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/VerifyCommandTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/VerifyCommandTests.cs new file mode 100644 index 000000000000..051ada9862e0 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.CLI.UnitTests/VerifyCommandTests.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using FluentAssertions; +using Microsoft.TemplateEngine.Authoring.CLI.Commands.Verify; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier; + +namespace Microsoft.TemplateEngine.Authoring.CLI.UnitTests +{ + public class VerifyCommandTests + { + public static IEnumerable CanParseVerifyCommandArgsData => + new object?[][] + { + new object[] + { + "someName -p path --template-args \" a b cc\" --disable-diff-tool", + new VerifyCommandArgs( + "someName", + " a b cc") + { + TemplatePath = "path", + DisableDiffTool = true, + DisableDefaultVerificationExcludePatterns = false, + VerificationExcludePatterns = Enumerable.Empty(), + VerificationIncludePatterns = Enumerable.Empty(), + VerifyCommandOutput = false, + IsCommandExpectedToFail = false, + UniqueFor = UniqueForOption.None, + } + }, + new object[] + { + "someName -p path --template-args \" a \'b cc\'\" --unique-for Runtime", + new VerifyCommandArgs( + "someName", + " a \"b cc\"") + { + TemplatePath = "path", + DisableDiffTool = false, + DisableDefaultVerificationExcludePatterns = false, + VerificationExcludePatterns = Enumerable.Empty(), + VerificationIncludePatterns = Enumerable.Empty(), + VerifyCommandOutput = false, + IsCommandExpectedToFail = false, + UniqueFor = UniqueForOption.Runtime, + } + }, + + new object[] + { + "someName", + new VerifyCommandArgs( + "someName", + null) + { + DisableDiffTool = false, + DisableDefaultVerificationExcludePatterns = false, + VerificationExcludePatterns = Enumerable.Empty(), + VerificationIncludePatterns = Enumerable.Empty(), + VerifyCommandOutput = false, + IsCommandExpectedToFail = false, + UniqueFor = UniqueForOption.None, + } + }, + }; + + [Theory] + [MemberData(nameof(CanParseVerifyCommandArgsData))] + internal void CanParseVerifyCommandArgs(string command, VerifyCommandArgs expVerifyCommandArgs) + { + VerifyCommand verifyCommand = new VerifyCommand(); + + ParseResult parseResult = verifyCommand.Parse(command); + + VerifyCommandArgs args = verifyCommand.ParseContext(parseResult); + + args.Should().BeEquivalentTo(expVerifyCommandArgs); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/LocalizeTemplateTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/LocalizeTemplateTests.cs new file mode 100644 index 000000000000..8d35ddab4964 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/LocalizeTemplateTests.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.CommandUtils; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Xunit.Abstractions; + +namespace Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests +{ + public class LocalizeTemplateTests : TestBase + { + private readonly ITestOutputHelper _log; + + public LocalizeTemplateTests(ITestOutputHelper log) + { + _log = log; + } + + [Fact] + public void CanRunTask() + { + string tmpDir = TestUtils.CreateTemporaryFolder(); + TestUtils.DirectoryCopy("Resources/BasicTemplatePackage", tmpDir, true); + TestUtils.SetupNuGetConfigForPackagesLocation(tmpDir, ShippingPackagesLocation); + + new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Pass(); + + new DotnetCommand(_log, "build") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Pass(); + + string locFolder = Path.Combine(tmpDir, "content/TemplateWithSourceName/.template.config/localize"); + + Assert.True(Directory.Exists(locFolder)); + Assert.Equal(14, Directory.GetFiles(locFolder).Length); + Assert.True(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); + + Directory.Delete(tmpDir, true); + } + + [Fact] + public void CanRunTaskSelectedLangs() + { + string tmpDir = TestUtils.CreateTemporaryFolder(); + TestUtils.DirectoryCopy("Resources/TemplatePackageEnDe", tmpDir, true); + TestUtils.SetupNuGetConfigForPackagesLocation(tmpDir, ShippingPackagesLocation); + + new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Pass(); + + new DotnetCommand(_log, "build") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Pass(); + + string locFolder = Path.Combine(tmpDir, "content/TemplateWithSourceName/.template.config/localize"); + + Assert.True(Directory.Exists(locFolder)); + Assert.Equal(2, Directory.GetFiles(locFolder).Length); + Assert.True(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); + Assert.False(File.Exists(Path.Combine(locFolder, "templatestrings.fr.json"))); + + Directory.Delete(tmpDir, true); + } + + [Fact] + public void CanRunTaskSelectedTemplates() + { + string tmpDir = TestUtils.CreateTemporaryFolder(); + TestUtils.DirectoryCopy("Resources/TemplatePackagePartiallyLocalized", tmpDir, true); + TestUtils.SetupNuGetConfigForPackagesLocation(tmpDir, ShippingPackagesLocation); + + new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Pass(); + + new DotnetCommand(_log, "build") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Pass(); + + string locFolder = Path.Combine(tmpDir, "content/localized/.template.config/localize"); + string noLocFolder = Path.Combine(tmpDir, "content/non-localized/.template.config/localize"); + + Assert.True(Directory.Exists(locFolder)); + Assert.Equal(14, Directory.GetFiles(locFolder).Length); + Assert.True(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); + Assert.False(Directory.Exists(noLocFolder)); + + Directory.Delete(tmpDir, true); + } + + [Fact] + public void CanRunTaskAndDetectError() + { + string tmpDir = TestUtils.CreateTemporaryFolder(); + TestUtils.DirectoryCopy("Resources/InvalidTemplatePackage", tmpDir, true); + TestUtils.SetupNuGetConfigForPackagesLocation(tmpDir, ShippingPackagesLocation); + + new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Pass(); + + new DotnetCommand(_log, "build") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Fail() + .And.HaveStdOutContaining("Build FAILED.") + .And.HaveStdOutContaining("Each child of '//postActions' should have a unique id"); + + string locFolder = Path.Combine(tmpDir, "content/TemplateWithSourceName/.template.config/localize"); + + Assert.False(Directory.Exists(locFolder)); + Directory.Delete(tmpDir, true); + } + + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests.csproj new file mode 100644 index 000000000000..d7e054333855 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests.csproj @@ -0,0 +1,31 @@ + + + + $(NetCurrent) + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/BasicTemplatePackage/TemplatePackage.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/BasicTemplatePackage/TemplatePackage.csproj new file mode 100644 index 000000000000..ebdea59c0a0d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/BasicTemplatePackage/TemplatePackage.csproj @@ -0,0 +1,21 @@ + + + $(BundledNETCoreAppTargetFramework) + Template + TemplatePackage + Microsoft + TemplatePackage + true + false + content + $(NoWarn);NU5128 + true + false + true + true + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/BasicTemplatePackage/content/TemplateWithSourceName/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/BasicTemplatePackage/content/TemplateWithSourceName/.template.config/template.json new file mode 100644 index 000000000000..7837eeacefb6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/BasicTemplatePackage/content/TemplateWithSourceName/.template.config/template.json @@ -0,0 +1,12 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourceName", + "description": "Test description", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceName", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceName", + "shortName": "TestAssets.TemplateWithSourceName", + "sourceName": "bar" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/BasicTemplatePackage/content/TemplateWithSourceName/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/BasicTemplatePackage/content/TemplateWithSourceName/foo.cs new file mode 100644 index 000000000000..1fc762b1f62e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/BasicTemplatePackage/content/TemplateWithSourceName/foo.cs @@ -0,0 +1,6 @@ +namespace TemplateWithSourceName +{ + internal class Foo + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage/TemplatePackage.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage/TemplatePackage.csproj new file mode 100644 index 000000000000..ebdea59c0a0d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage/TemplatePackage.csproj @@ -0,0 +1,21 @@ + + + $(BundledNETCoreAppTargetFramework) + Template + TemplatePackage + Microsoft + TemplatePackage + true + false + content + $(NoWarn);NU5128 + true + false + true + true + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage/content/TemplateWithSourceName/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage/content/TemplateWithSourceName/.template.config/template.json new file mode 100644 index 000000000000..7e8240d3e64e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage/content/TemplateWithSourceName/.template.config/template.json @@ -0,0 +1,38 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourceName", + "description": "Test description", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceName", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceName", + "shortName": "TestAssets.TemplateWithSourceName", + "sourceName": "bar", + "postActions": [ + { + "id": "restore", + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + }, + { + "id": "restore", + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage/content/TemplateWithSourceName/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage/content/TemplateWithSourceName/foo.cs new file mode 100644 index 000000000000..1fc762b1f62e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage/content/TemplateWithSourceName/foo.cs @@ -0,0 +1,6 @@ +namespace TemplateWithSourceName +{ + internal class Foo + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingName/TemplatePackage.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingName/TemplatePackage.csproj new file mode 100644 index 000000000000..ebdea59c0a0d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingName/TemplatePackage.csproj @@ -0,0 +1,21 @@ + + + $(BundledNETCoreAppTargetFramework) + Template + TemplatePackage + Microsoft + TemplatePackage + true + false + content + $(NoWarn);NU5128 + true + false + true + true + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingName/content/TemplateWithSourceName/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingName/content/TemplateWithSourceName/.template.config/template.json new file mode 100644 index 000000000000..8d28ccb9f489 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingName/content/TemplateWithSourceName/.template.config/template.json @@ -0,0 +1,10 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "description": "Test description", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceName", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceName", + "sourceName": "bar" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingName/content/TemplateWithSourceName/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingName/content/TemplateWithSourceName/foo.cs new file mode 100644 index 000000000000..1fc762b1f62e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingName/content/TemplateWithSourceName/foo.cs @@ -0,0 +1,6 @@ +namespace TemplateWithSourceName +{ + internal class Foo + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingOptionalData/TemplatePackage.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingOptionalData/TemplatePackage.csproj new file mode 100644 index 000000000000..ebdea59c0a0d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingOptionalData/TemplatePackage.csproj @@ -0,0 +1,21 @@ + + + $(BundledNETCoreAppTargetFramework) + Template + TemplatePackage + Microsoft + TemplatePackage + true + false + content + $(NoWarn);NU5128 + true + false + true + true + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingOptionalData/content/TemplateWithSourceName/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingOptionalData/content/TemplateWithSourceName/.template.config/template.json new file mode 100644 index 000000000000..e3a8f04a733a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingOptionalData/content/TemplateWithSourceName/.template.config/template.json @@ -0,0 +1,10 @@ +{ + "name": "TemplateWithSourceName", + "description": "Test description", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceName", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceName", + "shortName": "TestAssets.TemplateWithSourceName", + "sourceName": "bar" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingOptionalData/content/TemplateWithSourceName/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingOptionalData/content/TemplateWithSourceName/foo.cs new file mode 100644 index 000000000000..1fc762b1f62e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/InvalidTemplatePackage_MissingOptionalData/content/TemplateWithSourceName/foo.cs @@ -0,0 +1,6 @@ +namespace TemplateWithSourceName +{ + internal class Foo + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackageEnDe/TemplatePackage.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackageEnDe/TemplatePackage.csproj new file mode 100644 index 000000000000..59fa7fd9e06f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackageEnDe/TemplatePackage.csproj @@ -0,0 +1,22 @@ + + + $(BundledNETCoreAppTargetFramework) + Template + TemplatePackage + Microsoft + TemplatePackage + true + false + content + $(NoWarn);NU5128 + true + false + true + true + de;en + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackageEnDe/content/TemplateWithSourceName/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackageEnDe/content/TemplateWithSourceName/.template.config/template.json new file mode 100644 index 000000000000..7837eeacefb6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackageEnDe/content/TemplateWithSourceName/.template.config/template.json @@ -0,0 +1,12 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourceName", + "description": "Test description", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceName", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceName", + "shortName": "TestAssets.TemplateWithSourceName", + "sourceName": "bar" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackageEnDe/content/TemplateWithSourceName/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackageEnDe/content/TemplateWithSourceName/foo.cs new file mode 100644 index 000000000000..1fc762b1f62e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackageEnDe/content/TemplateWithSourceName/foo.cs @@ -0,0 +1,6 @@ +namespace TemplateWithSourceName +{ + internal class Foo + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/TemplatePackage.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/TemplatePackage.csproj new file mode 100644 index 000000000000..3bbc7b54e880 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/TemplatePackage.csproj @@ -0,0 +1,22 @@ + + + $(BundledNETCoreAppTargetFramework) + Template + TemplatePackage + Microsoft + TemplatePackage + true + false + content + $(NoWarn);NU5128 + true + false + true + true + content\localized + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/localized/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/localized/.template.config/template.json new file mode 100644 index 000000000000..7837eeacefb6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/localized/.template.config/template.json @@ -0,0 +1,12 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourceName", + "description": "Test description", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceName", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceName", + "shortName": "TestAssets.TemplateWithSourceName", + "sourceName": "bar" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/localized/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/localized/foo.cs new file mode 100644 index 000000000000..1fc762b1f62e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/localized/foo.cs @@ -0,0 +1,6 @@ +namespace TemplateWithSourceName +{ + internal class Foo + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/non-localized/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/non-localized/.template.config/template.json new file mode 100644 index 000000000000..7837eeacefb6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/non-localized/.template.config/template.json @@ -0,0 +1,12 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourceName", + "description": "Test description", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceName", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceName", + "shortName": "TestAssets.TemplateWithSourceName", + "sourceName": "bar" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/non-localized/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/non-localized/foo.cs new file mode 100644 index 000000000000..1fc762b1f62e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/Resources/TemplatePackagePartiallyLocalized/content/non-localized/foo.cs @@ -0,0 +1,6 @@ +namespace TemplateWithSourceName +{ + internal class Foo + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/ValidateTemplatesTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/ValidateTemplatesTests.cs new file mode 100644 index 000000000000..5d76acf455c1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/ValidateTemplatesTests.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.CommandUtils; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Xunit.Abstractions; + +namespace Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests +{ + public class ValidateTemplatesTests : TestBase + { + private readonly ITestOutputHelper _log; + + public ValidateTemplatesTests(ITestOutputHelper log) + { + _log = log; + } + + [Fact] + public void CanRunValidateTask_OnError() + { + string tmpDir = TestUtils.CreateTemporaryFolder(); + TestUtils.DirectoryCopy("Resources/InvalidTemplatePackage_MissingName", tmpDir, true); + TestUtils.SetupNuGetConfigForPackagesLocation(tmpDir, ShippingPackagesLocation); + + new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Pass(); + + new DotnetCommand(_log, "build") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Fail() + .And.HaveStdOutContaining("Template configuration error MV002: Missing 'name'.") + .And.HaveStdOutContaining("Template configuration error MV003: Missing 'shortName'."); + } + + [Fact] + public void CanRunValidateTask_OnInfo() + { + string tmpDir = TestUtils.CreateTemporaryFolder(); + TestUtils.DirectoryCopy("Resources/InvalidTemplatePackage_MissingOptionalData", tmpDir, true); + TestUtils.SetupNuGetConfigForPackagesLocation(tmpDir, ShippingPackagesLocation); + + new DotnetCommand(_log, "add", "TemplatePackage.csproj", "package", "Microsoft.TemplateEngine.Authoring.Tasks", "--prerelease") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Pass(); + + new DotnetCommand(_log, "build") + .WithoutTelemetry() + .WithWorkingDirectory(tmpDir) + .Execute() + .Should() + .Pass() + .And.HaveStdOutContaining("Template configuration message MV006: Missing 'author'.") + .And.HaveStdOutContaining("Template configuration message MV010: Missing 'classifications'."); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.XunitV3.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.XunitV3.csproj new file mode 100644 index 000000000000..3112ec90ef5f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.XunitV3.csproj @@ -0,0 +1,27 @@ + + + + + + $(DefineConstants);XUNIT_V3 + + + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/ExampleTemplateTest.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/ExampleTemplateTest.cs new file mode 100644 index 000000000000..118d215a55f7 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/ExampleTemplateTest.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Authoring.TemplateApiVerifier; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +#if !XUNIT_V3 +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests +{ + public class ExampleTemplateTest : TestBase + { + private readonly ILogger _log; + + public ExampleTemplateTest(ITestOutputHelper log) + { + _log = new XunitLoggerProvider(log).CreateLogger("TestRun"); + } + + // Following 2 tests share identical snapshot folder - that's the reason for the additional + // naming parameters (DoNotPrependCallerMethodNameToScenarioName, DoNotAppendTemplateArgsToScenarioName, ScenarioName) + // The identity of snapshots ilustrates that execution through API and through full blown command leads to identical results + + [Fact] + public async Task VerificationEngineSampleDogfoodTest() + { + string workingDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string templateShortName = "TestAssets.SampleTestTemplate"; + + //get the template location + string templateLocation = Path.Combine(TestTemplatesLocation, "TestTemplate"); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplateSpecificArgs = new string[] { "--paramB", "true" }, + TemplatePath = templateLocation, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + VerifyCommandOutput = true, + DoNotPrependCallerMethodNameToScenarioName = true, + DoNotAppendTemplateArgsToScenarioName = true, + ScenarioName = "SampleDogfoodTest", + // This is here just for testing and documentation purposes - showing functionality of differing snapshots for arch + UniqueFor = UniqueForOption.Architecture, + } + .WithCustomScrubbers( + ScrubbersDefinition.Empty + .AddScrubber(sb => sb.Replace("B is enabled", "*******")) + .AddScrubber((path, content) => + { + if (path.Replace(Path.DirectorySeparatorChar, '/') == "std-streams/stdout.txt") + { + content.Replace("SampleTestTemplate", "%TEMPLATE%"); + } + })); + + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + } + + [Fact] + public async Task VerificationEngineSampleDogfoodTest_ExecThroughApi() + { + string workingDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string templateShortName = "TestAssets.SampleTestTemplate"; + + //get the template location + string templateLocation = Path.Combine(TestTemplatesLocation, "TestTemplate"); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + VerifyCommandOutput = true, + DoNotPrependCallerMethodNameToScenarioName = true, + ScenarioName = "SampleDogfoodTest", + UniqueFor = UniqueForOption.Architecture, + } + .WithInstantiationThroughTemplateCreatorApi(new Dictionary() { { "paramB", "true" } }) + .WithCustomScrubbers( + ScrubbersDefinition.Empty + .AddScrubber(sb => sb.Replace("B is enabled", "*******")) + .AddScrubber((path, content) => + { + if (path.Replace(Path.DirectorySeparatorChar, '/') == "std-streams/stdout.txt") + { + content.Replace("SampleTestTemplate", "%TEMPLATE%"); + } + })); + + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.Shared.props b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.Shared.props new file mode 100644 index 000000000000..c7162b430dee --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.Shared.props @@ -0,0 +1,11 @@ + + + + $(NetCurrent) + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.csproj new file mode 100644 index 000000000000..41f44b6d2310 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/TestAssets.SampleTestTemplate.SampleDogfoodTest.x64.verified/TestAssets.SampleTestTemplate/Test.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/TestAssets.SampleTestTemplate.SampleDogfoodTest.x64.verified/TestAssets.SampleTestTemplate/Test.cs new file mode 100644 index 000000000000..e68913f9bb42 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/TestAssets.SampleTestTemplate.SampleDogfoodTest.x64.verified/TestAssets.SampleTestTemplate/Test.cs @@ -0,0 +1,6 @@ + +// value of paramA: false +// value of paramB: true + + + // ******* diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/TestAssets.SampleTestTemplate.SampleDogfoodTest.x64.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/TestAssets.SampleTestTemplate.SampleDogfoodTest.x64.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/TestAssets.SampleTestTemplate.SampleDogfoodTest.x64.verified/std-streams/stderr.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/TestAssets.SampleTestTemplate.SampleDogfoodTest.x64.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/TestAssets.SampleTestTemplate.SampleDogfoodTest.x64.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..5a67fb40d407 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/TestAssets.SampleTestTemplate.SampleDogfoodTest.x64.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "%TEMPLATE%" was created successfully. \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_DotFile_EditorConfigTests.editorconfig.--empty.verified/editorconfig/.editorconfig b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_DotFile_EditorConfigTests.editorconfig.--empty.verified/editorconfig/.editorconfig new file mode 100644 index 000000000000..6a5db887fd68 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_DotFile_EditorConfigTests.editorconfig.--empty.verified/editorconfig/.editorconfig @@ -0,0 +1,2 @@ +root = true + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_DotFile_EditorConfigTests.editorconfig.--empty.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_DotFile_EditorConfigTests.editorconfig.--empty.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_DotFile_EditorConfigTests.editorconfig.--empty.verified/std-streams/stderr.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_DotFile_EditorConfigTests.editorconfig.--empty.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_DotFile_EditorConfigTests.editorconfig.--empty.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..7d111cb75350 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_DotFile_EditorConfigTests.editorconfig.--empty.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "EditorConfig file" was created successfully. \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_InstallsToCustomLocation_WithSettingsDirectorySpecified.editorconfig.--empty.verified/editorconfig/.editorconfig b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_InstallsToCustomLocation_WithSettingsDirectorySpecified.editorconfig.--empty.verified/editorconfig/.editorconfig new file mode 100644 index 000000000000..6a5db887fd68 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/VerificationEngine_InstallsToCustomLocation_WithSettingsDirectorySpecified.editorconfig.--empty.verified/editorconfig/.editorconfig @@ -0,0 +1,2 @@ +root = true + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample01.basic-template.verified/sample01/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample01.basic-template.verified/sample01/Program.cs new file mode 100644 index 000000000000..5f667a7ad45c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample01.basic-template.verified/sample01/Program.cs @@ -0,0 +1,13 @@ +using System; + +namespace sample01 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine(@" +Date created: 01/01/1999"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample01.basic-template.verified/sample01/sample01.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample01.basic-template.verified/sample01/sample01.csproj new file mode 100644 index 000000000000..b456b34f655b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample01.basic-template.verified/sample01/sample01.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample02.add-parameters.copyrightName=Test Copyright.title=Test Title.verified/sample02/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample02.add-parameters.copyrightName=Test Copyright.title=Test Title.verified/sample02/Program.cs new file mode 100644 index 000000000000..a8d9069dfc8a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample02.add-parameters.copyrightName=Test Copyright.title=Test Title.verified/sample02/Program.cs @@ -0,0 +1,15 @@ +using System; + +namespace sample02 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine(@" +Copyright: Test Copyright +Title: Test Title +"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample02.add-parameters.copyrightName=Test Copyright.title=Test Title.verified/sample02/sample02.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample02.add-parameters.copyrightName=Test Copyright.title=Test Title.verified/sample02/sample02.csproj new file mode 100644 index 000000000000..b456b34f655b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample02.add-parameters.copyrightName=Test Copyright.title=Test Title.verified/sample02/sample02.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Controllers/HomeController.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Controllers/HomeController.cs new file mode 100644 index 000000000000..9eed6f150279 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Controllers/HomeController.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Mvc; + +namespace sample03.Controllers +{ + public class HomeController : Controller + { + public IActionResult Index() + { + return View(); + } + + public IActionResult About() + { + ViewData["Message"] = "Your application description page."; + + return View(); + } + + public IActionResult Error() + { + return View(); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Program.cs new file mode 100644 index 000000000000..e05e2ef40478 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Program.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Hosting; + +namespace sample03 +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Startup.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Startup.cs new file mode 100644 index 000000000000..ec4c961eae45 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Startup.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace sample03 +{ + public class Startup + { + public Startup(IWebHostEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Add framework services. + services.AddMvc(); + services.AddRouting(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + app.UseRouting(); + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseBrowserLink(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + } + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); + }); + + app.UseStaticFiles(); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Home/About.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Home/About.cshtml new file mode 100644 index 000000000000..50476d1fbd4c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Home/About.cshtml @@ -0,0 +1,7 @@ +@{ + ViewData["Title"] = "About"; +} +

@ViewData["Title"].

+

@ViewData["Message"]

+ +

Use this area to provide additional information.

diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Home/Index.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Home/Index.cshtml new file mode 100644 index 000000000000..549f31ec28ce --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Home/Index.cshtml @@ -0,0 +1,5 @@ +@{ + ViewData["Title"] = "Home Page"; +} + +

Hello World!

diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Shared/Error.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Shared/Error.cshtml new file mode 100644 index 000000000000..e514139c454a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Shared/Error.cshtml @@ -0,0 +1,14 @@ +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Shared/_Layout.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Shared/_Layout.cshtml new file mode 100644 index 000000000000..3cd52322c379 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/Shared/_Layout.cshtml @@ -0,0 +1,36 @@ + + + + + + @ViewData["Title"] - Hello Web + + + +
+ @RenderBody() +
+
+

© 2017 - John Smith

+
+
+ + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/_ViewImports.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/_ViewImports.cshtml new file mode 100644 index 000000000000..9a31aaea8213 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using sample03 +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/_ViewStart.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/_ViewStart.cshtml new file mode 100644 index 000000000000..a5f10045db97 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/appsettings.Development.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/appsettings.Development.json new file mode 100644 index 000000000000..fa8ce71a97a3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/appsettings.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/appsettings.json new file mode 100644 index 000000000000..5fff67bacc46 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/sample03.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/sample03.csproj new file mode 100644 index 000000000000..6cd98a269ead --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.enableContactPage=true.verified/sample03/sample03.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Controllers/HomeController.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Controllers/HomeController.cs new file mode 100644 index 000000000000..9eed6f150279 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Controllers/HomeController.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Mvc; + +namespace sample03.Controllers +{ + public class HomeController : Controller + { + public IActionResult Index() + { + return View(); + } + + public IActionResult About() + { + ViewData["Message"] = "Your application description page."; + + return View(); + } + + public IActionResult Error() + { + return View(); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Program.cs new file mode 100644 index 000000000000..e05e2ef40478 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Program.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Hosting; + +namespace sample03 +{ + public static class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Startup.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Startup.cs new file mode 100644 index 000000000000..ec4c961eae45 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Startup.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace sample03 +{ + public class Startup + { + public Startup(IWebHostEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Add framework services. + services.AddMvc(); + services.AddRouting(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + app.UseRouting(); + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseBrowserLink(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + } + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); + }); + + app.UseStaticFiles(); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Home/About.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Home/About.cshtml new file mode 100644 index 000000000000..50476d1fbd4c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Home/About.cshtml @@ -0,0 +1,7 @@ +@{ + ViewData["Title"] = "About"; +} +

@ViewData["Title"].

+

@ViewData["Message"]

+ +

Use this area to provide additional information.

diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Home/Index.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Home/Index.cshtml new file mode 100644 index 000000000000..549f31ec28ce --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Home/Index.cshtml @@ -0,0 +1,5 @@ +@{ + ViewData["Title"] = "Home Page"; +} + +

Hello World!

diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Shared/Error.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Shared/Error.cshtml new file mode 100644 index 000000000000..e514139c454a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Shared/Error.cshtml @@ -0,0 +1,14 @@ +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. +

diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Shared/_Layout.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Shared/_Layout.cshtml new file mode 100644 index 000000000000..3cd52322c379 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/Shared/_Layout.cshtml @@ -0,0 +1,36 @@ + + + + + + @ViewData["Title"] - Hello Web + + + +
+ @RenderBody() +
+
+

© 2017 - John Smith

+
+
+ + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/_ViewImports.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/_ViewImports.cshtml new file mode 100644 index 000000000000..9a31aaea8213 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using sample03 +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/_ViewStart.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/_ViewStart.cshtml new file mode 100644 index 000000000000..a5f10045db97 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/appsettings.Development.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/appsettings.Development.json new file mode 100644 index 000000000000..fa8ce71a97a3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/appsettings.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/appsettings.json new file mode 100644 index 000000000000..5fff67bacc46 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/sample03.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/sample03.csproj new file mode 100644 index 000000000000..6cd98a269ead --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample03.optional-page.verified/sample03/sample03.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample04.parameter-from-list.BackgroundColor=dimgray.verified/sample04/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample04.parameter-from-list.BackgroundColor=dimgray.verified/sample04/Program.cs new file mode 100644 index 000000000000..f256fc53296b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample04.parameter-from-list.BackgroundColor=dimgray.verified/sample04/Program.cs @@ -0,0 +1,16 @@ +using System; + +namespace sample04 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine(@" +Copyright: (c) Contoso +Title: Contoso Sample +Background color: dimgray +"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample04.parameter-from-list.BackgroundColor=dimgray.verified/sample04/sample04.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample04.parameter-from-list.BackgroundColor=dimgray.verified/sample04/sample04.csproj new file mode 100644 index 000000000000..b456b34f655b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample04.parameter-from-list.BackgroundColor=dimgray.verified/sample04/sample04.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=false.verified/sample05/README.md b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=false.verified/sample05/README.md new file mode 100644 index 000000000000..0dc20726878c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=false.verified/sample05/README.md @@ -0,0 +1,5 @@ +The sample in this folder demonstrates: + + - **Multi-project template** - Console with an optional unit test project + +See [`template.json`](./.template.config/template.json) diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=false.verified/sample05/sample05/Sample.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=false.verified/sample05/sample05/Sample.cs new file mode 100644 index 000000000000..dc37e91a1a91 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=false.verified/sample05/sample05/Sample.cs @@ -0,0 +1,12 @@ +using System; + +namespace sample05 +{ + public static class Sample + { + public static string GetName() + { + return "Console and unit test demo"; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=false.verified/sample05/sample05/sample05.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=false.verified/sample05/sample05/sample05.csproj new file mode 100644 index 000000000000..082dac9c2385 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=false.verified/sample05/sample05/sample05.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/README.md b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/README.md new file mode 100644 index 000000000000..0dc20726878c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/README.md @@ -0,0 +1,5 @@ +The sample in this folder demonstrates: + + - **Multi-project template** - Console with an optional unit test project + +See [`template.json`](./.template.config/template.json) diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05.Test/MyTest.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05.Test/MyTest.cs new file mode 100644 index 000000000000..062b0122669c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05.Test/MyTest.cs @@ -0,0 +1,16 @@ +using System; +using Xunit; +using sample05; + +namespace sample05.Test +{ + public class sample05_UnitTest + { + [Fact] + public void sample05_Test() + { + var name = Sample.GetName(); + Assert.Equal("Console and unit test demo",name); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05.Test/sample05.Test.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05.Test/sample05.Test.csproj new file mode 100644 index 000000000000..b775eb077bcc --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05.Test/sample05.Test.csproj @@ -0,0 +1,17 @@ + + + + netapp7.0 + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05/Sample.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05/Sample.cs new file mode 100644 index 000000000000..dc37e91a1a91 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05/Sample.cs @@ -0,0 +1,12 @@ +using System; + +namespace sample05 +{ + public static class Sample + { + public static string GetName() + { + return "Console and unit test demo"; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05/sample05.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05/sample05.csproj new file mode 100644 index 000000000000..082dac9c2385 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample05.multi-project.includetest=true.verified/sample05/sample05/sample05.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample07.param-with-custom-short-name.verified/sample07/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample07.param-with-custom-short-name.verified/sample07/Program.cs new file mode 100644 index 000000000000..d49f6c3abfe3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample07.param-with-custom-short-name.verified/sample07/Program.cs @@ -0,0 +1,15 @@ +using System; + +namespace sample07 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine(@" +Copyright: John Smith +Title: Your title +"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample07.param-with-custom-short-name.verified/sample07/contact.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample07.param-with-custom-short-name.verified/sample07/contact.txt new file mode 100644 index 000000000000..b1b81d8b43cb --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample07.param-with-custom-short-name.verified/sample07/contact.txt @@ -0,0 +1 @@ +Sample file \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample07.param-with-custom-short-name.verified/sample07/sample07.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample07.param-with-custom-short-name.verified/sample07/sample07.csproj new file mode 100644 index 000000000000..b456b34f655b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample07.param-with-custom-short-name.verified/sample07/sample07.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample08.restore-on-create.verified/sample08/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample08.restore-on-create.verified/sample08/Program.cs new file mode 100644 index 000000000000..fec79c241018 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample08.restore-on-create.verified/sample08/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace sample08 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample08.restore-on-create.verified/sample08/sample08.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample08.restore-on-create.verified/sample08/sample08.csproj new file mode 100644 index 000000000000..5f2a78c19fb5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample08.restore-on-create.verified/sample08/sample08.csproj @@ -0,0 +1,7 @@ + + + + Exe + net7.0 + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/Program.cs new file mode 100644 index 000000000000..3555e2baa706 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace sample09 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine(@"see replacement in site.css"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/contact.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/contact.txt new file mode 100644 index 000000000000..b1b81d8b43cb --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/contact.txt @@ -0,0 +1 @@ +Sample file \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/sample09.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/sample09.csproj new file mode 100644 index 000000000000..b456b34f655b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/sample09.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/site.css b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/site.css new file mode 100644 index 000000000000..2109d7197f5b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample09.replace-onlyif-after.backgroundColor=grey.verified/sample09/site.css @@ -0,0 +1,42 @@ +body { + padding-top: 50px; + padding-bottom: 20px; + background-color: grey; +} + +/* Set widths on the form inputs since otherwise they're 100% wide */ +input, +select, +textarea { + max-width: 280px; + color: black; +} + +/* Carousel */ +.carousel-caption p { + font-size: 20px; + line-height: 1.4; + color: black +} + +/* Wrapping element */ +/* Set some basic padding to keep content from hitting the edges */ +.body-content { + padding-left: 15px; + padding-right: 15px; +} + + + +/* Make .svg files in the carousel display properly in older browsers */ +.carousel-inner .item img[src$=".svg"] { + width: 100%; +} + +/* Hide/rearrange for smaller screens */ +@media screen and (max-width: 767px) { + /* Hide captions */ + .carousel-caption { + display: none; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample10.symbol-from-date.verified/sample10/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample10.symbol-from-date.verified/sample10/Program.cs new file mode 100644 index 000000000000..5cd854702b0e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample10.symbol-from-date.verified/sample10/Program.cs @@ -0,0 +1,13 @@ +using System; + +namespace sample10 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine(@" +Date created: **/**/****"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample10.symbol-from-date.verified/sample10/sample10.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample10.symbol-from-date.verified/sample10/sample10.csproj new file mode 100644 index 000000000000..b456b34f655b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample10.symbol-from-date.verified/sample10/sample10.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample11.change-string-casing.verified/sample11/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample11.change-string-casing.verified/sample11/Program.cs new file mode 100644 index 000000000000..d8a0e5e1c88c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample11.change-string-casing.verified/sample11/Program.cs @@ -0,0 +1,15 @@ +using System; + +namespace sample11 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine(@" +Name: John Doe +Name upper: JOHN DOE +Name lower: john doe"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample11.change-string-casing.verified/sample11/sample11.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample11.change-string-casing.verified/sample11/sample11.csproj new file mode 100644 index 000000000000..b456b34f655b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample11.change-string-casing.verified/sample11/sample11.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample13.constant-value.verified/sample13/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample13.constant-value.verified/sample13/Program.cs new file mode 100644 index 000000000000..713262d4c7ae --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample13.constant-value.verified/sample13/Program.cs @@ -0,0 +1,13 @@ +using System; + +namespace sample13 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine(@" +From const: 5001"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample13.constant-value.verified/sample13/sample13.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample13.constant-value.verified/sample13/sample13.csproj new file mode 100644 index 000000000000..b456b34f655b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample13.constant-value.verified/sample13/sample13.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample15.computed-symbol.verified/sample15/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample15.computed-symbol.verified/sample15/Program.cs new file mode 100644 index 000000000000..d655d2ddd4fc --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample15.computed-symbol.verified/sample15/Program.cs @@ -0,0 +1,18 @@ +using System; + +namespace sample15 +{ + class Program + { + static void Main(string[] args) + { + string copyright = "(c) Contoso"; + string title = "Contoso Sample"; + + Console.WriteLine($"Copyright : {copyright}"); + Console.WriteLine($"Title : {title}"); + Console.WriteLine($"BackgroundGreyAndDisplayCopyright evaulated to true"); + + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample15.computed-symbol.verified/sample15/sample15.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample15.computed-symbol.verified/sample15/sample15.csproj new file mode 100644 index 000000000000..b456b34f655b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample15.computed-symbol.verified/sample15/sample15.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample16.string-value-transform.verified/sample16/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample16.string-value-transform.verified/sample16/Program.cs new file mode 100644 index 000000000000..6bc8616823c4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample16.string-value-transform.verified/sample16/Program.cs @@ -0,0 +1,16 @@ +using System; + +namespace sample16 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine(@" +Name: John Doe +Name PascalCase: JohnDoe +Name camelCase: johnDoe +Name kebab-case: john-doe"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample16.string-value-transform.verified/sample16/sample16.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample16.string-value-transform.verified/sample16/sample16.csproj new file mode 100644 index 000000000000..b456b34f655b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/Snapshots/sample16.string-value-transform.verified/sample16/sample16.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/TemplateEngineSamplesTest.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/TemplateEngineSamplesTest.cs new file mode 100644 index 000000000000..ff3d458cae85 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/TemplateEngineSamplesTest.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Authoring.TemplateApiVerifier; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +#if !XUNIT_V3 +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests +{ + public class TemplateEngineSamplesTest : TestBase + { + private readonly ILogger _log; + + public TemplateEngineSamplesTest(ITestOutputHelper log) + { + _log = new XunitLoggerProvider(log).CreateLogger("TestRun"); + } + + [Theory] + [InlineData("01-basic-template", "sample01", null, "no args")] + [InlineData("02-add-parameters", "sample02", new[] { "copyrightName", "Test Copyright", "title", "Test Title" }, "text args")] + [InlineData("03-optional-page", "sample03", new[] { "enableContactPage", "true" }, "optional content included")] + [InlineData("03-optional-page", "sample03", null, "optional content excluded")] + [InlineData("04-parameter-from-list", "sample04", new[] { "BackgroundColor", "dimgray" }, "the choice parameter")] + [InlineData("05-multi-project", "sample05", new[] { "includetest", "true" }, "the optional test project included")] + [InlineData("05-multi-project", "sample05", new[] { "includetest", "false" }, "the optional test project excluded")] + [InlineData("07-param-with-custom-short-name", "sample07", null, "customised parameter name")] + [InlineData("08-restore-on-create", "sample08", null, "restore on create")] + [InlineData("09-replace-onlyif-after", "sample09", new[] { "backgroundColor", "grey" }, "replacing with onlyif condition")] + [InlineData("10-symbol-from-date", "sample10", null, "usage of date generator")] + [InlineData("11-change-string-casing", "sample11", null, "usage of casing generator")] + [InlineData("13-constant-value", "sample13", null, "replacing of constant value")] + [InlineData("15-computed-symbol", "sample15", null, "usage computed symbols")] + [InlineData("16-string-value-transform", "sample16", null, "usage of derived parameter")] + public async Task TemplateEngineSamplesProjectTest( + string folderName, + string shortName, + string[]? args, + string caseDescription) + { + _log.LogInformation($"Template with {caseDescription}"); + + //get the template location + string templateLocation = Path.Combine(GetSamplesTemplateLocation(), folderName); + + var (templateArgs, argsScenarioName) = GetTemplateArgs(args); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: shortName) + { + TemplatePath = templateLocation, + DoNotPrependCallerMethodNameToScenarioName = true, + ScenarioName = $"{folderName.Substring(folderName.IndexOf('-') + 1)}{argsScenarioName}" + } + .WithInstantiationThroughTemplateCreatorApi(templateArgs) + .WithCustomScrubbers( + ScrubbersDefinition.Empty + .AddScrubber(sb => sb.Replace(DateTime.Now.ToString("MM/dd/yyyy"), "**/**/****"))); + + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + } + + private string GetSamplesTemplateLocation() => Path.Combine(CodeBaseRoot, "dotnet-template-samples", "content"); + + private (Dictionary Args, string ArgsScenarioName) GetTemplateArgs(string[]? args) + { + var templateArgs = new Dictionary(); + StringBuilder sb = new StringBuilder(); + + if (args != null) + { + sb.Append('.'); + + for (int indx = 0; indx < args.Length; indx += 2) + { + templateArgs.Add(args[indx], args[indx + 1]); + + sb.Append($"{args[indx]}={args[indx + 1]}"); + if (indx < args.Length - 2) + { + sb.Append('.'); + } + } + } + + return (templateArgs, sb.ToString()); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/VerificationEngineTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/VerificationEngineTests.cs new file mode 100644 index 000000000000..8bc7929e8623 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests/VerificationEngineTests.cs @@ -0,0 +1,184 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.TestHelper; +#if !XUNIT_V3 +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier.IntegrationTests +{ + public class VerificationEngineTests + { + private readonly ILogger _log; + + public VerificationEngineTests(ITestOutputHelper log) + { + _log = new XunitLoggerProvider(log).CreateLogger("TestRun"); + } + + [Fact] + public async Task VerificationEngineFullDevLoop() + { + string workingDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string snapshotsDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string templateDir = "path with spaces"; + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: "console") + { + TemplateSpecificArgs = new string[] { "--use-program-main", "-o", templateDir, "--no-restore" }, + DisableDiffTool = true, + SnapshotsDirectory = snapshotsDir, + OutputDirectory = workingDir, + VerifyCommandOutput = true, + UniqueFor = UniqueForOption.OsPlatform | UniqueForOption.OsPlatform, + }; + + VerificationEngine engine = new VerificationEngine(_log); + Func executeTask = () => engine.Execute(options); + await executeTask + .Should() + .ThrowAsync() + .Where(e => e.TemplateVerificationErrorCode == TemplateVerificationErrorCode.VerificationFailed); + + // Assert template created + Directory.Exists(Path.Combine(workingDir, templateDir)).Should().BeTrue(); + File.Exists(Path.Combine(workingDir, templateDir, "console.csproj")).Should().BeTrue(); + File.Exists(Path.Combine(workingDir, templateDir, "Program.cs")).Should().BeTrue(); + + // Assert verification files created + Directory.Exists(snapshotsDir).Should().BeTrue(); + Directory.GetDirectories(snapshotsDir).Length.Should().Be(2); + //for simplicity move to the received dir + snapshotsDir = Directory.GetDirectories(snapshotsDir).Single(d => d.EndsWith(".received", StringComparison.Ordinal)); + File.Exists(Path.Combine(snapshotsDir, templateDir, "console.csproj")).Should().BeTrue(); + File.Exists(Path.Combine(snapshotsDir, templateDir, "Program.cs")).Should().BeTrue(); + File.Exists(Path.Combine(snapshotsDir, "std-streams", "stdout.txt")).Should().BeTrue(); + File.Exists(Path.Combine(snapshotsDir, "std-streams", "stderr.txt")).Should().BeTrue(); + Directory.GetFiles(snapshotsDir, "*", SearchOption.AllDirectories).Length.Should().Be(4); + + File.ReadAllText(Path.Combine(snapshotsDir, templateDir, "console.csproj").UnixifyLineBreaks()).Should() + .BeEquivalentTo(File.ReadAllText(Path.Combine(workingDir, templateDir, "console.csproj")).UnixifyLineBreaks()); + File.ReadAllText(Path.Combine(snapshotsDir, templateDir, "Program.cs").UnixifyLineBreaks()).Should() + .BeEquivalentTo(File.ReadAllText(Path.Combine(workingDir, templateDir, "Program.cs")).UnixifyLineBreaks()); + + // Accept changes + string verifiedDir = snapshotsDir.Replace(".received", ".verified", StringComparison.Ordinal); + Directory.Delete(verifiedDir, false); + Directory.Move(snapshotsDir, verifiedDir); + + //reset the expectations dir to where it was before previous run + snapshotsDir = Path.GetDirectoryName(snapshotsDir)!; + + // And run again same scenario - verification should succeed now + string workingDir2 = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + TemplateVerifierOptions options2 = new TemplateVerifierOptions(templateName: "console") + { + TemplateSpecificArgs = new string[] { "--use-program-main", "-o", templateDir, "--no-restore" }, + DisableDiffTool = true, + SnapshotsDirectory = snapshotsDir, + OutputDirectory = workingDir2, + VerifyCommandOutput = true, + UniqueFor = UniqueForOption.OsPlatform | UniqueForOption.OsPlatform, + }; + + Func executeTask2 = () => engine.Execute(options2); + await executeTask2 + .Should() + .NotThrowAsync(); + + Directory.Delete(workingDir, true); + Directory.Delete(workingDir2, true); + Directory.Delete(snapshotsDir, true); + } + + [Fact] + public async Task VerificationEngineCustomVerifier() + { + string workingDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string templateDir = "path with spaces"; + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: "console") + { + TemplateSpecificArgs = new string[] { "--use-program-main", "-o", templateDir, "--no-restore" }, + DisableDiffTool = true, + OutputDirectory = workingDir, + VerificationExcludePatterns = new[] { "*.cs" }, + VerifyCommandOutput = true, + UniqueFor = UniqueForOption.OsPlatform | UniqueForOption.OsPlatform, + } + .WithCustomScrubbers( + ScrubbersDefinition.Empty + .AddScrubber(sb => sb.Replace("Donut", "Veggies"), "txt") + .AddScrubber(sb => sb.Replace(DateTime.UtcNow.ToString("yyyy-MM-dd"), "2000-01-01"))) + .WithCustomDirectoryVerifier( + async (content, contentFetcher) => + { + await foreach (var (filePath, scrubbedContent) in contentFetcher.Value) + { + if (Path.GetExtension(filePath).Equals(".cs")) + { + throw new Exception(".cs files should be excluded per VerificationExcludePatterns"); + } + + if (Path.GetFileName(filePath).Equals("stdout.txt", StringComparison.OrdinalIgnoreCase) + && !scrubbedContent.Contains("Console")) + { + throw new Exception("stdout should contain 'Console'"); + } + + if (Path.GetExtension(filePath).Equals(".csproj") + && !scrubbedContent.Contains("enable")) + { + throw new Exception("Implicit usings should be used"); + } + } + }); + + VerificationEngine engine = new VerificationEngine(_log); + Func executeTask = () => engine.Execute(options); + await executeTask + .Should() + .NotThrowAsync(); + + Directory.Delete(workingDir, true); + } + + [Fact] + public async Task VerificationEngine_DotFile_EditorConfigTests() + { + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: "editorconfig") + { + TemplateSpecificArgs = new[] { "--empty" }, + VerifyCommandOutput = true, + }; + + VerificationEngine engine = new VerificationEngine(_log); + // Demonstrate well handling of dot files - workarounding Verify bug https://github.com/VerifyTests/Verify/issues/699 + await engine.Execute(options); + } + + [Fact] + public async Task VerificationEngine_InstallsToCustomLocation_WithSettingsDirectorySpecified() + { + var settingsPath = TestUtils.CreateTemporaryFolder(); + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: "editorconfig") + { + TemplateSpecificArgs = new[] { "--empty" }, + VerifyCommandOutput = false, + SettingsDirectory = settingsPath + }; + + VerificationEngine engine = new VerificationEngine(_log); + Func executeTask = () => engine.Execute(options); + await executeTask + .Should() + .NotThrowAsync(); + + Assert.True(Directory.GetFileSystemEntries(settingsPath).Length != 0); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.XunitV3.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.XunitV3.csproj new file mode 100644 index 000000000000..7e966c33ead2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.XunitV3/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.XunitV3.csproj @@ -0,0 +1,25 @@ + + + + + + $(DefineConstants);XUNIT_V3 + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.Shared.props b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.Shared.props new file mode 100644 index 000000000000..6fc088b56ec0 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.Shared.props @@ -0,0 +1,12 @@ + + + + $(NetCurrent) + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.csproj new file mode 100644 index 000000000000..2e9d35f0d3ce --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests.csproj @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--a#-b#c#--d.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--a#-b#c#--d.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..994af8b0a8a7 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--a#-b#c#--d.verified/std-streams/stderr.txt @@ -0,0 +1 @@ +stderr content \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--a#-b#c#--d.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--a#-b#c#--d.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..e9cc259327d9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--a#-b#c#--d.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +stdout content \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--x#y#-z.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--x#y#-z.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..1980902be4f6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--x#y#-z.verified/std-streams/stderr.txt @@ -0,0 +1 @@ +another stderr content \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--x#y#-z.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--x#y#-z.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..db3dcaee121c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationFailure.made-up-template.--x#y#-z.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +different stdout content \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationSuccess.made-up-template.--x#y#-z.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationSuccess.made-up-template.--x#y#-z.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..1980902be4f6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationSuccess.made-up-template.--x#y#-z.verified/std-streams/stderr.txt @@ -0,0 +1 @@ +another stderr content \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationSuccess.made-up-template.--x#y#-z.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationSuccess.made-up-template.--x#y#-z.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..db3dcaee121c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/Snapshots/ExecuteSucceedsOnExpectedInstantiationSuccess.made-up-template.--x#y#-z.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +different stdout content \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/VerificationEngineTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/VerificationEngineTests.cs new file mode 100644 index 000000000000..876ad080ff0e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests/VerificationEngineTests.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FakeItEasy; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier.Commands; +using Microsoft.TemplateEngine.CommandUtils; +using Microsoft.TemplateEngine.TestHelper; +#if !XUNIT_V3 +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.Authoring.TemplateVerifier.UnitTests +{ + public class VerificationEngineTests + { + private readonly ILogger _log; + + public VerificationEngineTests(ITestOutputHelper log) + { + _log = new XunitLoggerProvider(log).CreateLogger("TestRun"); + } + + [Fact] + public async Task CreateVerificationTaskWithCustomScrubbersAndVerifier() + { + string verifyLocation = "foo\\bar\\baz"; + + Dictionary files = new Dictionary() + { + { "Program.cs", "aa bb cc" }, + { "Subfolder\\Class.cs", "123 456 789 aa" }, + { "out.dll", "a1 b2" } + }; + + IPhysicalFileSystemEx fileSystem = A.Fake(); + A.CallTo(() => fileSystem.EnumerateFiles(verifyLocation, "*", SearchOption.AllDirectories)).Returns(files.Keys); + A.CallTo(() => fileSystem.ReadAllTextAsync(A._, A._)) + .ReturnsLazily((string fileName, CancellationToken _) => Task.FromResult(files[fileName])); + A.CallTo(() => fileSystem.PathRelativeTo(A._, A._)) + .ReturnsLazily((string target, string relativeTo) => target); + + Dictionary resultContents = new Dictionary(); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: "console") + { + TemplateSpecificArgs = null, + OutputDirectory = verifyLocation, + VerificationExcludePatterns = new[] { "*.dll" }, + UniqueFor = null, + } + .WithCustomScrubbers( + ScrubbersDefinition.Empty + .AddScrubber(sb => sb.Replace("bb", "xx"), "cs") + .AddScrubber(sb => sb.Replace("cc", "yy"), "dll") + .AddScrubber(sb => sb.Replace("123", "yy"), "dll") + .AddScrubber(sb => sb.Replace("aa", "zz")) + // supports multiple scrubbers per extension + .AddScrubber(sb => sb.Replace("cc", "**"), "cs")) + .WithCustomDirectoryVerifier( + async (content, contentFetcher) => + { + await foreach (var (filePath, scrubbedContent) in contentFetcher.Value) + { + resultContents[filePath] = scrubbedContent; + } + }); + + await VerificationEngine.CreateVerificationTask( + new VerificationEngine.CallerInfo() + { + ContentDirectory = verifyLocation, + CallerSourceFile = "callerLocation", + CallerMethod = null + }, + options, + fileSystem); + + resultContents.Keys.Count.Should().Be(2); + resultContents["Program.cs"].Should().BeEquivalentTo("zz xx **"); + resultContents["Subfolder\\Class.cs"].Should().BeEquivalentTo("123 456 789 zz"); + } + + [Fact] + public async Task ExecuteFailsOnInstantiationFailure() + { + string workingDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string snapshotsDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + + ICommandRunner commandRunner = A.Fake(); + A.CallTo(() => commandRunner.RunCommand(A._)) + .Returns(new CommandResultData(20, "stdout content", "stderr content", workingDir)); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: "made-up-template") + { + TemplateSpecificArgs = new string[] { "--a", "-b", "c", "--d" }, + DisableDiffTool = true, + SnapshotsDirectory = snapshotsDir, + OutputDirectory = workingDir, + VerifyCommandOutput = true, + UniqueFor = UniqueForOption.OsPlatform | UniqueForOption.OsPlatform, + }; + + VerificationEngine engine = new VerificationEngine(_log); + Func executeTask = () => engine.Execute(options); + await executeTask + .Should() + .ThrowAsync() + .Where(e => e.TemplateVerificationErrorCode == TemplateVerificationErrorCode.InstantiationFailed); + } + + [Fact] + public async Task ExecuteSucceedsOnExpectedInstantiationFailure() + { + string workingDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string snapshotsDir = "Snapshots"; + + ICommandRunner commandRunner = A.Fake(); + A.CallTo(() => commandRunner.RunCommand(A._)) + .Returns(new CommandResultData(20, "stdout content", "stderr content", workingDir)); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: "made-up-template") + { + TemplateSpecificArgs = new string[] { "--a", "-b", "c", "--d" }, + //DisableDiffTool = true, + SnapshotsDirectory = snapshotsDir, + IsCommandExpectedToFail = true, + OutputDirectory = workingDir, + VerifyCommandOutput = true, + }; + + VerificationEngine engine = new VerificationEngine(commandRunner, _log); + await engine.Execute(options); + } + + [Fact] + public async Task ExecuteSucceedsOnExpectedInstantiationSuccess() + { + string workingDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName().Replace(".", string.Empty)); + string snapshotsDir = "Snapshots"; + + ICommandRunner commandRunner = A.Fake(); + A.CallTo(() => commandRunner.RunCommand(A._)) + .Returns(new CommandResultData(0, "different stdout content", "another stderr content", workingDir)); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: "made-up-template") + { + TemplateSpecificArgs = new string[] { "--x", "y", "-z" }, + //DisableDiffTool = true, + SnapshotsDirectory = snapshotsDir, + IsCommandExpectedToFail = false, + OutputDirectory = workingDir, + VerifyCommandOutput = true, + }; + + VerificationEngine engine = new VerificationEngine(commandRunner, _log); + await engine.Execute(options); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/AuthoringTemplatesTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/AuthoringTemplatesTests.cs new file mode 100644 index 000000000000..f87a29467580 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/AuthoringTemplatesTests.cs @@ -0,0 +1,224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Authoring.TemplateApiVerifier; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Xunit.Abstractions; + +namespace Microsoft.TemplateEngine.Authoring.Templates.Tests +{ + public class AuthoringTemplatesTests : TestBase + { + private readonly ILogger _log; + + public AuthoringTemplatesTests(ITestOutputHelper log) + { + _log = new XunitLoggerProvider(log).CreateLogger("TestRun"); + } + + [Fact] + public async Task TemplateJsonTest() + { + string workingDir = TestUtils.CreateTemporaryFolder(); + string templateShortName = "template.json"; + + //get the template location + string templateLocation = Path.Combine(TemplateFeedLocation, "Microsoft.TemplateEngine.Authoring.Templates"); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + DoNotPrependCallerMethodNameToScenarioName = true, + ScenarioName = "Basic", + } + .WithInstantiationThroughTemplateCreatorApi(new Dictionary()); + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + } + + [Fact] + public async Task TemplateJsonTest_WithParameters() + { + string workingDir = TestUtils.CreateTemporaryFolder(); + string templateShortName = "template.json"; + + //get the template location + string templateLocation = Path.Combine(TemplateFeedLocation, "Microsoft.TemplateEngine.Authoring.Templates"); + + var templateParams = new Dictionary() + { + { "TemplateName", "MyTemplate" }, + { "TemplateShortName", "mytemplate" }, + { "TemplateIdentity", "My.Template.Identity" }, + }; + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + DoNotPrependCallerMethodNameToScenarioName = true, + ScenarioName = "WithParams", + } + .WithInstantiationThroughTemplateCreatorApi(templateParams); + + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + } + + [Fact] + public async Task TemplateJsonTest_NoConfigFolder() + { + string workingDir = TestUtils.CreateTemporaryFolder(); + string templateShortName = "template.json"; + + //get the template location + string templateLocation = Path.Combine(TemplateFeedLocation, "Microsoft.TemplateEngine.Authoring.Templates"); + + var templateParams = new Dictionary() + { + { "CreateTemplateConfigFolder", "false" } + }; + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + DoNotPrependCallerMethodNameToScenarioName = true, + ScenarioName = "NoConfigFolder", + } + .WithInstantiationThroughTemplateCreatorApi(templateParams); + + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + } + + [Fact] + public async Task TemplatePackageTest() + { + string workingDir = TestUtils.CreateTemporaryFolder(); + string templateShortName = "templatepack"; + + //get the template location + string templateLocation = Path.Combine(TemplateFeedLocation, "Microsoft.TemplateEngine.Authoring.Templates"); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + DoNotPrependCallerMethodNameToScenarioName = true, + ScenarioName = "Basic", + } + .WithInstantiationThroughTemplateCreatorApi(new Dictionary()); + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + } + + [Fact] + public async Task TemplatePackageTest_WithName() + { + string workingDir = TestUtils.CreateTemporaryFolder(); + string templateShortName = "templatepack"; + + //get the template location + string templateLocation = Path.Combine(TemplateFeedLocation, "Microsoft.TemplateEngine.Authoring.Templates"); + + var templateParams = new Dictionary() + { + { "name", "MyTemplatePackage" }, + }; + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + DoNotPrependCallerMethodNameToScenarioName = true, + ScenarioName = "WithName", + } + .WithInstantiationThroughTemplateCreatorApi(templateParams); + + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + } + + [Fact] + public async Task TemplatePackageTest_NoMSBuildTasks() + { + string workingDir = TestUtils.CreateTemporaryFolder(); + string templateShortName = "templatepack"; + + //get the template location + string templateLocation = Path.Combine(TemplateFeedLocation, "Microsoft.TemplateEngine.Authoring.Templates"); + + var templateParams = new Dictionary() + { + { "EnableMSBuildTasks", "false" }, + }; + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + DoNotPrependCallerMethodNameToScenarioName = true, + ScenarioName = "NoMSBuildTasks", + } + .WithInstantiationThroughTemplateCreatorApi(templateParams); + + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + } + + [Fact] + public async Task TemplateJsonTest_CLI() + { + string workingDir = TestUtils.CreateTemporaryFolder(); + string templateShortName = "template.json"; + + //get the template location + string templateLocation = Path.Combine(TemplateFeedLocation, "Microsoft.TemplateEngine.Authoring.Templates"); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + DoNotPrependCallerMethodNameToScenarioName = true, + VerifyCommandOutput = true, + ScenarioName = "CLI", + }; + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + } + + [Fact] + public async Task TemplatePackageTest_CLI() + { + string workingDir = TestUtils.CreateTemporaryFolder(); + string templateShortName = "templatepack"; + + //get the template location + string templateLocation = Path.Combine(TemplateFeedLocation, "Microsoft.TemplateEngine.Authoring.Templates"); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + DoNotPrependCallerMethodNameToScenarioName = true, + VerifyCommandOutput = true, + ScenarioName = "CLI", + }; + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests.csproj new file mode 100644 index 000000000000..a6c2777544c2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests.csproj @@ -0,0 +1,27 @@ + + + + $(NetCurrent) + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.Basic.verified/template.json/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.Basic.verified/template.json/.template.config/template.json new file mode 100644 index 000000000000..3759f9bbeb16 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.Basic.verified/template.json/.template.config/template.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json.schemastore.org/template", + "$comment": "See https://aka.ms/template-json-reference for complete configuration description. Complete TODOs below and remove the $comment properties. It is recommended to use the JSON editor that supports schema hints to get more information about defined JSON properties and their description.", + "author": "TODO: fill author name here.", + "classifications": [ + "TODO: fill classification here. Common classifications are: Library, Test, Web etc. Each classification should be a separate element of the array. For more details, see https://aka.ms/template-json-reference#classifications." + ], + "name": "New Template", + "description": "TODO: fill the template description here.", + "precedence": "0", + "identity": "New.Template.Identity", + "shortName": "new-template", + "tags": { + "$comment": "TODO: fill the language and type below. Common types are: project, item, solution.", + "language": "C#", + "type": "project" + }, + "$comment_sourceName": "TODO: Source name should follow these rules: https://aka.ms/template-json-source-name. Source name may be removed if replacement for name is not required.", + "sourceName": "New.Template.1", + "preferNameDirectory": true, + "symbols": { + "ExampleParameter": { + "$comment": "TODO: The symbols section defines variables and their values. For more details, see https://aka.ms/template-json-reference#symbols.", + "type": "parameter", + "datatype": "string", + "defaultValue": "example", + "replaces": "valueToReplace", + "isEnabled": false + } + }, + "sources": [ + { + "$comment": "TODO: Sources control the paths for source and target content. For more details see https://aka.ms/template-json-reference#symbols#source-definition. If source definition is not required, remove the property.", + "condition": "false", + "source": "./source-files", + "target": "./target" + } + ], + "constraints": { + "SampleConstraint": { + "$comment": "TODO: The template may define the constraints all of which must be met in order for the template to be used. For more details see https://aka.ms/template-json-reference#symbols#template-constraints. If constraints are not required, remove the property.", + "type": "os", + "args": [ "Linux", "OSX", "Windows" ] + } + }, + "primaryOutputs": [ + { + "$comment": "TODO: Primary outputs define the list of template files for further processing, usually post actions. If primary outputs are not required, remove the property.", + "path": "New.Template.1.cs", + "condition": "false" + } + ], + "postActions": [ + { + "$comment": "TODO: Enables actions to be performed after the project is created. For more details see https://aka.ms/template-json-post-actions. If post actions are not required, remove the property.", + "actionId": "84C0DA21-51C8-4541-9940-6CA19AF04EE6", + "condition": "false", + "id": "sample-post-action", + "manualInstructions": [ + { + "text": "Open file in New.Template.1.cs in the editor." + } + ], + "args": { + "files": "0" + } + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.CLI.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.CLI.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.CLI.verified/std-streams/stderr.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.CLI.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.CLI.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..ef0b9b51e7bc --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.CLI.verified/std-streams/stdout.txt @@ -0,0 +1,5 @@ +The template "template.json configuration file" was created successfully. + +Processing post-creation actions... +Description: Manual actions required +Manual instructions: Open template.json in the editor and complete the configuration. \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.CLI.verified/template.json/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.CLI.verified/template.json/.template.config/template.json new file mode 100644 index 000000000000..3759f9bbeb16 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.CLI.verified/template.json/.template.config/template.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json.schemastore.org/template", + "$comment": "See https://aka.ms/template-json-reference for complete configuration description. Complete TODOs below and remove the $comment properties. It is recommended to use the JSON editor that supports schema hints to get more information about defined JSON properties and their description.", + "author": "TODO: fill author name here.", + "classifications": [ + "TODO: fill classification here. Common classifications are: Library, Test, Web etc. Each classification should be a separate element of the array. For more details, see https://aka.ms/template-json-reference#classifications." + ], + "name": "New Template", + "description": "TODO: fill the template description here.", + "precedence": "0", + "identity": "New.Template.Identity", + "shortName": "new-template", + "tags": { + "$comment": "TODO: fill the language and type below. Common types are: project, item, solution.", + "language": "C#", + "type": "project" + }, + "$comment_sourceName": "TODO: Source name should follow these rules: https://aka.ms/template-json-source-name. Source name may be removed if replacement for name is not required.", + "sourceName": "New.Template.1", + "preferNameDirectory": true, + "symbols": { + "ExampleParameter": { + "$comment": "TODO: The symbols section defines variables and their values. For more details, see https://aka.ms/template-json-reference#symbols.", + "type": "parameter", + "datatype": "string", + "defaultValue": "example", + "replaces": "valueToReplace", + "isEnabled": false + } + }, + "sources": [ + { + "$comment": "TODO: Sources control the paths for source and target content. For more details see https://aka.ms/template-json-reference#symbols#source-definition. If source definition is not required, remove the property.", + "condition": "false", + "source": "./source-files", + "target": "./target" + } + ], + "constraints": { + "SampleConstraint": { + "$comment": "TODO: The template may define the constraints all of which must be met in order for the template to be used. For more details see https://aka.ms/template-json-reference#symbols#template-constraints. If constraints are not required, remove the property.", + "type": "os", + "args": [ "Linux", "OSX", "Windows" ] + } + }, + "primaryOutputs": [ + { + "$comment": "TODO: Primary outputs define the list of template files for further processing, usually post actions. If primary outputs are not required, remove the property.", + "path": "New.Template.1.cs", + "condition": "false" + } + ], + "postActions": [ + { + "$comment": "TODO: Enables actions to be performed after the project is created. For more details see https://aka.ms/template-json-post-actions. If post actions are not required, remove the property.", + "actionId": "84C0DA21-51C8-4541-9940-6CA19AF04EE6", + "condition": "false", + "id": "sample-post-action", + "manualInstructions": [ + { + "text": "Open file in New.Template.1.cs in the editor." + } + ], + "args": { + "files": "0" + } + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.NoConfigFolder.verified/template.json/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.NoConfigFolder.verified/template.json/template.json new file mode 100644 index 000000000000..3759f9bbeb16 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.NoConfigFolder.verified/template.json/template.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json.schemastore.org/template", + "$comment": "See https://aka.ms/template-json-reference for complete configuration description. Complete TODOs below and remove the $comment properties. It is recommended to use the JSON editor that supports schema hints to get more information about defined JSON properties and their description.", + "author": "TODO: fill author name here.", + "classifications": [ + "TODO: fill classification here. Common classifications are: Library, Test, Web etc. Each classification should be a separate element of the array. For more details, see https://aka.ms/template-json-reference#classifications." + ], + "name": "New Template", + "description": "TODO: fill the template description here.", + "precedence": "0", + "identity": "New.Template.Identity", + "shortName": "new-template", + "tags": { + "$comment": "TODO: fill the language and type below. Common types are: project, item, solution.", + "language": "C#", + "type": "project" + }, + "$comment_sourceName": "TODO: Source name should follow these rules: https://aka.ms/template-json-source-name. Source name may be removed if replacement for name is not required.", + "sourceName": "New.Template.1", + "preferNameDirectory": true, + "symbols": { + "ExampleParameter": { + "$comment": "TODO: The symbols section defines variables and their values. For more details, see https://aka.ms/template-json-reference#symbols.", + "type": "parameter", + "datatype": "string", + "defaultValue": "example", + "replaces": "valueToReplace", + "isEnabled": false + } + }, + "sources": [ + { + "$comment": "TODO: Sources control the paths for source and target content. For more details see https://aka.ms/template-json-reference#symbols#source-definition. If source definition is not required, remove the property.", + "condition": "false", + "source": "./source-files", + "target": "./target" + } + ], + "constraints": { + "SampleConstraint": { + "$comment": "TODO: The template may define the constraints all of which must be met in order for the template to be used. For more details see https://aka.ms/template-json-reference#symbols#template-constraints. If constraints are not required, remove the property.", + "type": "os", + "args": [ "Linux", "OSX", "Windows" ] + } + }, + "primaryOutputs": [ + { + "$comment": "TODO: Primary outputs define the list of template files for further processing, usually post actions. If primary outputs are not required, remove the property.", + "path": "New.Template.1.cs", + "condition": "false" + } + ], + "postActions": [ + { + "$comment": "TODO: Enables actions to be performed after the project is created. For more details see https://aka.ms/template-json-post-actions. If post actions are not required, remove the property.", + "actionId": "84C0DA21-51C8-4541-9940-6CA19AF04EE6", + "condition": "false", + "id": "sample-post-action", + "manualInstructions": [ + { + "text": "Open file in New.Template.1.cs in the editor." + } + ], + "args": { + "files": "0" + } + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.WithParams.verified/template.json/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.WithParams.verified/template.json/.template.config/template.json new file mode 100644 index 000000000000..d4d55a312a44 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/template.json.WithParams.verified/template.json/.template.config/template.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json.schemastore.org/template", + "$comment": "See https://aka.ms/template-json-reference for complete configuration description. Complete TODOs below and remove the $comment properties. It is recommended to use the JSON editor that supports schema hints to get more information about defined JSON properties and their description.", + "author": "TODO: fill author name here.", + "classifications": [ + "TODO: fill classification here. Common classifications are: Library, Test, Web etc. Each classification should be a separate element of the array. For more details, see https://aka.ms/template-json-reference#classifications." + ], + "name": "MyTemplate", + "description": "TODO: fill the template description here.", + "precedence": "0", + "identity": "My.Template.Identity", + "shortName": "mytemplate", + "tags": { + "$comment": "TODO: fill the language and type below. Common types are: project, item, solution.", + "language": "C#", + "type": "project" + }, + "$comment_sourceName": "TODO: Source name should follow these rules: https://aka.ms/template-json-source-name. Source name may be removed if replacement for name is not required.", + "sourceName": "New.Template.1", + "preferNameDirectory": true, + "symbols": { + "ExampleParameter": { + "$comment": "TODO: The symbols section defines variables and their values. For more details, see https://aka.ms/template-json-reference#symbols.", + "type": "parameter", + "datatype": "string", + "defaultValue": "example", + "replaces": "valueToReplace", + "isEnabled": false + } + }, + "sources": [ + { + "$comment": "TODO: Sources control the paths for source and target content. For more details see https://aka.ms/template-json-reference#symbols#source-definition. If source definition is not required, remove the property.", + "condition": "false", + "source": "./source-files", + "target": "./target" + } + ], + "constraints": { + "SampleConstraint": { + "$comment": "TODO: The template may define the constraints all of which must be met in order for the template to be used. For more details see https://aka.ms/template-json-reference#symbols#template-constraints. If constraints are not required, remove the property.", + "type": "os", + "args": [ "Linux", "OSX", "Windows" ] + } + }, + "primaryOutputs": [ + { + "$comment": "TODO: Primary outputs define the list of template files for further processing, usually post actions. If primary outputs are not required, remove the property.", + "path": "New.Template.1.cs", + "condition": "false" + } + ], + "postActions": [ + { + "$comment": "TODO: Enables actions to be performed after the project is created. For more details see https://aka.ms/template-json-post-actions. If post actions are not required, remove the property.", + "actionId": "84C0DA21-51C8-4541-9940-6CA19AF04EE6", + "condition": "false", + "id": "sample-post-action", + "manualInstructions": [ + { + "text": "Open file in New.Template.1.cs in the editor." + } + ], + "args": { + "files": "0" + } + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.Basic.verified/templatepack/README.md b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.Basic.verified/templatepack/README.md new file mode 100644 index 000000000000..e9c4c2fb0593 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.Basic.verified/templatepack/README.md @@ -0,0 +1,2 @@ + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.Basic.verified/templatepack/content/SampleTemplate/placeholder.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.Basic.verified/templatepack/content/SampleTemplate/placeholder.txt new file mode 100644 index 000000000000..5c9d9e9116a5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.Basic.verified/templatepack/content/SampleTemplate/placeholder.txt @@ -0,0 +1 @@ +Place your templates to this folder. diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.Basic.verified/templatepack/templatepack.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.Basic.verified/templatepack/templatepack.csproj new file mode 100644 index 000000000000..459520edd2d2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.Basic.verified/templatepack/templatepack.csproj @@ -0,0 +1,42 @@ + + + + + + templatepack + 1.0 + TODO: fill the package name here + TODO: fill the author name or organization here + TODO: fill the package description here + TODO: fill the tags here + TODO: include a link to an associated project, repository, or company website + + + Template + $(BundledNETCoreAppTargetFramework) + true + false + content + $(NoWarn);NU5128 + true + README.md + + + + false + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/std-streams/stderr.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..f4cbee2a6ed7 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/std-streams/stdout.txt @@ -0,0 +1,5 @@ +The template "Template Package" was created successfully. + +Processing post-creation actions... +Description: Manual actions required +Manual instructions: Open *.csproj in the editor and complete the package metadata configuration. Copy the templates to 'content' folder. Fill in README.md. \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/templatepack/README.md b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/templatepack/README.md new file mode 100644 index 000000000000..e9c4c2fb0593 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/templatepack/README.md @@ -0,0 +1,2 @@ + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/templatepack/content/SampleTemplate/placeholder.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/templatepack/content/SampleTemplate/placeholder.txt new file mode 100644 index 000000000000..5c9d9e9116a5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/templatepack/content/SampleTemplate/placeholder.txt @@ -0,0 +1 @@ +Place your templates to this folder. diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/templatepack/templatepack.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/templatepack/templatepack.csproj new file mode 100644 index 000000000000..459520edd2d2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.CLI.verified/templatepack/templatepack.csproj @@ -0,0 +1,42 @@ + + + + + + templatepack + 1.0 + TODO: fill the package name here + TODO: fill the author name or organization here + TODO: fill the package description here + TODO: fill the tags here + TODO: include a link to an associated project, repository, or company website + + + Template + $(BundledNETCoreAppTargetFramework) + true + false + content + $(NoWarn);NU5128 + true + README.md + + + + false + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.NoMSBuildTasks.verified/templatepack/README.md b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.NoMSBuildTasks.verified/templatepack/README.md new file mode 100644 index 000000000000..e9c4c2fb0593 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.NoMSBuildTasks.verified/templatepack/README.md @@ -0,0 +1,2 @@ + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.NoMSBuildTasks.verified/templatepack/content/SampleTemplate/placeholder.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.NoMSBuildTasks.verified/templatepack/content/SampleTemplate/placeholder.txt new file mode 100644 index 000000000000..5c9d9e9116a5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.NoMSBuildTasks.verified/templatepack/content/SampleTemplate/placeholder.txt @@ -0,0 +1 @@ +Place your templates to this folder. diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.NoMSBuildTasks.verified/templatepack/templatepack.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.NoMSBuildTasks.verified/templatepack/templatepack.csproj new file mode 100644 index 000000000000..9a3a04a084a3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.NoMSBuildTasks.verified/templatepack/templatepack.csproj @@ -0,0 +1,34 @@ + + + + + + templatepack + 1.0 + TODO: fill the package name here + TODO: fill the author name or organization here + TODO: fill the package description here + TODO: fill the tags here + TODO: include a link to an associated project, repository, or company website + + + Template + $(BundledNETCoreAppTargetFramework) + true + false + content + $(NoWarn);NU5128 + true + README.md + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.WithName.verified/templatepack/MyTemplatePackage.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.WithName.verified/templatepack/MyTemplatePackage.csproj new file mode 100644 index 000000000000..9fed62d66981 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.WithName.verified/templatepack/MyTemplatePackage.csproj @@ -0,0 +1,42 @@ + + + + + + MyTemplatePackage + 1.0 + TODO: fill the package name here + TODO: fill the author name or organization here + TODO: fill the package description here + TODO: fill the tags here + TODO: include a link to an associated project, repository, or company website + + + Template + $(BundledNETCoreAppTargetFramework) + true + false + content + $(NoWarn);NU5128 + true + README.md + + + + false + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.WithName.verified/templatepack/README.md b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.WithName.verified/templatepack/README.md new file mode 100644 index 000000000000..e9c4c2fb0593 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.WithName.verified/templatepack/README.md @@ -0,0 +1,2 @@ + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.WithName.verified/templatepack/content/SampleTemplate/placeholder.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.WithName.verified/templatepack/content/SampleTemplate/placeholder.txt new file mode 100644 index 000000000000..5c9d9e9116a5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Templates.IntegrationTests/Snapshots/templatepack.WithName.verified/templatepack/content/SampleTemplate/placeholder.txt @@ -0,0 +1 @@ +Place your templates to this folder. diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/BalancedNestingTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/BalancedNestingTests.cs new file mode 100644 index 000000000000..455087985b6a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/BalancedNestingTests.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class BalancedNestingTests : TestBase, IClassFixture + { + private readonly ILogger _logger; + + public BalancedNestingTests(TestLoggerFactory testLoggerFactory) + { + _logger = testLoggerFactory.CreateLogger(); + } + + // The initial construction of the BalancedNesting operation is supposed to have comment fixing off by default. + // (Usually, it gets toggled by conditional processing). + // This test ensures that it's off by default. + [Fact(DisplayName = nameof(VerifyPseudoCommentFixingIsOffByDefault))] + public void VerifyPseudoCommentFixingIsOffByDefault() + { + string originalValue = @""; // pseudo comment is fixed + + IProcessor processor = SetupXmlBalancedNestingProcessor(true); // comment fixing on + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + // Inner pseudo comments should never get fixed, regardless of whether outer comment fixing occurs, + // and regardless of whether comment fixing is turned on or off. + [Fact(DisplayName = nameof(VerifyInnerPseudoCommentIsNotFixed))] + public void VerifyInnerPseudoCommentIsNotFixed() + { + string originalValue = @""; + string expectedValue = @""; // inner pseudo comment is not changed + + IProcessor processor = SetupXmlBalancedNestingProcessor(true); // comment fixing on + RunAndVerify(originalValue, expectedValue, processor, 9999, changeOverride: true); // value doesn't change, but processer implies it does (thus the test check override) + } + + // Inner pseudo comments should never get fixed, regardless of whether outer comment fixing occurs, + // and regardless of whether comment fixing is turned on or off. + [Fact(DisplayName = nameof(VerifyInnerPseudoCommentIsNotFixedWhenOuterCommentIsFixed))] + public void VerifyInnerPseudoCommentIsNotFixedWhenOuterCommentIsFixed() + { + string originalValue = @""; // outer is fixed, inner is not changed + + IProcessor processor = SetupXmlBalancedNestingProcessor(true); // comment fixing on + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + // Lead heavy comments are not correctly dealt with. It would require too much look-ahead. + // This sort of situation is really an authoring problem. + // This tests that the values remain unchanged - + // (which isn't ideal, but as good as we can hope for without significant performance loss). + // The trailing pseudo comment is not an outer-balanced comment, and so remains unchanged. + [Fact(DisplayName = nameof(VerifyUnbalancedLeadHeavyCommentsAreHandledSanely))] + public void VerifyUnbalancedLeadHeavyCommentsAreHandledSanely() + { + string originalValue = @" -- >"; + + IProcessor processor = SetupXmlBalancedNestingProcessor(true); // comment fixing on + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + // This situation is also indicative of an authoring problem. + // The first pseudo-comment is made real because it's balanced with the leading comment. + // But the second (real) end comment is unbalanced and remains unchanged. + [Fact(DisplayName = nameof(VerifyUnbalancedTrailingHeavyRealCommentIsHandledSanely))] + public void VerifyUnbalancedTrailingHeavyRealCommentIsHandledSanely() + { + string originalValue = @""; + string expectedValue = @" -->"; + + IProcessor processor = SetupXmlBalancedNestingProcessor(true); // comment fixing on + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + private IProcessor SetupXmlBalancedNestingProcessor(bool? isCommentFixingInitiallyOn = null) + { + string commentFixOperationId = "Fix Comments"; + string resetId = "Reset"; + + IOperationProvider[] operations = + { + new BalancedNesting("".TokenConfig(), "-- >".TokenConfig(), commentFixOperationId, resetId, isCommentFixingInitiallyOn ?? false), + }; + VariableCollection variables = new VariableCollection(); + EngineConfig engineConfig = new EngineConfig(_logger, variables); + IProcessor processor = Processor.Create(engineConfig, operations); + + if (isCommentFixingInitiallyOn.HasValue) + { + engineConfig.Flags[commentFixOperationId] = isCommentFixingInitiallyOn.Value; + } + + return processor; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ChunkMemoryStream.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ChunkMemoryStream.cs new file mode 100644 index 000000000000..cd63e3eefbb4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ChunkMemoryStream.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Core.UnitTests; + +internal class ChunkMemoryStream : MemoryStream +{ + private readonly int _chunkSize; + + internal ChunkMemoryStream(int chunkSize) + { + _chunkSize = chunkSize; + } + + internal ChunkMemoryStream(byte[] buffer, int chunkSize) : base(buffer) + { + _chunkSize = chunkSize; + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (count > _chunkSize) + { + count = _chunkSize; + } + return base.Read(buffer, offset, count); + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ChunkStreamReadTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ChunkStreamReadTests.cs new file mode 100644 index 000000000000..5646c256292d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ChunkStreamReadTests.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class ChunkStreamReadTests : TestBase, IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public ChunkStreamReadTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: this.GetType().Name, virtualize: true); + } + + [Fact] + public void VerifyLongStreamNoReplacement() + { + Random rnd = new Random(); + byte[] valueBytes = new byte[1024 * 1024]; + rnd.NextBytes(valueBytes); + ChunkMemoryStream input = new ChunkMemoryStream(valueBytes, 512); + ChunkMemoryStream output = new ChunkMemoryStream(1024); + + IOperationProvider[] operations = []; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + processor.Run(input, output, 1024); + Assert.Equal(input.Length, output.Length); + + input.Position = 0; + output.Position = 0; + + int file1byte; + int file2byte; + do + { + file1byte = input.ReadByte(); + file2byte = output.ReadByte(); + } + while ((file1byte == file2byte) && (file1byte != -1)); + Assert.Equal(0, file1byte - file2byte); + } + + [Fact] + public void VerifyLongStreamWithReplacement() + { + string value = @"test value test"; + string expected = @"test foo test"; + + StringBuilder valueBuilder = new StringBuilder(); + StringBuilder expectedBuilder = new StringBuilder(); + + int repetitionsInMaxInMemoryBuffer = StreamProxy.MaxRecommendedBufferedFileSize / expected.Length; + + for (int i = 0; i < repetitionsInMaxInMemoryBuffer + 1; i++) + { + valueBuilder.Append(value); + expectedBuilder.Append(expected); + } + value = valueBuilder.ToString(); + expected = expectedBuilder.ToString(); + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + ChunkMemoryStream input = new ChunkMemoryStream(valueBytes, 10); + ChunkMemoryStream output = new ChunkMemoryStream(10); + + IOperationProvider[] operations = { new Replacement("value".TokenConfig(), "foo", null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1000); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact] + public void VerifyLongStreamWithReplacementBeforeAfter() + { + string value = @"test valueA before valueA after valueB valueB"; + string expected = @"test foo before valueA after bar valueB"; + + StringBuilder valueBuilder = new StringBuilder(); + StringBuilder expectedBuilder = new StringBuilder(); + + int repetitionsInMaxInMemoryBuffer = StreamProxy.MaxRecommendedBufferedFileSize / expected.Length; + + for (int i = 0; i < repetitionsInMaxInMemoryBuffer + 1; i++) + { + valueBuilder.Append(value); + expectedBuilder.Append(expected); + } + value = valueBuilder.ToString(); + expected = expectedBuilder.ToString(); + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + ChunkMemoryStream input = new ChunkMemoryStream(valueBytes, 10); + ChunkMemoryStream output = new ChunkMemoryStream(10); + + IOperationProvider[] operations = + { + new Replacement("valueA".TokenConfigBuilder().OnlyIfBefore(" before"), "foo", null, true), + new Replacement("valueB".TokenConfigBuilder().OnlyIfAfter("after "), "bar", null, true), + }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1000); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact] + public void VerifyConsumeWholeLine() + { + MockOperation o = new MockOperation( + null, + (state, length, ref position, token) => + { + state.ConsumeWholeLine(ref length, ref position); + return 0; + }, + true, + "There"u8.ToArray()); + + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, new VariableCollection()); + IProcessor processor = Processor.Create(cfg, o.Provider); + byte[] data = "Hello \r\n There \r\n You"u8.ToArray(); + Stream input = new ChunkMemoryStream(data, 1); + Stream output = new ChunkMemoryStream(1); + bool changed = processor.Run(input, output, 5); + + Verify(Encoding.UTF8, output, changed, "Hello \r\n There \r\n You", "Hello \r\n You"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/CombinedStreamTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/CombinedStreamTests.cs new file mode 100644 index 000000000000..012e9d0e238b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/CombinedStreamTests.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Util; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class CombinedStreamTests + { + [Theory] + [InlineData(2 * 1024 * 1024)] + [InlineData(0)] + [InlineData(1)] + [InlineData(10000)] + [InlineData((1024 * 1024) + 10000)] + public void CanReadStream(int requestedCount) + { + Random rnd = new Random(); + byte[] valueBytes1 = new byte[1024 * 1024]; + byte[] valueBytes2 = new byte[1024 * 1024]; + rnd.NextBytes(valueBytes1); + rnd.NextBytes(valueBytes2); + Stream stream1 = new ChunkMemoryStream(valueBytes1, 1024); + Stream stream2 = new ChunkMemoryStream(valueBytes2, 1024); + + CombinedStream stream = new CombinedStream(stream1, stream2, inner => stream2 = inner); + + byte[] read = new byte[2 * 1024 * 1024]; + int nRead = stream.Read(read, 0, requestedCount); + + Assert.Equal(requestedCount, nRead); + + var upperBound = requestedCount > 1024 * 1024 ? 1024 * 1024 : requestedCount; + for (int i = 0; i < upperBound; i++) + { + Assert.Equal(valueBytes1[i], read[i]); + } + if (requestedCount > 1024 * 1024) + { + for (int i = 0; i < requestedCount - (1024 * 1024); i++) + { + Assert.Equal(valueBytes2[i], read[i + (1024 * 1024)]); + } + } + for (int i = requestedCount; i < 2 * 1024 * 1024; i++) + { + Assert.Equal(0, read[i]); + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/CommonOperationsTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/CommonOperationsTests.cs new file mode 100644 index 000000000000..fab88b00b5b5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/CommonOperationsTests.cs @@ -0,0 +1,286 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class CommonOperationsTests : IClassFixture + { + private readonly ILogger _logger; + + public CommonOperationsTests(TestLoggerFactory testLoggerFactory) + { + _logger = testLoggerFactory.CreateLogger(); + } + + [Fact(DisplayName = nameof(VerifyTrimWhitespaceForward))] + public void VerifyTrimWhitespaceForward() + { + MockOperation o = new MockOperation( + null, + (state, length, ref position, token) => + { + state.TrimWhitespace(true, false, ref length, ref position); + return 0; + }, + true, + "Hello"u8.ToArray()); + + EngineConfig cfg = new EngineConfig(_logger, new VariableCollection()); + IProcessor processor = Processor.Create(cfg, o.Provider); + byte[] data = "Hello \r\n There"u8.ToArray(); + Stream d = new MemoryStream(data); + MemoryStream result = new MemoryStream(); + bool modified = processor.Run(d, result); + + Assert.True(modified); + string outcomeString = Encoding.UTF8.GetString(result.ToArray()); + Assert.Equal(" There", outcomeString); + } + + [Fact(DisplayName = nameof(VerifyTrimWhitespaceBackward))] + public void VerifyTrimWhitespaceBackward() + { + MockOperation o = new MockOperation( + null, + (state, length, ref position, token) => + { + state.TrimWhitespace(false, true, ref length, ref position); + return 0; + }, + true, + "There"u8.ToArray()); + + EngineConfig cfg = new EngineConfig(_logger, new VariableCollection()); + IProcessor processor = Processor.Create(cfg, o.Provider); + byte[] data = "Hello \r\n There"u8.ToArray(); + Stream d = new MemoryStream(data); + MemoryStream result = new MemoryStream(); + bool modified = processor.Run(d, result); + + Assert.True(modified); + string outcomeString = Encoding.UTF8.GetString(result.ToArray()); + Assert.Equal("Hello \r\n", outcomeString); + } + + [Fact(DisplayName = nameof(VerifyTrimWhitespaceBothDirections))] + public void VerifyTrimWhitespaceBothDirections() + { + MockOperation o = new MockOperation( + null, + (state, length, ref position, token) => + { + state.TrimWhitespace(true, true, ref length, ref position); + return 0; + }, + true, + "There"u8.ToArray()); + + EngineConfig cfg = new EngineConfig(_logger, new VariableCollection()); + IProcessor processor = Processor.Create(cfg, o.Provider); + byte[] data = "Hello \r\n There \r\n You"u8.ToArray(); + Stream d = new MemoryStream(data); + MemoryStream result = new MemoryStream(); + bool modified = processor.Run(d, result); + + Assert.True(modified); + string outcomeString = Encoding.UTF8.GetString(result.ToArray()); + Assert.Equal("Hello \r\n You", outcomeString); + } + + [Fact(DisplayName = nameof(VerifyTrimWhitespaceNeitherDirection))] + public void VerifyTrimWhitespaceNeitherDirection() + { + MockOperation o = new MockOperation( + null, + (state, length, ref position, token) => + { + state.TrimWhitespace(false, false, ref length, ref position); + return 0; + }, + true, + "There"u8.ToArray()); + + EngineConfig cfg = new EngineConfig(_logger, new VariableCollection()); + IProcessor processor = Processor.Create(cfg, o.Provider); + byte[] data = "Hello \r\n There \r\n You"u8.ToArray(); + Stream d = new MemoryStream(data); + MemoryStream result = new MemoryStream(); + bool modified = processor.Run(d, result); + + Assert.True(modified); + string outcomeString = Encoding.UTF8.GetString(result.ToArray()); + Assert.Equal("Hello \r\n \r\n You", outcomeString); + } + + [Fact(DisplayName = nameof(VerifyConsumeWholeLine))] + public void VerifyConsumeWholeLine() + { + MockOperation o = new MockOperation( + null, + (state, length, ref position, token) => + { + state.ConsumeWholeLine(ref length, ref position); + return 0; + }, + true, + "There"u8.ToArray()); + + EngineConfig cfg = new EngineConfig(_logger, new VariableCollection()); + IProcessor processor = Processor.Create(cfg, o.Provider); + byte[] data = "Hello \r\n There \r\n You"u8.ToArray(); + Stream d = new MemoryStream(data); + MemoryStream result = new MemoryStream(); + bool modified = processor.Run(d, result); + + Assert.True(modified); + string outcomeString = Encoding.UTF8.GetString(result.ToArray()); + Assert.Equal("Hello \r\n You", outcomeString); + } + + [Theory(DisplayName = nameof(VerifyWhitespaceHandlerConsumeWholeLine))] + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + [InlineData(true, true, true)] + public void VerifyWhitespaceHandlerConsumeWholeLine(bool trim, bool trimForward, bool trimBackward) + { + MockOperation o = new MockOperation( + null, + (state, length, ref position, token) => + { + state.WhitespaceHandler(ref length, ref position, true, trim, trimForward, trimBackward); + return 0; + }, + true, + "There"u8.ToArray()); + + EngineConfig cfg = new EngineConfig(_logger, new VariableCollection()); + IProcessor processor = Processor.Create(cfg, o.Provider); + byte[] data = "Hello \r\n There \r\n You"u8.ToArray(); + Stream d = new MemoryStream(data); + MemoryStream result = new MemoryStream(); + bool modified = processor.Run(d, result); + + Assert.True(modified); + string outcomeString = Encoding.UTF8.GetString(result.ToArray()); + Assert.Equal("Hello \r\n You", outcomeString); + } + + [Theory(DisplayName = nameof(VerifyWhitespaceHandlerTrim))] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void VerifyWhitespaceHandlerTrim(bool trimForward, bool trimBackward) + { + MockOperation o = new MockOperation( + null, + (state, length, ref position, token) => + { + state.WhitespaceHandler(ref length, ref position, false, true, trimForward, trimBackward); + return 0; + }, + true, + "There"u8.ToArray()); + + EngineConfig cfg = new EngineConfig(_logger, new VariableCollection()); + IProcessor processor = Processor.Create(cfg, o.Provider); + byte[] data = "Hello \r\n There \r\n You"u8.ToArray(); + Stream d = new MemoryStream(data); + MemoryStream result = new MemoryStream(); + bool modified = processor.Run(d, result); + + Assert.True(modified); + result.Position = 0; + string outcomeString = Encoding.UTF8.GetString(result.ToArray()); + Assert.Equal("Hello \r\n You", outcomeString); + } + + [Fact(DisplayName = nameof(VerifyWhitespaceHandlerTrimForwardButNotBack))] + public void VerifyWhitespaceHandlerTrimForwardButNotBack() + { + MockOperation o = new MockOperation( + null, + (state, length, ref position, token) => + { + state.WhitespaceHandler(ref length, ref position, false, false, true, false); + return 0; + }, + true, + "There"u8.ToArray()); + + EngineConfig cfg = new EngineConfig(_logger, new VariableCollection()); + IProcessor processor = Processor.Create(cfg, o.Provider); + byte[] data = "Hello \r\n There \r\n You"u8.ToArray(); + Stream d = new MemoryStream(data); + MemoryStream result = new MemoryStream(); + bool modified = processor.Run(d, result); + + Assert.True(modified); + string outcomeString = Encoding.UTF8.GetString(result.ToArray()); + Assert.Equal("Hello \r\n You", outcomeString); + } + + [Fact(DisplayName = nameof(VerifyWhitespaceHandlerTrimBackButNotForward))] + public void VerifyWhitespaceHandlerTrimBackButNotForward() + { + MockOperation o = new MockOperation( + null, + (state, length, ref position, token) => + { + state.WhitespaceHandler(ref length, ref position, false, false, false, true); + return 0; + }, + true, + "There"u8.ToArray()); + + EngineConfig cfg = new EngineConfig(_logger, new VariableCollection()); + IProcessor processor = Processor.Create(cfg, o.Provider); + byte[] data = "Hello \r\n There \r\n You"u8.ToArray(); + Stream d = new MemoryStream(data); + MemoryStream result = new MemoryStream(); + bool modified = processor.Run(d, result); + + Assert.True(modified); + string outcomeString = Encoding.UTF8.GetString(result.ToArray()); + Assert.Equal("Hello \r\n \r\n You", outcomeString); + } + + [Fact(DisplayName = nameof(VerifyWhitespaceHandlerTrimBackAndForward))] + public void VerifyWhitespaceHandlerTrimBackAndForward() + { + MockOperation o = new MockOperation( + null, + (state, length, ref position, token) => + { + state.WhitespaceHandler(ref length, ref position, false, false, true, true); + return 0; + }, + true, + "There"u8.ToArray()); + + EngineConfig cfg = new EngineConfig(_logger, new VariableCollection()); + IProcessor processor = Processor.Create(cfg, o.Provider); + byte[] data = "Hello \r\n There \r\n You"u8.ToArray(); + Stream d = new MemoryStream(data); + MemoryStream result = new MemoryStream(); + bool modified = processor.Run(d, result); + + Assert.True(modified); + string outcomeString = Encoding.UTF8.GetString(result.ToArray()); + Assert.Equal("Hello \r\n You", outcomeString); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.CStyleEvaluator.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.CStyleEvaluator.cs new file mode 100644 index 000000000000..ee8cd908c399 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.CStyleEvaluator.cs @@ -0,0 +1,1667 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + [Fact(DisplayName = nameof(VerifyIfEndifTrueCondition))] + public void VerifyIfEndifTrueCondition() + { + string value = @"Hello + #if (VALUE) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection { ["VALUE"] = true }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseEndifTrueCondition))] + public void VerifyIfElseEndifTrueCondition() + { + string value = @"Hello + #if (VALUE) +value + #else +other + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection { ["VALUE"] = true }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseEndifTrueConditionContainsTabs))] + public void VerifyIfElseEndifTrueConditionContainsTabs() + { + string value = @"Hello + #if " + "\t" + @" (VALUE) +value + #else +other + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection { ["VALUE"] = true }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseEndifTrueConditionQuotedString))] + public void VerifyIfElseEndifTrueConditionQuotedString() + { + string value = @"Hello + #if (""Hello" + "\t" + @"There"" == VALUE) +value + #else +other + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection { ["VALUE"] = "Hello\tThere" }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseEndifTrueConditionQuotedString))] + public void VerifyIfElseEndifTrueConditionUnquotedString() + { + string value = @"Hello + #if (Foo == VALUE) +value + #else +other + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + VariableCollection vc = new VariableCollection { ["VALUE"] = "Foo", ["Foo"] = "Foo" }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseEndifTrueConditionLiteralFirst))] + public void VerifyIfElseEndifTrueConditionLiteralFirst() + { + string value = @"Hello + #if (3 > VALUE) +value + #else +other + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseEndifTrueConditionLiteralAgainst))] + public void VerifyIfElseEndifTrueConditionLiteralAgainst() + { + string value = @"Hello + #if(3 > VALUE) +value + #else +other + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseEndifTrueConditionAgainstIf))] + public void VerifyIfElseEndifTrueConditionAgainstIf() + { + string value = @"Hello + #if(VALUE) +value + #else +other + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection { ["VALUE"] = true }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseEndifFalseCondition))] + public void VerifyIfElseEndifFalseCondition() + { + string value = @"Hello + #if VALUE +value + #else +other + #endif +There"; + string expected = @"Hello +other +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection { ["VALUE"] = false }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifEndifTrueFalseCondition))] + public void VerifyIfElseifEndifTrueFalseCondition() + { + string value = @"Hello + #if (VALUE) +value + #elseif (VALUE2) +other + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = true, + ["VALUE2"] = false + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElifEndifTrueFalseCondition))] + public void VerifyIfElifEndifTrueFalseCondition() + { + string value = @"Hello + #if (VALUE) +value + #elif (VALUE2) +other + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = true, + ["VALUE2"] = false + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifEndifTrueTrueCondition))] + public void VerifyIfElseifEndifTrueTrueCondition() + { + string value = @"Hello + #if (VALUE) +value + #elseif (VALUE2) +other + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = true, + ["VALUE2"] = true + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElifEndifTrueTrueCondition))] + public void VerifyIfElifEndifTrueTrueCondition() + { + string value = @"Hello + #if (VALUE) +value + #elif (VALUE2) +other + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = true, + ["VALUE2"] = true + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifEndifFalseTrueCondition))] + public void VerifyIfElseifEndifFalseTrueCondition() + { + string value = @"Hello + #if (VALUE) +value + #elseif (VALUE2) +other + #endif +There"; + string expected = @"Hello +other +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = false, + ["VALUE2"] = true + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifElseEndifTrueFalseCondition))] + public void VerifyIfElseifElseEndifTrueFalseCondition() + { + string value = @"Hello + #if (VALUE) +value + #elseif (VALUE2) +other + #else +other2 + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = true, + ["VALUE2"] = false + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifElseEndifFalseTrueCondition))] + public void VerifyIfElseifElseEndifFalseTrueCondition() + { + string value = @"Hello + #if (VALUE) +value + #elseif (VALUE2) +other + #else +other2 + #endif +There"; + string expected = @"Hello +other +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = false, + ["VALUE2"] = true + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifElseEndifFalseFalseCondition))] + public void VerifyIfElseifElseEndifFalseFalseCondition() + { + string value = @"Hello + #if VALUE +value + #elseif VALUE2 +other + #else +other2 + #endif +There"; + string expected = @"Hello +other2 +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = false, + ["VALUE2"] = false + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyNestedIfTrueTrue))] + public void VerifyNestedIfTrueTrue() + { + string value = @"Hello + #if (VALUE) + #if (VALUE2) +value + #else +other + #endif + #else +other2 + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = true, + ["VALUE2"] = true + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifElseifElseEndifTrueTrueCondition))] + public void VerifyIfElseifElseifElseEndifTrueTrueCondition() + { + string value = @"Hello + #if (VALUE) +value + #elseif (VALUE2) +other + #else +other2 + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = true, + ["VALUE2"] = true + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifElseifElseEndifTrueFalseCondition))] + public void VerifyIfElseifElseifElseEndifTrueFalseCondition() + { + string value = @"Hello + #if (VALUE) +value + #elseif (VALUE2) +other + #else +other2 + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = true, + ["VALUE2"] = false + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifElseifElseEndifFalseTrueCondition))] + public void VerifyIfElseifElseifElseEndifFalseTrueCondition() + { + string value = @"Hello + #if (VALUE) +value + #elseif (VALUE2) +other + #else +other2 + #endif +There"; + string expected = @"Hello +other +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = false, + ["VALUE2"] = true + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifElseifElseEndifFalseFalseCondition))] + public void VerifyIfElseifElseifElseEndifFalseFalseCondition() + { + string value = @"Hello + #if VALUE +value + #elseif VALUE2 +other + #else +other2 + #endif +There"; + string expected = @"Hello +other2 +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = false, + ["VALUE2"] = false + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifElseifEndifTrueFalseFalseCondition))] + public void VerifyIfElseifElseifEndifTrueFalseFalseCondition() + { + string value = @"Hello + #if (VALUE) +value + #elseif (VALUE2) +other + #elseif (VALUE3) +other2 + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = true, + ["VALUE2"] = false, + ["VALUE3"] = false + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifElseifEndifFalseTrueFalseCondition))] + public void VerifyIfElseifElseifEndifFalseTrueFalseCondition() + { + string value = @"Hello + #if (VALUE) +value + #elseif (VALUE2) +other + #elseif (VALUE3) +other2 + #endif +There"; + string expected = @"Hello +other +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = false, + ["VALUE2"] = true, + ["VALUE3"] = false + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseifElseifEndifFalseFalseTrueCondition))] + public void VerifyIfElseifElseifEndifFalseFalseTrueCondition() + { + string value = @"Hello + #if (VALUE) +value + #elseif (VALUE2) +other + #elseif (VALUE3) +other2 + #endif +There"; + string expected = @"Hello +other2 +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = false, + ["VALUE2"] = false, + ["VALUE3"] = true + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueEqualsCondition))] + public void VerifyIfEndifTrueEqualsCondition() + { + string value = @"Hello + #if (VALUE == 2) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2L + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueNotEqualsCondition))] + public void VerifyIfEndifTrueNotEqualsCondition() + { + string value = @"Hello + #if (VALUE != 3) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 4 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueGreaterThanCondition))] + public void VerifyIfEndifTrueGreaterThanCondition() + { + string value = @"Hello + #if (VALUE > 3) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 4 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifOperandStealing))] + public void VerifyIfEndifOperandStealing() + { + string value = @"Hello + #if ((VALUE == 3) == true) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 3L + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifOperandStealing2))] + public void VerifyIfEndifOperandStealing2() + { + string value = @"Hello + #if (!VALUE == true) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = false + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueGreaterThanOrEqualToCondition))] + public void VerifyIfEndifTrueGreaterThanOrEqualToCondition() + { + string value = @"Hello + #if (VALUE >= 3) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 3 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifFalseGreaterThanOrEqualToCondition))] + public void VerifyIfEndifFalseGreaterThanOrEqualToCondition() + { + string value = @"Hello + #if (VALUE >= 3) +value + #endif +There"; + string expected = @"Hello +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueLessThanCondition))] + public void VerifyIfEndifTrueLessThanCondition() + { + string value = @"Hello + #if (VALUE < 3) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueLessThanOrEqualToCondition))] + public void VerifyIfEndifTrueLessThanOrEqualToCondition() + { + string value = @"Hello + #if (VALUE <= 3) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 3 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueNotCondition))] + public void VerifyIfEndifTrueNotCondition() + { + string value = @"Hello + #if (!VALUE) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = false + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueNotNotCondition))] + public void VerifyIfEndifTrueNotNotCondition() + { + string value = @"Hello + #if (!!VALUE) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = true + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueAndCondition))] + public void VerifyIfEndifTrueAndCondition() + { + string value = @"Hello + #if (VALUE < 3 && VALUE > 0) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueXorCondition))] + public void VerifyIfEndifTrueXorCondition() + { + string value = @"Hello + #if (VALUE < 3 ^ VALUE == 7) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueAndAndCondition))] + public void VerifyIfEndifTrueAndAndCondition() + { + string value = @"Hello + #if (VALUE < 3 && VALUE < 4 && VALUE < 5) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueOrCondition))] + public void VerifyIfEndifTrueOrCondition() + { + string value = @"Hello + #if (VALUE == 6 || VALUE < 3) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueOrOrCondition))] + public void VerifyIfEndifTrueOrOrCondition() + { + string value = @"Hello + #if (VALUE == 6 || VALUE == 7 || VALUE < 3) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueOrAndCondition))] + public void VerifyIfEndifTrueOrAndCondition() + { + string value = @"Hello + #if (VALUE == 6 || (VALUE != 7 && VALUE < 3)) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueAndOrCondition))] + public void VerifyIfEndifTrueAndOrCondition() + { + string value = @"Hello + #if ((VALUE != 7 && VALUE < 3) || VALUE == 6) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueBitwiseAndEqualsCondition))] + public void VerifyIfEndifTrueBitwiseAndEqualsCondition() + { + string value = @"Hello + #if ((VALUE & 0xFFFF) == 2) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueBitwiseOrEqualsCondition))] + public void VerifyIfEndifTrueBitwiseOrEqualsCondition() + { + string value = @"Hello + #if (VALUE | 0xFFFD == 0xFFFF) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueShlCondition))] + public void VerifyIfEndifTrueShlCondition() + { + string value = @"Hello + #if (VALUE << 1 == 8) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 4 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueShrCondition))] + public void VerifyIfEndifTrueShrCondition() + { + string value = @"Hello + #if (VALUE >> 1 == 2) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 4 + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfEndifTrueGroupedCondition))] + public void VerifyIfEndifTrueGroupedCondition() + { + string value = @"Hello + #if ((VALUE == 2) && (VALUE2 == 3)) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = 2L, + ["VALUE2"] = 3L + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseEndifConditionUsesNull))] + public void VerifyIfElseEndifConditionUsesNull() + { + string value = @"Hello + #if (VALUE2 == null) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseEndifConditionUsesFalse))] + public void VerifyIfElseEndifConditionUsesFalse() + { + string value = @"Hello + #if (!false) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Theory(DisplayName = nameof(VerifyIfElseEndifConditionUsesDouble))] + [InlineData("", "Hello\r\n#if (1.2 < 2.5)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("invariant", "Hello\r\n#if (1.2 < 2.5)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("pl-PL", "Hello\r\n#if (1.2 < 2.5)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("ru-RU", "Hello\r\n#if (1.2 < 2.5)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("ru-RU", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("tr-TR", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("tr-TR", "Hello\r\n#if (2,5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("en-US", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("en-US", "Hello\r\n#if (2,5 < 3,5)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nThere")] + [InlineData("en-GB", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("hr-HR", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("hi-IN", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("fr-CH", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("zh-CN", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("zh-SG", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("zh-TW", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("zh-CHS", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + [InlineData("zh-CHT", "Hello\r\n#if (2.5 < 25)\r\nvalue\r\n#endif\r\nThere", "Hello\r\nvalue\r\nThere")] + public void VerifyIfElseEndifConditionUsesDouble(string culture, string value, string expected) + { + if (!string.IsNullOrEmpty(culture)) + { + CultureInfo.CurrentCulture = culture == "invariant" ? CultureInfo.InvariantCulture : new CultureInfo(culture); + } + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfElseEndifConditionUsesFalsePositiveHex))] + public void VerifyIfElseEndifConditionUsesFalsePositiveHex() + { + string value = @"Hello + #if (0xChicken == null) +value + #endif +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyIfNoCondition))] + public void VerifyIfNoCondition() + { + string value = @"Hello + #if +value + #endif +There"; + string expected = @"Hello +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyConditionAtEnd))] + public void VerifyConditionAtEnd() + { + string value = @"Hello + #if (1.2 < 2.5)"; + string expected = @"Hello +"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyExcludeNestedCondition))] + public void VerifyExcludeNestedCondition() + { + string value = @"Hello + #if false + #if true + #if true + #endif + #endif + #if true + #if true + #endif + #endif + #endif"; + string expected = @"Hello +"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyExcludeNestedConditionInNonTakenBranch))] + public void VerifyExcludeNestedConditionInNonTakenBranch() + { + string value = @"Hello + #if true + #else + #if true + #if true + #endif + #endif + #if true + #if true + #endif + #endif + #endif +There"; + string expected = @"Hello +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyEmitStrayToken))] + public void VerifyEmitStrayToken() + { + string value = @"Hello + #endif + #else + #elseif foo"; + string expected = @"Hello + #endif + #else + #elseif foo"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + + //Changes should be made + processor.Run(input, output, 28); + //Override the change indication - the stream was technically mutated in this case, + // pretend it's false because the inputs and outputs are the same + Verify(Encoding.UTF8, output, false, value, expected); + } + + [Theory] + [InlineData("foo", true)] + [InlineData("foo", false)] + [InlineData("def", true)] + [InlineData("def", false)] + // dotnet new crashes if template contains #ifdef + // https://github.com/dotnet/templating/issues/3085 + public void VerifyMisstypedIfTokenDoesntCrash(string varName, bool varValue) + { + string value = @"#ifdef +GAGA +#endif"; + string expected = varName == "def" && varValue ? @"GAGA +" : string.Empty; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection() + { + [varName] = varValue + }; + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + processor.Run(input, output, 999); + Verify(Encoding.UTF8, output, true, value, expected); + } + + [Fact] + public void VerifyTheScopeAfterLongConditionIsNotLost() + { + //the buffer size is selected in the way so after processing the condition, the buffer is in the end and should be read + //for trie, the last know position is 4 and it misses the buffer window after condition processing. + string value = """ + #ifdef false + Long text, long text, long text, long text, long text, long text, long text, long text, long text, long text, long text, long text, long tex + #endif + Long test after condition, Long test after condition,Long test after condition, Long test after condition,Long test after condition + """; + string expected = "Long test after condition, Long test after condition,Long test after condition, Long test after condition,Long test after condition"; + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + using MemoryStream input = new(valueBytes); + using MemoryStream output = new(); + + VariableCollection vc = new(); + IProcessor processor = SetupCStyleNoCommentsProcessor(vc); + processor.Run(input, output, 20); + Verify(Encoding.UTF8, output, true, value, expected); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.HamlLineCommenting.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.HamlLineCommenting.cs new file mode 100644 index 000000000000..e16eab1cd43e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.HamlLineCommenting.cs @@ -0,0 +1,275 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + [Fact(DisplayName = nameof(VerifyBasicHamlCommentHandling))] + public void VerifyBasicHamlCommentHandling() + { + string originalValue = @"Start +-#-#if (CLAUSE) +-#-# Actual Comment +-# content +-#-#endif +-# end comment +-#-# end quad comment +End"; + string expectedValue = @"Start +-# Actual Comment + content +-# end comment +-#-# end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["CLAUSE"] = true, + }; + + IProcessor processor = SetupHamlLineCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + string originalValueEndifChanged = @"Start +-#-#if (CLAUSE) +-#-# Actual Comment +-# content +-#endif +-# end comment +-#-# end quad comment +End"; + RunAndVerify(originalValueEndifChanged, expectedValue, processor, 9999); + + string originalNoCommentRemoval = @"Start +-#if (CLAUSE) +-#-# Actual Comment +-# content +-#-#endif +-# end comment +-#-# end quad comment +End"; + string expectedValueNoCommentRemoval = @"Start +-#-# Actual Comment +-# content +-# end comment +-#-# end quad comment +End"; + RunAndVerify(originalNoCommentRemoval, expectedValueNoCommentRemoval, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyHamlCommentRemovalForEachClauseNoEmbedding))] + public void VerifyHamlCommentRemovalForEachClauseNoEmbedding() + { + string originalValue = @"Start +-#-#if (IF) +-# content: if +-#-# Comment: if +-# content: if part 2 +-#-# Comment: if part 2 +-#-#elseif (ELSEIF) +-#-# Comment: elseif +-# content: elseif +-#-# Comment: elseif part 2 +-# content: elseif part 2 +-#-#else +-# content: else +-#-# Comment: else +-#-# Comment: else 2 +-# content: else 2 +-#-#endif +-# end comment +-#-# end quad comment +End"; + string ifExpectedValue = @"Start + content: if +-# Comment: if + content: if part 2 +-# Comment: if part 2 +-# end comment +-#-# end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["IF"] = true, + }; + IProcessor processor = SetupHamlLineCommentsProcessor(vc); + RunAndVerify(originalValue, ifExpectedValue, processor, 9999); + + string elseIfExpectedValue = @"Start +-# Comment: elseif + content: elseif +-# Comment: elseif part 2 + content: elseif part 2 +-# end comment +-#-# end quad comment +End"; + vc = new VariableCollection + { + ["IF"] = false, + ["ELSEIF"] = true + }; + processor = SetupHamlLineCommentsProcessor(vc); + RunAndVerify(originalValue, elseIfExpectedValue, processor, 9999); + + string elseExpectedValue = @"Start + content: else +-# Comment: else +-# Comment: else 2 + content: else 2 +-# end comment +-#-# end quad comment +End"; + vc = new VariableCollection + { + ["IF"] = false, + ["ELSEIF"] = false + }; + processor = SetupHamlLineCommentsProcessor(vc); + RunAndVerify(originalValue, elseExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyHamlStyleCommentRemovalWithNestedClause))] + public void VerifyHamlStyleCommentRemovalWithNestedClause() + { + string originalValue = @"Start +-#-#if (OUTER_IF) + -#-# Comment: outer if + -#content outer if + -#-#if (INNER_IF) + -#-# Comment: inner if + -#content: inner if + -#-#endif +-#-#endif +-# end comment +-#-# end quad comment +End"; + string outerTrueInnerFalseExpectedValue = @"Start + -# Comment: outer if + content outer if +-# end comment +-#-# end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false + }; + IProcessor processor = SetupHamlLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerFalseExpectedValue, processor, 9999); + + string outerTrueInnerTrueExpectedValue = @"Start + -# Comment: outer if + content outer if + -# Comment: inner if + content: inner if +-# end comment +-#-# end quad comment +End"; + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true + }; + processor = SetupHamlLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerTrueExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyHamlStyleCommentRemovalNestedDoesntRemove))] + public void VerifyHamlStyleCommentRemovalNestedDoesntRemove() + { + string originalValue = @"Start +-#-#if (OUTER_IF) + -#-# Comment: outer if + -#content outer if + -#-#if (INNER_IF) + -#-# Comment: inner if + -#content: inner if + -#-#endif +-#-#endif +-# end comment +-#-# end quad comment +End"; + string outerTrueInnerFalseExpectedValue = @"Start + -# Comment: outer if + content outer if +-# end comment +-#-# end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false + }; + IProcessor processor = SetupHamlLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerFalseExpectedValue, processor, 9999); + + string outerTrueInnerTrueExpectedValue = @"Start + -# Comment: outer if + content outer if + -# Comment: inner if + content: inner if +-# end comment +-#-# end quad comment +End"; + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true + }; + processor = SetupHamlLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerTrueExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyHamlSignMixedConditionalsThreeLevelEmbedding))] + public void VerifyHamlSignMixedConditionalsThreeLevelEmbedding() + { + string originalValue = @"Lead content +-#-#if (LEVEL_1_IF) +-# content: level-1 if +-# -#-#if (LEVEL_2_IF) +-# -# content: level-2 if +-# -# -#-#if (LEVEL_3_IF) +-# -# -# content: level-3 if +-# -# -#-#elseif (LEVEL_3_ELSEIF) +-# -# -# content: level-3 elseif +-# -# -#-#else +-# -# -# content: level-3 else +-# -# -#-#endif +-# -#-#elseif (LEVEL_2_ELSEIF) +-# -# content: level-2 elseif +-# -#-#else +-# -# content: level-2 else +-# -#-#endif +-#-#elseif true +-# content: level-1 elseif +-#-#else +-# content: level-1 else +-#-#endif +-# commented trailing content +moar trailing content"; + + // outer if & inner if get uncommented + string expectedValue = @"Lead content + content: level-1 if + content: level-2 if + content: level-3 if +-# commented trailing content +moar trailing content"; + + VariableCollection vc = new VariableCollection + { + ["LEVEL_1_IF"] = true, + ["LEVEL_2_IF"] = true, + ["LEVEL_3_IF"] = true, + ["LEVEL_3_ELSEIF"] = true, // irrelevant + ["LEVEL_2_ELSEIF"] = true, // irrelevant + }; + + IProcessor processor = SetupHamlLineCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.HashCommentWithCStyleEvaluator.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.HashCommentWithCStyleEvaluator.cs new file mode 100644 index 000000000000..a21278ffd2e2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.HashCommentWithCStyleEvaluator.cs @@ -0,0 +1,275 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + [Fact(DisplayName = nameof(VerifyBasicHashCommentHandling))] + public void VerifyBasicHashCommentHandling() + { + string originalValue = @"Start +##if (CLAUSE) +## Actual Comment +# content +##endif +# end comment +## end quad comment +End"; + string expectedValue = @"Start +# Actual Comment + content +# end comment +## end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["CLAUSE"] = true, + }; + + IProcessor processor = SetupHashSignLineCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + string originalValueEndifChanged = @"Start +##if (CLAUSE) +## Actual Comment +# content +#endif +# end comment +## end quad comment +End"; + RunAndVerify(originalValueEndifChanged, expectedValue, processor, 9999); + + string originalNoCommentRemoval = @"Start +#if (CLAUSE) +## Actual Comment +# content +##endif +# end comment +## end quad comment +End"; + string expectedValueNoCommentRemoval = @"Start +## Actual Comment +# content +# end comment +## end quad comment +End"; + RunAndVerify(originalNoCommentRemoval, expectedValueNoCommentRemoval, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyHashStyleCommentRemovalForEachClauseNoEmbedding))] + public void VerifyHashStyleCommentRemovalForEachClauseNoEmbedding() + { + string originalValue = @"Start +##if (IF) +# content: if +## Comment: if +# content: if part 2 +## Comment: if part 2 +##elseif (ELSEIF) +## Comment: elseif +# content: elseif +## Comment: elseif part 2 +# content: elseif part 2 +##else +# content: else +## Comment: else +## Comment: else 2 +# content: else 2 +##endif +# end comment +## end quad comment +End"; + string ifExpectedValue = @"Start + content: if +# Comment: if + content: if part 2 +# Comment: if part 2 +# end comment +## end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["IF"] = true, + }; + IProcessor processor = SetupHashSignLineCommentsProcessor(vc); + RunAndVerify(originalValue, ifExpectedValue, processor, 9999); + + string elseIfExpectedValue = @"Start +# Comment: elseif + content: elseif +# Comment: elseif part 2 + content: elseif part 2 +# end comment +## end quad comment +End"; + vc = new VariableCollection + { + ["IF"] = false, + ["ELSEIF"] = true + }; + processor = SetupHashSignLineCommentsProcessor(vc); + RunAndVerify(originalValue, elseIfExpectedValue, processor, 9999); + + string elseExpectedValue = @"Start + content: else +# Comment: else +# Comment: else 2 + content: else 2 +# end comment +## end quad comment +End"; + vc = new VariableCollection + { + ["IF"] = false, + ["ELSEIF"] = false + }; + processor = SetupHashSignLineCommentsProcessor(vc); + RunAndVerify(originalValue, elseExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyHashStyleCommentRemovalWithNestedClause))] + public void VerifyHashStyleCommentRemovalWithNestedClause() + { + string originalValue = @"Start +##if (OUTER_IF) + ## Comment: outer if + #content outer if + ##if (INNER_IF) + ## Comment: inner if + #content: inner if + ##endif +##endif +# end comment +## end quad comment +End"; + string outerTrueInnerFalseExpectedValue = @"Start + # Comment: outer if + content outer if +# end comment +## end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false + }; + IProcessor processor = SetupHashSignLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerFalseExpectedValue, processor, 9999); + + string outerTrueInnerTrueExpectedValue = @"Start + # Comment: outer if + content outer if + # Comment: inner if + content: inner if +# end comment +## end quad comment +End"; + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true + }; + processor = SetupHashSignLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerTrueExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyHashStyleCommentRemovalNestedDoesntRemove))] + public void VerifyHashStyleCommentRemovalNestedDoesntRemove() + { + string originalValue = @"Start +##if (OUTER_IF) + ## Comment: outer if + #content outer if + ##if (INNER_IF) + ## Comment: inner if + #content: inner if + ##endif +##endif +# end comment +## end quad comment +End"; + string outerTrueInnerFalseExpectedValue = @"Start + # Comment: outer if + content outer if +# end comment +## end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false + }; + IProcessor processor = SetupHashSignLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerFalseExpectedValue, processor, 9999); + + string outerTrueInnerTrueExpectedValue = @"Start + # Comment: outer if + content outer if + # Comment: inner if + content: inner if +# end comment +## end quad comment +End"; + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true + }; + processor = SetupHashSignLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerTrueExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyHashSignMixedConditionalsThreeLevelEmbedding))] + public void VerifyHashSignMixedConditionalsThreeLevelEmbedding() + { + string originalValue = @"Lead content +##if (LEVEL_1_IF) +# content: level-1 if +# ##if (LEVEL_2_IF) +# # content: level-2 if +# # ##if (LEVEL_3_IF) +# # # content: level-3 if +# # ##elseif (LEVEL_3_ELSEIF) +# # # content: level-3 elseif +# # ##else +# # # content: level-3 else +# # ##endif +# ##elseif (LEVEL_2_ELSEIF) +# # content: level-2 elseif +# ##else +# # content: level-2 else +# ##endif +##elseif true +# content: level-1 elseif +##else +# content: level-1 else +##endif +# commented trailing content +moar trailing content"; + + // outer if & inner if get uncommented + string expectedValue = @"Lead content + content: level-1 if + content: level-2 if + content: level-3 if +# commented trailing content +moar trailing content"; + + VariableCollection vc = new VariableCollection + { + ["LEVEL_1_IF"] = true, + ["LEVEL_2_IF"] = true, + ["LEVEL_3_IF"] = true, + ["LEVEL_3_ELSEIF"] = true, // irrelevant + ["LEVEL_2_ELSEIF"] = true, // irrelevant + }; + + IProcessor processor = SetupHashSignLineCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.InlineMarkup.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.InlineMarkup.cs new file mode 100644 index 000000000000..d921fa4bcdb1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.InlineMarkup.cs @@ -0,0 +1,385 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Expressions.MSBuild; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class InlineMarkupConditionalTests : TestBase, IClassFixture + { + private readonly ILogger _logger; + + public InlineMarkupConditionalTests(TestLoggerFactory testLoggerFactory) + { + _logger = testLoggerFactory.CreateLogger(); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupTrue))] + public void VerifyInlineMarkupTrue() + { + string originalValue = @" + + + + + + +"; + + string expectedValue = @" + + + + + + +"; + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = true, + ["SECOND_IF"] = true + }; + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupSelfClosedFalse))] + public void VerifyInlineMarkupSelfClosedFalse() + { + string originalValue = @" + + + + + + +"; + + string expectedValue = @" + + + + + +"; + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = false, + ["SECOND_IF"] = true + }; + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupElementWithChildrenFalse))] + public void VerifyInlineMarkupElementWithChildrenFalse() + { + string originalValue = @" + + + + + + +"; + + string expectedValue = @" + +"; + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = true, + ["SECOND_IF"] = false + }; + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupFalse))] + public void VerifyInlineMarkupFalse() + { + string originalValue = @" + + + + + + +"; + + string expectedValue = @" +"; + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = false, + ["SECOND_IF"] = false + }; + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupExpandedConditions1))] + public void VerifyInlineMarkupExpandedConditions1() + { + string originalValue = @" + +"; + + string expectedValue = @" + +"; + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = false, + ["SECOND_IF"] = false + }; + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupExpandedConditions2))] + public void VerifyInlineMarkupExpandedConditions2() + { + string originalValue = @" + +"; + + string expectedValue = @" + +"; + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = false, + ["SECOND_IF"] = false + }; + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupExpandedConditionsEscaping))] + public void VerifyInlineMarkupExpandedConditionsEscaping() + { + string originalValue = @" + +"; + + string expectedValue = @" + +"; + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = ">< &' \"" + }; + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupExpandedConditionsVersion))] + public void VerifyInlineMarkupExpandedConditionsVersion() + { + string originalValue = @" + +"; + + string expectedValue = @" + +"; + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = "1.2.3" + }; + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupExpandedConditionsUndefinedSymbolEmitsOriginal))] + public void VerifyInlineMarkupExpandedConditionsUndefinedSymbolEmitsOriginal() + { + string originalValue = @" + +"; + + string expectedValue = @" + +"; + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999, true); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupExpandedConditionsUndefinedSymbolEmitsOriginal2))] + public void VerifyInlineMarkupExpandedConditionsUndefinedSymbolEmitsOriginal2() + { + string originalValue = @"$(PaketToolsPath)paket.exe"; + + string expectedValue = @"$(PaketToolsPath)paket.exe"; + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999, true); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupExpandedConditionsNumerics))] + public void VerifyInlineMarkupExpandedConditionsNumerics() + { + string originalValue = @" + +"; + + string expectedValue = @" + +"; + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = ">< &' \"" + }; + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupExpandedConditions3))] + public void VerifyInlineMarkupExpandedConditions3() + { + string originalValue = @" + +"; + + string expectedValue = @" + +"; + VariableCollection vc = new VariableCollection + { + ["Configuration"] = "Debug", + ["Platform"] = "AnyCPU" + }; + IProcessor processor = SetupXmlPlusMsBuildProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + originalValue = @" + +"; + + expectedValue = @" + +"; + + RunAndVerify(originalValue, expectedValue, processor, 9999); + + originalValue = @" + +"; + + expectedValue = @" +"; + + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupRejectGetsProcessed))] + public void VerifyInlineMarkupRejectGetsProcessed() + { + string originalValue = @" + +"; + + string expectedValue = @" + +"; + VariableCollection vc = new VariableCollection { }; + IProcessor processor = SetupXmlPlusMsBuildProcessorAndReplacement(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyInlineMarkupRejectGetsProcessedWithLookaround))] + public void VerifyInlineMarkupRejectGetsProcessedWithLookaround() + { + string originalValue = @" + +"; + + string expectedValue = @" + +"; + VariableCollection vc = new VariableCollection { }; + IProcessor processor = SetupXmlPlusMsBuildProcessorAndReplacementWithLookaround(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + private IProcessor SetupXmlPlusMsBuildProcessor(IVariableCollection vc) + { + EngineConfig cfg = new EngineConfig(_logger, vc, "$({0})"); + return Processor.Create(cfg, new InlineMarkupConditional( + new MarkupTokens( + "<".TokenConfig(), + "".TokenConfig(), + "/>".TokenConfig(), + "Condition=\"".TokenConfig(), + "\"".TokenConfig(), + "".TokenConfig()), + true, + true, + MSBuildStyleEvaluatorDefinition.Evaluate, + "$({0})", + null, + true)); + } + + private IProcessor SetupXmlPlusMsBuildProcessorAndReplacement(IVariableCollection vc) + { + EngineConfig cfg = new EngineConfig(_logger, vc, "$({0})"); + return Processor.Create( + cfg, + new InlineMarkupConditional( + new MarkupTokens( + "<".TokenConfig(), + "".TokenConfig(), + "/>".TokenConfig(), + "Condition=\"".TokenConfig(), + "\"".TokenConfig(), + "".TokenConfig()), + true, + true, + MSBuildStyleEvaluatorDefinition.Evaluate, + "$({0})", + null, + true), + new Replacement("ReplaceMe".TokenConfig(), "I've been replaced", null, true)); + } + + private IProcessor SetupXmlPlusMsBuildProcessorAndReplacementWithLookaround(IVariableCollection vc) + { + EngineConfig cfg = new EngineConfig(_logger, vc, "$({0})"); + return Processor.Create( + cfg, + new InlineMarkupConditional( + new MarkupTokens( + "<".TokenConfig(), + "".TokenConfig(), + "/>".TokenConfig(), + "Condition=\"".TokenConfig(), + "\"".TokenConfig(), + "".TokenConfig()), + true, + true, + MSBuildStyleEvaluatorDefinition.Evaluate, + "$({0})", + null, + true), + new Replacement("ReplaceMe".TokenConfigBuilder().OnlyIfAfter("Condition=\"Exists("), "I've been replaced", null, true)); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.JsxBlockComments.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.JsxBlockComments.cs new file mode 100644 index 000000000000..c2429498b4a0 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.JsxBlockComments.cs @@ -0,0 +1,353 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + private const string JsxNoDefaultValue = @"Start +{/*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else + content: outer-else + {/*#if (INNER_IF_CLAUSE) + content: inner-if + #elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + #else + content: inner-else + #endif +#endif*/} +Trailing stuff +{/* trailing comment */}"; + + private const string JsxOuterIfDefaultValue = @"Start +{/*#if (OUTER_IF_CLAUSE) */} + content: outer-if +{/*#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else + content: outer-else + {/*#if (INNER_IF_CLAUSE) + content: inner-if + #elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + #else + content: inner-else + #endif +#endif*/} +Trailing stuff +{/* trailing comment */}"; + + private const string JsxOuterIfTrueExpectedValue = @"Start + content: outer-if +Trailing stuff +{/* trailing comment */}"; + + private const string JsxOuterElseifDefaultValue = @"Start +{/*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) */} + content: outer-elseif +{/*#else + content: outer-else + {/*#if (INNER_IF_CLAUSE) + content: inner-if + #elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + #else + content: inner-else + #endif +#endif*/} +Trailing stuff +{/* trailing comment */}"; + + // this one seems formatted weird, but correct. + private const string JsxOuterElseDefaultValue = @"Start +{/*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else */} + content: outer-else + {/*#if (INNER_IF_CLAUSE) + content: inner-if + #elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + #else + content: inner-else + #endif +#endif*/} +Trailing stuff +{/* trailing comment */}"; + + private const string JsxInnerIfDefaultValue = @"Start +{/*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else + content: outer-else + {/*#if (INNER_IF_CLAUSE) */} + content: inner-if + {/*#elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + #else + content: inner-else + #endif +#endif*/} +Trailing stuff +{/* trailing comment */}"; + + private const string JsxInnerElseifDefaultValue = @"Start +{/*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else + content: outer-else + {/*#if (INNER_IF_CLAUSE) + content: inner-if + {/*#elseif (INNER_ELSEIF_CLAUSE) */} + content: inner-elseif + {/*#else + content: inner-else + #endif +#endif*/} +Trailing stuff +{/* trailing comment */}"; + + private const string JsxInnerElseDefaultValue = @"Start +{/*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else + content: outer-else + {/*#if (INNER_IF_CLAUSE) + content: inner-if + #elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + {/*#else*/} + content: inner-else + #endif +#endif*/} +Trailing stuff +{/* trailing comment */}"; + + private const string JsxOuterElseifTrueExpectedValue = @"Start + content: outer-elseif +Trailing stuff +{/* trailing comment */}"; + + private const string JsxOuterElseHappensInnerIfTrueExpectedValue = @"Start + content: outer-else + content: inner-if +Trailing stuff +{/* trailing comment */}"; + + private const string JsxOuterElseHappensInnerElseifTrueExpectedValue = @"Start + content: outer-else + content: inner-elseif +Trailing stuff +{/* trailing comment */}"; + + private const string JsxOuterElseHappensInnerElseHappensExpectedValue = @"Start + content: outer-else + content: inner-else +Trailing stuff +{/* trailing comment */}"; + + private static readonly VariableCollection JsxOuterElseifTrueVariableCollection = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = true, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false + }; + + private static readonly VariableCollection JsxInnerIfTrueVariableCollection = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = true, + ["INNER_ELSEIF_CLAUSE"] = false + }; + + private static readonly VariableCollection JsxInnerElseIfTrueVariableCollection = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = true + }; + + private static readonly VariableCollection JsxAllFalse = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false + }; + + private static VariableCollection JsxOuterIfTrueVariableCollection => new VariableCollection + { + ["OUTER_IF_CLAUSE"] = true, + ["OUTER_ELSEIF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false + }; + + [Theory(DisplayName = nameof(VerifyJsxBlockCommentEmbeddedInElseTestOuterIfTrue))] + [InlineData(JsxNoDefaultValue, JsxOuterIfTrueExpectedValue)] + [InlineData(JsxOuterIfDefaultValue, JsxOuterIfTrueExpectedValue)] + [InlineData(JsxOuterElseifDefaultValue, JsxOuterIfTrueExpectedValue)] + [InlineData(JsxOuterElseDefaultValue, JsxOuterIfTrueExpectedValue)] + [InlineData(JsxInnerIfDefaultValue, JsxOuterIfTrueExpectedValue)] + [InlineData(JsxInnerElseifDefaultValue, JsxOuterIfTrueExpectedValue)] + [InlineData(JsxInnerElseDefaultValue, JsxOuterIfTrueExpectedValue)] + public void VerifyJsxBlockCommentEmbeddedInElseTestOuterIfTrue(string source, string expected) + { + IProcessor processor = SetupJsxBlockCommentsProcessor(JsxOuterIfTrueVariableCollection); + RunAndVerify(source, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(VerifyJsxBlockCommentEmbeddedInElseTestJsxOuterElseifTrueExpectedValue))] + [InlineData(JsxNoDefaultValue, JsxOuterElseifTrueExpectedValue)] + [InlineData(JsxOuterIfDefaultValue, JsxOuterElseifTrueExpectedValue)] + [InlineData(JsxOuterElseifDefaultValue, JsxOuterElseifTrueExpectedValue)] + [InlineData(JsxOuterElseDefaultValue, JsxOuterElseifTrueExpectedValue)] + [InlineData(JsxInnerIfDefaultValue, JsxOuterElseifTrueExpectedValue)] + [InlineData(JsxInnerElseifDefaultValue, JsxOuterElseifTrueExpectedValue)] + [InlineData(JsxInnerElseDefaultValue, JsxOuterElseifTrueExpectedValue)] + public void VerifyJsxBlockCommentEmbeddedInElseTestJsxOuterElseifTrueExpectedValue(string source, string expected) + { + IProcessor processor = SetupJsxBlockCommentsProcessor(JsxOuterElseifTrueVariableCollection); + RunAndVerify(source, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(VerifyJsxBlockCommentEmbeddedInElseTestJsxOuterElseHappensInnerIfTrueExpectedValue))] + [InlineData(JsxNoDefaultValue, JsxOuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(JsxOuterIfDefaultValue, JsxOuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(JsxOuterElseifDefaultValue, JsxOuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(JsxOuterElseDefaultValue, JsxOuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(JsxInnerIfDefaultValue, JsxOuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(JsxInnerElseifDefaultValue, JsxOuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(JsxInnerElseDefaultValue, JsxOuterElseHappensInnerIfTrueExpectedValue)] + public void VerifyJsxBlockCommentEmbeddedInElseTestJsxOuterElseHappensInnerIfTrueExpectedValue(string source, string expected) + { + IProcessor processor = SetupJsxBlockCommentsProcessor(JsxInnerIfTrueVariableCollection); + RunAndVerify(source, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(VerifyJsxBlockCommentEmbeddedInElseTestJsxOuterElseHappensInnerElseifTrueExpectedValue))] + [InlineData(JsxNoDefaultValue, JsxOuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(JsxOuterIfDefaultValue, JsxOuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(JsxOuterElseifDefaultValue, JsxOuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(JsxOuterElseDefaultValue, JsxOuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(JsxInnerIfDefaultValue, JsxOuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(JsxInnerElseifDefaultValue, JsxOuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(JsxInnerElseDefaultValue, JsxOuterElseHappensInnerElseifTrueExpectedValue)] + public void VerifyJsxBlockCommentEmbeddedInElseTestJsxOuterElseHappensInnerElseifTrueExpectedValue(string source, string expected) + { + IProcessor processor = SetupJsxBlockCommentsProcessor(JsxInnerElseIfTrueVariableCollection); + RunAndVerify(source, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(VerifyJsxBlockCommentEmbeddedInElseTestJsxOuterElseHappensInnerElseHappensExpectedValue))] + [InlineData(JsxNoDefaultValue, JsxOuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(JsxOuterIfDefaultValue, JsxOuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(JsxOuterElseifDefaultValue, JsxOuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(JsxOuterElseDefaultValue, JsxOuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(JsxInnerIfDefaultValue, JsxOuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(JsxInnerElseifDefaultValue, JsxOuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(JsxInnerElseDefaultValue, JsxOuterElseHappensInnerElseHappensExpectedValue)] + public void VerifyJsxBlockCommentEmbeddedInElseTestJsxOuterElseHappensInnerElseHappensExpectedValue(string source, string expected) + { + IProcessor processor = SetupJsxBlockCommentsProcessor(JsxAllFalse); + RunAndVerify(source, expected, processor, 9999); + } + + private const string JsxBasicValue = @"Start +{/*#if (CLAUSE) + content: if +#elseif (CLAUSE_2) + content: elseif +#else + content: else +#endif*/} +Trailing stuff"; + + private const string JsxBasicWithDefault = @"Start +{/*#if (CLAUSE) */} + content: if +{/*#elseif (CLAUSE_2) + content: elseif +#else + content: else +#endif*/} +Trailing stuff"; + + private const string JsxIfEmitted = @"Start + content: if +Trailing stuff"; + + private const string JsxElseIfEmitted = @"Start + content: elseif +Trailing stuff"; + + private const string JsxElseEmitted = @"Start + content: else +Trailing stuff"; + + private static readonly VariableCollection JsxBothClausesTrue = new VariableCollection + { + ["CLAUSE"] = true, + ["CLAUSE_2"] = true // irrelevant + }; + + private static readonly VariableCollection JsxClauseTwoTrue = new VariableCollection + { + ["CLAUSE"] = false, + ["CLAUSE_2"] = true + }; + + private static readonly VariableCollection JsxNeitherClauseTrue = new VariableCollection + { + ["CLAUSE"] = false, + ["CLAUSE_2"] = false + }; + + [Theory(DisplayName = nameof(JsxBlockCommentsBasicTestBothClausesTrue))] + [InlineData(JsxBasicValue, JsxIfEmitted)] + [InlineData(JsxBasicWithDefault, JsxIfEmitted)] + public void JsxBlockCommentsBasicTestBothClausesTrue(string test, string expected) + { + IProcessor processor = SetupJsxBlockCommentsProcessor(JsxBothClausesTrue); + RunAndVerify(test, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(JsxBlockCommentsBasicTestClauseTwoTrue))] + [InlineData(JsxBasicValue, JsxElseIfEmitted)] + [InlineData(JsxBasicWithDefault, JsxElseIfEmitted)] + public void JsxBlockCommentsBasicTestClauseTwoTrue(string test, string expected) + { + IProcessor processor = SetupJsxBlockCommentsProcessor(JsxClauseTwoTrue); + RunAndVerify(test, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(JsxBlockCommentsBasicTestNeitherClauseTrue))] + [InlineData(JsxBasicValue, JsxElseEmitted)] + [InlineData(JsxBasicWithDefault, JsxElseEmitted)] + public void JsxBlockCommentsBasicTestNeitherClauseTrue(string test, string expected) + { + IProcessor processor = SetupJsxBlockCommentsProcessor(JsxNeitherClauseTrue); + RunAndVerify(test, expected, processor, 9999); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.LineCommentingWithNestedCommentsWithCStyleEvaluator.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.LineCommentingWithNestedCommentsWithCStyleEvaluator.cs new file mode 100644 index 000000000000..971fa9349547 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.LineCommentingWithNestedCommentsWithCStyleEvaluator.cs @@ -0,0 +1,264 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + [Fact(DisplayName = nameof(VerifyBasicQuadCommentRemoval))] + public void VerifyBasicQuadCommentRemoval() + { + string originalValue = @"Start +////#if (CLAUSE) +//// Actual Comment +// content +//#endif +// end comment +//// end quad comment +End"; + string expectedValue = @"Start +// Actual Comment + content +// end comment +//// end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["CLAUSE"] = true, + }; + + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // The if is //#if (as opposed to ////#if) so no comment removal in the content + string originalNoCommentRemoval = @"Start +////#if (CLAUSE) +//// Actual Comment +// content +//#endif +// end comment +//// end quad comment +End"; + string expectedValueNoCommentRemoval = @"Start +// Actual Comment + content +// end comment +//// end quad comment +End"; + RunAndVerify(originalNoCommentRemoval, expectedValueNoCommentRemoval, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyQuadCommentRemovalForEachClauseNoEmbedding))] + public void VerifyQuadCommentRemovalForEachClauseNoEmbedding() + { + string originalValue = @"Start +////#if (IF) +// content: if +//// Comment: if +// content: if part 2 +//// Comment: if part 2 +////#elseif (ELSEIF) +//// Comment: elseif +// content: elseif +//// Comment: elseif part 2 +// content: elseif part 2 +////#else +// content: else +//// Comment: else +//// Comment: else 2 +// content: else 2 +//#endif +// end comment +//// end quad comment +End"; + string ifExpectedValue = @"Start + content: if +// Comment: if + content: if part 2 +// Comment: if part 2 +// end comment +//// end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["IF"] = true, + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, ifExpectedValue, processor, 9999); + + string elseIfExpectedValue = @"Start +// Comment: elseif + content: elseif +// Comment: elseif part 2 + content: elseif part 2 +// end comment +//// end quad comment +End"; + vc = new VariableCollection + { + ["IF"] = false, + ["ELSEIF"] = true + }; + processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, elseIfExpectedValue, processor, 9999); + + string elseExpectedValue = @"Start + content: else +// Comment: else +// Comment: else 2 + content: else 2 +// end comment +//// end quad comment +End"; + vc = new VariableCollection + { + ["IF"] = false, + ["ELSEIF"] = false + }; + processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, elseExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyQuadCommentRemovalWithNestedClause))] + public void VerifyQuadCommentRemovalWithNestedClause() + { + string originalValue = @"Start +////#if (OUTER_IF) + //// Comment: outer if + //content outer if + ////#if (INNER_IF) + //// Comment: inner if + //content: inner if + //#endif +//#endif +// end comment +//// end quad comment +End"; + string outerTrueInnerFalseExpectedValue = @"Start + // Comment: outer if + content outer if +// end comment +//// end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerFalseExpectedValue, processor, 9999); + + string outerTrueInnerTrueExpectedValue = @"Start + // Comment: outer if + content outer if + // Comment: inner if + content: inner if +// end comment +//// end quad comment +End"; + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true + }; + processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerTrueExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyQuadCommentRemovalNestedDoesntRemove))] + public void VerifyQuadCommentRemovalNestedDoesntRemove() + { + string originalValue = @"Start +////#if (OUTER_IF) + //// Comment: outer if + //content outer if + //#if (INNER_IF) + //// Comment: inner if + //content: inner if + //#endif +//#endif +// end comment +//// end quad comment +End"; + string outerTrueInnerFalseExpectedValue = @"Start + // Comment: outer if + content outer if +// end comment +//// end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerFalseExpectedValue, processor, 9999); + + // TODO: determine if this is correct, or if the inner should //#if overrides the outer ////#if + string outerTrueInnerTrueExpectedValue = @"Start + // Comment: outer if + content outer if + // Comment: inner if + content: inner if +// end comment +//// end quad comment +End"; + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true + }; + processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerTrueExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyQuadCommentRemovalOnlyNestedRemoves))] + public void VerifyQuadCommentRemovalOnlyNestedRemoves() + { + string originalValue = @"Start +//#if (OUTER_IF) + // Comment: outer if + content outer if + ////#if (INNER_IF) + //// Comment: inner if + //content: inner if + //#endif +//#endif +// end comment +//// end quad comment +End"; + string outerTrueInnerFalseExpectedValue = @"Start + // Comment: outer if + content outer if +// end comment +//// end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerFalseExpectedValue, processor, 9999); + + string outerTrueInnerTrueExpectedValue = @"Start + // Comment: outer if + content outer if + // Comment: inner if + content: inner if +// end comment +//// end quad comment +End"; + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true + }; + processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerTrueExpectedValue, processor, 9999); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.NestedConditionsWithCStyleEvaluator.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.NestedConditionsWithCStyleEvaluator.cs new file mode 100644 index 000000000000..1e41a005b6c3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.NestedConditionsWithCStyleEvaluator.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + /// + /// Tests that the inner if-elseif-else with special tokens gets processed correctly. + /// + [Fact(DisplayName = nameof(VerifyOuterIfAndEmbeddedConditionals))] + public void VerifyOuterIfAndEmbeddedConditionals() + { + string originalValue = @"Lead content +////#if (OUTER_IF) +// outer if content +// ////#if (INNER_IF) +// // inner if content +// ////#elseif (INNER_ELSEIF) +// // inner elseif content +// ////#else +// // inner else content +// //#endif +////#else +// outer else content +//#endif +// commented trailing content +moar trailing content"; + + // outer if & inner if get uncommented + string expectedValue = @"Lead content + outer if content + inner if content +// commented trailing content +moar trailing content"; + + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true, + ["INNER_ELSEIF"] = true + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // outer if & inner elseif + expectedValue = @"Lead content + outer if content + inner elseif content +// commented trailing content +moar trailing content"; + + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false, + ["INNER_ELSEIF"] = true + }; + processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // outer if & inner else + expectedValue = @"Lead content + outer if content + inner else content +// commented trailing content +moar trailing content"; + + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false, + ["INNER_ELSEIF"] = false + }; + processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // outer else - nothing from the inner if should get processed + expectedValue = @"Lead content + outer else content +// commented trailing content +moar trailing content"; + + vc = new VariableCollection + { + ["OUTER_IF"] = false, + ["INNER_IF"] = true, // irrelevant + ["INNER_ELSEIF"] = true // ireelevant + }; + processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyThreeLevelEmbedding))] + public void VerifyThreeLevelEmbedding() + { + string originalValue = @"Lead content +////#if (LEVEL_1_IF) +// content: level-1 if +// ////#if (LEVEL_2_IF) +// // content: level-2 if +// // ////#if (LEVEL_3_IF) +// // // content: level-3 if +// // ////#elseif (LEVEL_3_ELSEIF) +// // // content: level-3 elseif +// // ////#else +// // // content: level-3 else +// // ////#endif +// ////#elseif (LEVEL_2_ELSEIF) +// // content: level-2 elseif +// ////#else +// // content: level-2 else +// ////#endif +////#elseif true +// content: level-1 elseif +////#else +// content: level-1 else +//#endif +// commented trailing content +moar trailing content"; + + // outer if & inner if get uncommented + string expectedValue = @"Lead content + content: level-1 if + content: level-2 if + content: level-3 if +// commented trailing content +moar trailing content"; + + VariableCollection vc = new VariableCollection + { + ["LEVEL_1_IF"] = true, + ["LEVEL_2_IF"] = true, + ["LEVEL_3_IF"] = true, + ["LEVEL_3_ELSEIF"] = true, // irrelevant + ["LEVEL_2_ELSEIF"] = true, // irrelevant + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.NestedConditionsWithVbStyleEvaluator.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.NestedConditionsWithVbStyleEvaluator.cs new file mode 100644 index 000000000000..15be01881f7a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.NestedConditionsWithVbStyleEvaluator.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + /// + /// Tests that the inner if-elseif-else with special tokens gets processed correctly. + /// + [Fact(DisplayName = nameof(VbVerifyOuterIfAndEmbeddedConditionals))] + public void VbVerifyOuterIfAndEmbeddedConditionals() + { + const string originalValue = @"Lead content +#If (OUTER_IF) Then + outer if content + #If (INNER_IF) Then + inner if content + #ElseIf (INNER_ELSEIF) Then + inner elseif content + #Else + inner else content + #End If +#Else + outer else content +#End If +// commented trailing content +moar trailing content"; + + // outer if & inner if get uncommented + string expectedValue = @"Lead content + outer if content + inner if content +// commented trailing content +moar trailing content"; + + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true, + ["INNER_ELSEIF"] = true + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // outer if & inner elseif + expectedValue = @"Lead content + outer if content + inner elseif content +// commented trailing content +moar trailing content"; + + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false, + ["INNER_ELSEIF"] = true + }; + processor = SetupVBStyleNoCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // outer if & inner else + expectedValue = @"Lead content + outer if content + inner else content +// commented trailing content +moar trailing content"; + + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false, + ["INNER_ELSEIF"] = false + }; + processor = SetupVBStyleNoCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // outer else - nothing from the inner if should get processed + expectedValue = @"Lead content + outer else content +// commented trailing content +moar trailing content"; + + vc = new VariableCollection + { + ["OUTER_IF"] = false, + ["INNER_IF"] = true, // irrelevant + ["INNER_ELSEIF"] = true // ireelevant + }; + processor = SetupVBStyleNoCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VbVerifyThreeLevelEmbedding))] + public void VbVerifyThreeLevelEmbedding() + { + const string originalValue = @"Lead content +#If (LEVEL_1_IF) Then + content: level-1 if + #If (LEVEL_2_IF) Then + content: level-2 if + #If (LEVEL_3_IF) Then + content: level-3 if + #ElseIf (LEVEL_3_ELSEIF) Then + content: level-3 elseif + #Else + content: level-3 else + #End If + #ElseIf (LEVEL_2_ELSEIF) Then + content: level-2 elseif + #Else + content: level-2 else + #End If +#ElseIf true Then + content: level-1 elseif +#Else + content: level-1 else +#End If +// commented trailing content +moar trailing content"; + + // outer if & inner if get uncommented + const string expectedValue = @"Lead content + content: level-1 if + content: level-2 if + content: level-3 if +// commented trailing content +moar trailing content"; + + VariableCollection vc = new VariableCollection + { + ["LEVEL_1_IF"] = true, + ["LEVEL_2_IF"] = true, + ["LEVEL_3_IF"] = true, + ["LEVEL_3_ELSEIF"] = true, // irrelevant + ["LEVEL_2_ELSEIF"] = true, // irrelevant + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.RazorBlockComments.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.RazorBlockComments.cs new file mode 100644 index 000000000000..8bcb09453997 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.RazorBlockComments.cs @@ -0,0 +1,378 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + private const string NoDefaultValue = @"Start +@*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else + content: outer-else + @*#if (INNER_IF_CLAUSE) + content: inner-if + #elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + #else + content: inner-else + #endif +#endif*@ +Trailing stuff +@* trailing comment *@"; + + private const string OuterIfDefaultValue = @"Start +@*#if (OUTER_IF_CLAUSE) *@ + content: outer-if +@*#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else + content: outer-else + @*#if (INNER_IF_CLAUSE) + content: inner-if + #elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + #else + content: inner-else + #endif +#endif*@ +Trailing stuff +@* trailing comment *@"; + + private const string OuterIfTrueExpectedValue = @"Start + content: outer-if +Trailing stuff +@* trailing comment *@"; + + private const string OuterElseifDefaultValue = @"Start +@*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) *@ + content: outer-elseif +@*#else + content: outer-else + @*#if (INNER_IF_CLAUSE) + content: inner-if + #elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + #else + content: inner-else + #endif +#endif*@ +Trailing stuff +@* trailing comment *@"; + + // this one seems formatted weird, but correct. + private const string OuterElseDefaultValue = @"Start +@*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else *@ + content: outer-else + @*#if (INNER_IF_CLAUSE) + content: inner-if + #elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + #else + content: inner-else + #endif +#endif*@ +Trailing stuff +@* trailing comment *@"; + + private const string InnerIfDefaultValue = @"Start +@*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else + content: outer-else + @*#if (INNER_IF_CLAUSE) *@ + content: inner-if + @*#elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + #else + content: inner-else + #endif +#endif*@ +Trailing stuff +@* trailing comment *@"; + + private const string InnerElseifDefaultValue = @"Start +@*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else + content: outer-else + @*#if (INNER_IF_CLAUSE) + content: inner-if + @*#elseif (INNER_ELSEIF_CLAUSE) *@ + content: inner-elseif + @*#else + content: inner-else + #endif +#endif*@ +Trailing stuff +@* trailing comment *@"; + + private const string InnerElseDefaultValue = @"Start +@*#if (OUTER_IF_CLAUSE) + content: outer-if +#elseif (OUTER_ELSEIF_CLAUSE) + content: outer-elseif +#else + content: outer-else + @*#if (INNER_IF_CLAUSE) + content: inner-if + #elseif (INNER_ELSEIF_CLAUSE) + content: inner-elseif + @*#else*@ + content: inner-else + #endif +#endif*@ +Trailing stuff +@* trailing comment *@"; + + private const string OuterElseifTrueExpectedValue = @"Start + content: outer-elseif +Trailing stuff +@* trailing comment *@"; + + private const string OuterElseHappensInnerIfTrueExpectedValue = @"Start + content: outer-else + content: inner-if +Trailing stuff +@* trailing comment *@"; + + private const string OuterElseHappensInnerElseifTrueExpectedValue = @"Start + content: outer-else + content: inner-elseif +Trailing stuff +@* trailing comment *@"; + + private const string OuterElseHappensInnerElseHappensExpectedValue = @"Start + content: outer-else + content: inner-else +Trailing stuff +@* trailing comment *@"; + + private static readonly VariableCollection OuterElseifTrueVariableCollection = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = true, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false + }; + + private static readonly VariableCollection InnerIfTrueVariableCollection = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = true, + ["INNER_ELSEIF_CLAUSE"] = false + }; + + private static readonly VariableCollection InnerElseIfTrueVariableCollection = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = true + }; + + private static readonly VariableCollection AllFalse = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false + }; + + private static VariableCollection OuterIfTrueVariableCollection => new VariableCollection + { + ["OUTER_IF_CLAUSE"] = true, + ["OUTER_ELSEIF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false + }; + + [Theory(DisplayName = nameof(VerifyRazorBlockCommentEmbeddedInElseTestOuterIfTrue))] + [InlineData(NoDefaultValue, OuterIfTrueExpectedValue)] + [InlineData(OuterIfDefaultValue, OuterIfTrueExpectedValue)] + [InlineData(OuterElseifDefaultValue, OuterIfTrueExpectedValue)] + [InlineData(OuterElseDefaultValue, OuterIfTrueExpectedValue)] + [InlineData(InnerIfDefaultValue, OuterIfTrueExpectedValue)] + [InlineData(InnerElseifDefaultValue, OuterIfTrueExpectedValue)] + [InlineData(InnerElseDefaultValue, OuterIfTrueExpectedValue)] + public void VerifyRazorBlockCommentEmbeddedInElseTestOuterIfTrue(string source, string expected) + { + IProcessor processor = SetupRazorStyleProcessor(OuterIfTrueVariableCollection); + RunAndVerify(source, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(VerifyRazorBlockCommentEmbeddedInElseTestOuterElseifTrueExpectedValue))] + [InlineData(NoDefaultValue, OuterElseifTrueExpectedValue)] + [InlineData(OuterIfDefaultValue, OuterElseifTrueExpectedValue)] + [InlineData(OuterElseifDefaultValue, OuterElseifTrueExpectedValue)] + [InlineData(OuterElseDefaultValue, OuterElseifTrueExpectedValue)] + [InlineData(InnerIfDefaultValue, OuterElseifTrueExpectedValue)] + [InlineData(InnerElseifDefaultValue, OuterElseifTrueExpectedValue)] + [InlineData(InnerElseDefaultValue, OuterElseifTrueExpectedValue)] + public void VerifyRazorBlockCommentEmbeddedInElseTestOuterElseifTrueExpectedValue(string source, string expected) + { + IProcessor processor = SetupRazorStyleProcessor(OuterElseifTrueVariableCollection); + RunAndVerify(source, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(VerifyRazorBlockCommentEmbeddedInElseTestOuterElseHappensInnerIfTrueExpectedValue))] + [InlineData(NoDefaultValue, OuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(OuterIfDefaultValue, OuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(OuterElseifDefaultValue, OuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(OuterElseDefaultValue, OuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(InnerIfDefaultValue, OuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(InnerElseifDefaultValue, OuterElseHappensInnerIfTrueExpectedValue)] + [InlineData(InnerElseDefaultValue, OuterElseHappensInnerIfTrueExpectedValue)] + public void VerifyRazorBlockCommentEmbeddedInElseTestOuterElseHappensInnerIfTrueExpectedValue(string source, string expected) + { + IProcessor processor = SetupRazorStyleProcessor(InnerIfTrueVariableCollection); + RunAndVerify(source, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(VerifyRazorBlockCommentEmbeddedInElseTestOuterElseHappensInnerElseifTrueExpectedValue))] + [InlineData(NoDefaultValue, OuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(OuterIfDefaultValue, OuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(OuterElseifDefaultValue, OuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(OuterElseDefaultValue, OuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(InnerIfDefaultValue, OuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(InnerElseifDefaultValue, OuterElseHappensInnerElseifTrueExpectedValue)] + [InlineData(InnerElseDefaultValue, OuterElseHappensInnerElseifTrueExpectedValue)] + public void VerifyRazorBlockCommentEmbeddedInElseTestOuterElseHappensInnerElseifTrueExpectedValue(string source, string expected) + { + IProcessor processor = SetupRazorStyleProcessor(InnerElseIfTrueVariableCollection); + RunAndVerify(source, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(VerifyRazorBlockCommentEmbeddedInElseTestOuterElseHappensInnerElseHappensExpectedValue))] + [InlineData(NoDefaultValue, OuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(OuterIfDefaultValue, OuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(OuterElseifDefaultValue, OuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(OuterElseDefaultValue, OuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(InnerIfDefaultValue, OuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(InnerElseifDefaultValue, OuterElseHappensInnerElseHappensExpectedValue)] + [InlineData(InnerElseDefaultValue, OuterElseHappensInnerElseHappensExpectedValue)] + public void VerifyRazorBlockCommentEmbeddedInElseTestOuterElseHappensInnerElseHappensExpectedValue(string source, string expected) + { + IProcessor processor = SetupRazorStyleProcessor(AllFalse); + RunAndVerify(source, expected, processor, 9999); + } + + private const string BasicValue = @"Start +@*#if (CLAUSE) + content: if +#elseif (CLAUSE_2) + content: elseif +#else + content: else +#endif*@ +Trailing stuff"; + + private const string BasicWithDefault = @"Start +@*#if (CLAUSE) *@ + content: if +@*#elseif (CLAUSE_2) + content: elseif +#else + content: else +#endif*@ +Trailing stuff"; + + private const string IfEmitted = @"Start + content: if +Trailing stuff"; + + private const string ElseIfEmitted = @"Start + content: elseif +Trailing stuff"; + + private const string ElseEmitted = @"Start + content: else +Trailing stuff"; + + private static readonly VariableCollection BothClausesTrue = new VariableCollection + { + ["CLAUSE"] = true, + ["CLAUSE_2"] = true // irrelevant + }; + + private static readonly VariableCollection ClauseTwoTrue = new VariableCollection + { + ["CLAUSE"] = false, + ["CLAUSE_2"] = true + }; + + private static readonly VariableCollection NeitherClauseTrue = new VariableCollection + { + ["CLAUSE"] = false, + ["CLAUSE_2"] = false + }; + + [Theory(DisplayName = nameof(RazorBlockCommentsBasicTestBothClausesTrue))] + [InlineData(BasicValue, IfEmitted)] + [InlineData(BasicWithDefault, IfEmitted)] + public void RazorBlockCommentsBasicTestBothClausesTrue(string test, string expected) + { + IProcessor processor = SetupRazorStyleProcessor(BothClausesTrue); + RunAndVerify(test, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(RazorBlockCommentsBasicTestClauseTwoTrue))] + [InlineData(BasicValue, ElseIfEmitted)] + [InlineData(BasicWithDefault, ElseIfEmitted)] + public void RazorBlockCommentsBasicTestClauseTwoTrue(string test, string expected) + { + IProcessor processor = SetupRazorStyleProcessor(ClauseTwoTrue); + RunAndVerify(test, expected, processor, 9999); + } + + [Theory(DisplayName = nameof(RazorBlockCommentsBasicTestNeitherClauseTrue))] + [InlineData(BasicValue, ElseEmitted)] + [InlineData(BasicWithDefault, ElseEmitted)] + public void RazorBlockCommentsBasicTestNeitherClauseTrue(string test, string expected) + { + IProcessor processor = SetupRazorStyleProcessor(NeitherClauseTrue); + RunAndVerify(test, expected, processor, 9999); + } + + [Theory] + [InlineData(true, "No Auth")] + [InlineData(false, "Auth")] + // File encoding is not maintained when first line of file is a conditional statement + // https://github.com/dotnet/templating/issues/2217 + public void BomMentained(bool noAuth, string expected) + { + IProcessor processor = SetupRazorStyleProcessor(new VariableCollection + { + ["NoAuth"] = noAuth + }); + RunAndVerify( + @"@*#if (NoAuth) +No Auth +#else +Auth +#endif*@ +Trailing stuff", + expected + @" +Trailing stuff", + processor, + 9999, + emitBOM: true); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.RemCommentWithCStyleEvaluator.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.RemCommentWithCStyleEvaluator.cs new file mode 100644 index 000000000000..60f6445869b5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.RemCommentWithCStyleEvaluator.cs @@ -0,0 +1,269 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + [Fact(DisplayName = nameof(VerifyBasicBatRemCommentHandling))] + public void VerifyBasicBatRemCommentHandling() + { + string originalValue = @"Start +rem rem #if (CLAUSE) +rem rem Actual Comment +rem content +rem #endif +rem end comment +rem rem end quad comment +End"; + + string expectedValue = @"Start +rem Actual Comment + content +rem end comment +rem rem end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["CLAUSE"] = true, + }; + + IProcessor processor = SetupBatFileRemLineCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + string originalNoCommentRemoval = @"Start +rem #if (CLAUSE) +rem rem Actual Comment +rem content +rem #endif +rem end comment +rem rem end quad comment +End"; + + string expectedValueNoCommentRemoval = @"Start +rem rem Actual Comment +rem content +rem end comment +rem rem end quad comment +End"; + RunAndVerify(originalNoCommentRemoval, expectedValueNoCommentRemoval, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyBatRemCommentRemovalForEachClauseNoEmbedding))] + public void VerifyBatRemCommentRemovalForEachClauseNoEmbedding() + { + string originalValue = @"Start +rem rem #if (IF) +rem content: if +rem rem Comment: if +rem content: if part 2 +rem rem Comment: if part 2 +rem rem #elseif (ELSEIF) +rem rem Comment: elseif +rem content: elseif +rem rem Comment: elseif part 2 +rem content: elseif part 2 +rem rem #else +rem content: else +rem rem Comment: else +rem rem Comment: else 2 +rem content: else 2 +rem #endif +rem end comment +rem rem end quad comment +End"; + string ifExpectedValue = @"Start + content: if +rem Comment: if + content: if part 2 +rem Comment: if part 2 +rem end comment +rem rem end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["IF"] = true, + }; + IProcessor processor = SetupBatFileRemLineCommentsProcessor(vc); + RunAndVerify(originalValue, ifExpectedValue, processor, 9999); + + string elseIfExpectedValue = @"Start +rem Comment: elseif + content: elseif +rem Comment: elseif part 2 + content: elseif part 2 +rem end comment +rem rem end quad comment +End"; + vc = new VariableCollection + { + ["IF"] = false, + ["ELSEIF"] = true + }; + processor = SetupBatFileRemLineCommentsProcessor(vc); + RunAndVerify(originalValue, elseIfExpectedValue, processor, 9999); + + string elseExpectedValue = @"Start + content: else +rem Comment: else +rem Comment: else 2 + content: else 2 +rem end comment +rem rem end quad comment +End"; + + vc = new VariableCollection + { + ["IF"] = false, + ["ELSEIF"] = false + }; + processor = SetupBatFileRemLineCommentsProcessor(vc); + RunAndVerify(originalValue, elseExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyBatRemCommentRemovalWithNestedClause))] + public void VerifyBatRemCommentRemovalWithNestedClause() + { + string originalValue = @"Start +rem rem #if (OUTER_IF) + rem rem Comment: outer if + rem content outer if + rem rem #if (INNER_IF) + rem rem Comment: inner if + rem content: inner if + rem rem #endif +rem rem #endif +rem end comment +rem rem end quad comment +End"; + string outerTrueInnerFalseExpectedValue = @"Start + rem Comment: outer if + content outer if +rem end comment +rem rem end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false + }; + IProcessor processor = SetupBatFileRemLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerFalseExpectedValue, processor, 9999); + + string outerTrueInnerTrueExpectedValue = @"Start + rem Comment: outer if + content outer if + rem Comment: inner if + content: inner if +rem end comment +rem rem end quad comment +End"; + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true + }; + processor = SetupBatFileRemLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerTrueExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyBatRemCommentRemovalNestedDoesntRemove))] + public void VerifyBatRemCommentRemovalNestedDoesntRemove() + { + string originalValue = @"Start +rem rem #if (OUTER_IF) + rem rem Comment: outer if + rem content outer if + rem rem #if (INNER_IF) + rem rem Comment: inner if + rem content: inner if + rem rem #endif +rem rem #endif +rem end comment +rem rem end quad comment +End"; + string outerTrueInnerFalseExpectedValue = @"Start + rem Comment: outer if + content outer if +rem end comment +rem rem end quad comment +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false + }; + IProcessor processor = SetupBatFileRemLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerFalseExpectedValue, processor, 9999); + + // TODO: determine if this is correct, or if the inner should //#if overrides the outer ////#if + string outerTrueInnerTrueExpectedValue = @"Start + rem Comment: outer if + content outer if + rem Comment: inner if + content: inner if +rem end comment +rem rem end quad comment +End"; + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true + }; + processor = SetupBatFileRemLineCommentsProcessor(vc); + RunAndVerify(originalValue, outerTrueInnerTrueExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyBatRemMixedConditionalsThreeLevelEmbedding))] + public void VerifyBatRemMixedConditionalsThreeLevelEmbedding() + { + string originalValue = @"Lead content +rem rem #if (LEVEL_1_IF) +rem content: level-1 if +rem rem rem #if (LEVEL_2_IF) +rem rem content: level-2 if +rem rem rem rem #if (LEVEL_3_IF) +rem rem rem content: level-3 if +rem rem rem rem #elseif (LEVEL_3_ELSEIF) +rem rem rem content: level-3 elseif +rem rem rem rem #else +rem rem rem content: level-3 else +rem rem rem rem #endif +rem rem rem #elseif (LEVEL_2_ELSEIF) +rem rem content: level-2 elseif +rem rem rem #else +rem rem content: level-2 else +rem rem rem #endif +rem rem #elseif true +rem content: level-1 elseif +rem rem #else +rem content: level-1 else +rem rem #endif +rem commented trailing content +moar trailing content"; + + // outer if & inner if get uncommented + string expectedValue = @"Lead content + content: level-1 if + content: level-2 if + content: level-3 if +rem commented trailing content +moar trailing content"; + + VariableCollection vc = new VariableCollection + { + ["LEVEL_1_IF"] = true, + ["LEVEL_2_IF"] = true, + ["LEVEL_3_IF"] = true, + ["LEVEL_3_ELSEIF"] = true, // irrelevant + ["LEVEL_2_ELSEIF"] = true, // irrelevant + }; + + IProcessor processor = SetupBatFileRemLineCommentsProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.UncommentingBehavior.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.UncommentingBehavior.cs new file mode 100644 index 000000000000..f974f042d26b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.UncommentingBehavior.cs @@ -0,0 +1,850 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + [Fact(DisplayName = nameof(VerifyMixedConditionalsThreeLevelEmbedding))] + public void VerifyMixedConditionalsThreeLevelEmbedding() + { + string originalValue = @"Lead content +////#check (LEVEL_1_IF) +// content: level-1 if +// ////#if (LEVEL_2_IF) +// // content: level-2 if +// // ////#check (LEVEL_3_IF) +// // // content: level-3 if +// // ////#elseif (LEVEL_3_ELSEIF) +// // // content: level-3 elseif +// // ////#otherwise +// // // content: level-3 else +// // ////#endif +// ////#nextcheck (LEVEL_2_ELSEIF) +// // content: level-2 elseif +// ////#else +// // content: level-2 else +// ////#stop +////#nextcheck true +// content: level-1 elseif +////#else +// content: level-1 else +//#done +// commented trailing content +moar trailing content"; + + // outer if & inner if get uncommented + string expectedValue = @"Lead content + content: level-1 if + content: level-2 if + content: level-3 if +// commented trailing content +moar trailing content"; + + VariableCollection vc = new VariableCollection + { + ["LEVEL_1_IF"] = true, + ["LEVEL_2_IF"] = true, + ["LEVEL_3_IF"] = true, + ["LEVEL_3_ELSEIF"] = true, // irrelevant + ["LEVEL_2_ELSEIF"] = true, // irrelevant + }; + + IProcessor processor = SetupMadeUpStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(MixedTokenFormsTest))] + public void MixedTokenFormsTest() + { + IList testCases = new List(); + + string originalValue = @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(originalValue); + + originalValue = @"Hello +////#check (VALUE_IF) + //if value + //...if commented in original +////#nextcheck (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#otherwise + //else value + //...else commented in original +//#done +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(originalValue); + + // this one is pretty weird + originalValue = @"Hello +//#Z_if (VALUE_IF) + //if value + //...if commented in original +////#Z_elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#Z_else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(originalValue); + + originalValue = @"Hello +////#check (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#otherwise + //else value + //...else commented in original +//#nomore +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(originalValue); + + string expectedValue = @"Hello + else value + ...else commented in original +Past endif + ...uncommented in original +// dont uncomment"; + + VariableCollection vc = new VariableCollection + { + ["VALUE_IF"] = false, // must be false for the else to process + ["VALUE_ELSEIF"] = false // must be false for the else to process + }; + IProcessor processor = SetupMadeUpStyleProcessor(vc); + + foreach (string test in testCases) + { + RunAndVerify(test, expectedValue, processor, 28); + } + } + + [Fact(DisplayName = nameof(MultiTokenFormsBaseTest))] + public void MultiTokenFormsBaseTest() + { + IList testCases = new List(); + + // special #if (true) + string originalValue = @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(originalValue); + + string madeUpIfsValue = @"Hello +////#check (VALUE_IF) + //if value + //...if commented in original +//#nomore +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(madeUpIfsValue); + + string expectedValue = @"Hello + if value + ...if commented in original +Past endif + ...uncommented in original +// dont uncomment"; + + VariableCollection vc = new VariableCollection + { + ["VALUE_IF"] = true, // must be false for the else to process + }; + IProcessor processor = SetupMadeUpStyleProcessor(vc); + + foreach (string test in testCases) + { + RunAndVerify(test, expectedValue, processor, 28); + } + } + + /// + /// Tests that the if block is uncommented in each of the scenarios + /// because the if token is special and the clause is true in each case. + /// + [Theory(DisplayName = nameof(VerifySpecialIfTrueUncomments))] + [InlineData( + @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment", + "special #if (true)")] + [InlineData( + @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +//#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment", + "special #if (true), regular #else")] + [InlineData( + @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment", + "special #if (true), special #else ignored")] + [InlineData( + @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +//#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +//#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment", + "special #if (true), regular #elseif, regular #else")] + [InlineData( + @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +//#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment", + "special #if (true), special #elseif, regular #else")] + [InlineData( + @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +//#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment", + "special #if (true), regular #elseif, special #else")] + [InlineData( + @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment", + "special #if (true), special #elseif, special #else")] + public void VerifySpecialIfTrueUncomments(string test, string comment) + { + // with the if is true, all of the above test cases should emit this + string expectedValue = @"Hello + if value + ...if commented in original +Past endif + ...uncommented in original +// dont uncomment"; + + // setup for the if being true - always take the if + VariableCollection vc = new VariableCollection + { + ["VALUE_IF"] = true, // should be true to get the if to process + ["VALUE_ELSEIF"] = false // shouldn't matter, since the if is always true + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + RunAndVerify(test, expectedValue, processor, 28); + + //comment is unused - Asserting to bypass the warning. + Assert.Equal(comment, comment); + } + + /// + /// Tests that the elseif block is uncommented in each of the scenarios + /// because the elseif token is special and the clause is true in each case. + /// + [Fact(DisplayName = nameof(VerifySpecialElseifTrueUncomments))] + public void VerifySpecialElseifTrueUncomments() + { + IList testCases = new List(); + + //#if + ////#elseif + string ifRegularValue = @"Hello +//#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifRegularValue); + + ////#if + ////#elseif + string ifSpecialValue = @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifSpecialValue); + + //#if + ////#elseif + //#else + string ifRegularElseRegularValue = @"Hello +//#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +//#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifRegularElseRegularValue); + + ////#if + ////#elseif + //#else + string ifSpecialElseRegularValue = @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +//#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifSpecialElseRegularValue); + + //#if + ////#elseif + ////#else + string ifRegularElseSpecialValue = @"Hello +//#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifRegularElseSpecialValue); + + ////#if + ////#elseif + ////#else + string ifSpecialElseSpecialValue = @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifSpecialElseSpecialValue); + + // with the if false and the elseif true, all of the above test cases should emit this + string expectedValue = @"Hello + elseif value + ...elseif commented in original +Past endif + ...uncommented in original +// dont uncomment"; + + // setup for the if being true - always take the if + VariableCollection vc = new VariableCollection + { + ["VALUE_IF"] = false, // must be false, to get the elseif to process + ["VALUE_ELSEIF"] = true // must be true to get the elseif to process + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + + foreach (string test in testCases) + { + RunAndVerify(test, expectedValue, processor, 28); + } + } + + /// + /// Tests that the else block is uncommented in each of the scenarios + /// because the if and elseif conditions (if present) are false in each case. + /// + [Fact(DisplayName = nameof(VerifySpecialElseTrueUncomments))] + public void VerifySpecialElseTrueUncomments() + { + IList testCases = new List(); + + //#if + ////#else + string ifRegularValue = @"Hello +//#if (VALUE_IF) + //if value + //...if commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifRegularValue); + + ////#if + ////#else + string ifSpecialValue = @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifSpecialValue); + + //#if + //#elseif + ////#else + string ifRegularElseifRegularValue = @"Hello +//#if (VALUE_IF) + //if value + //...if commented in original +//#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifRegularElseifRegularValue); + + ////#if + //#elseif + ////#else + string ifSpecialElseifRegularValue = @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +//#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifSpecialElseifRegularValue); + + //#if + ////#elseif + ////#else + string ifRegularElseifSpecialValue = @"Hello +//#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifRegularElseifSpecialValue); + + ////#if + ////#elseif + ////#else + string ifSpecialElseifSpecialValue = @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + testCases.Add(ifSpecialElseifSpecialValue); + + // with the if false and the elseif true, all of the above test cases should emit this + string expectedValue = @"Hello + else value + ...else commented in original +Past endif + ...uncommented in original +// dont uncomment"; + + // setup for the if being true - always take the if + VariableCollection vc = new VariableCollection + { + ["VALUE_IF"] = false, // must be false for the else to process + ["VALUE_ELSEIF"] = false // must be false for the else to process + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + + foreach (string test in testCases) + { + RunAndVerify(test, expectedValue, processor, 28); + } + } + + /// + /// The #if condition is false, so don't emit its value in any way. + /// + [Fact(DisplayName = nameof(VerifyFalseIfDoesNotUncomment))] + public void VerifyFalseIfDoesNotUncomment() + { + string ifOnlyValue = @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + + string expectedValue = @"Hello +Past endif + ...uncommented in original +// dont uncomment"; + + VariableCollection vc = new VariableCollection + { + ["VALUE_IF"] = false, // should be true to get the if to process + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + + // Test with just an if condition + RunAndVerify(ifOnlyValue, expectedValue, processor, 28); + } + + /// + /// The #if condition is false, so don't emit its value in any way. + /// But emit the else value without modification (because its not the special #else). + /// + [Fact(DisplayName = nameof(VerifyFalseIfDoesNotUncommentButElseIsEmitted))] + public void VerifyFalseIfDoesNotUncommentButElseIsEmitted() + { + string ifElseValue = @"Hello +////#if (VALUE_IF) + //if value + //...if commented in original +//#else + //else value + //...else commented in original - stays commented +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + + string expectedValue = @"Hello + //else value + //...else commented in original - stays commented +Past endif + ...uncommented in original +// dont uncomment"; + + VariableCollection vc = new VariableCollection + { + ["VALUE_IF"] = false, // should be true to get the if to process + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + + // Test with just an if condition + RunAndVerify(ifElseValue, expectedValue, processor, 28); + } + + /// + /// Tests that the #else block is uncommented in each of the scenarios because: + /// its the special #else + /// and the if and elseif conditions are false. + /// + [Fact(DisplayName = nameof(VerifyElseUncomments))] + public void VerifyElseUncomments() + { + string ifElseValue = @"Hello +//#if (VALUE_IF) + //if value + //...if commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + + string ifElseifElseValue = @"Hello +//#if (VALUE_IF) + //if value + //...if commented in original +//#elseif (VALUE_ELSEIF) + //elseif value + //...elseif commented in original +////#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + + // all of the above test cases should emit this + string expectedValue = @"Hello + else value + ...else commented in original +Past endif + ...uncommented in original +// dont uncomment"; + + VariableCollection vc = new VariableCollection + { + ["VALUE_IF"] = false, + ["VALUE_ELSEIF"] = false + }; + IProcessor processor = SetupCStyleWithCommentsProcessor(vc); + + // test with an if-else condition + RunAndVerify(ifElseValue, expectedValue, processor, 28); + + // test with an if-elseif-else condition + RunAndVerify(ifElseifElseValue, expectedValue, processor, 28); + } + + /// + /// Tests that the first elseif block is uncommented + /// It's the one with the true condition. + /// + [Fact(DisplayName = nameof(VerifyFirstElseifUncomments))] + public void VerifyFirstElseifUncomments() + { + string ifElseifElseValue = @"Hello +//#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF_ONE) + //elseif value one + //...elseif one commented in original +//#elseif (VALUE_ELSEIF_TWO) + //elseif value two + //...elseif two commented in original +//#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + + // all of the above test cases should emit this + string expectedValue = @"Hello + elseif value one + ...elseif one commented in original +Past endif + ...uncommented in original +// dont uncomment"; + + VariableCollection vc = new VariableCollection + { + ["VALUE_IF"] = false, + ["VALUE_ELSEIF_ONE"] = true, + ["VALUE_ELSEIF_TWO"] = true // value should not matter + }; + IProcessor processor = SetupMadeUpStyleProcessor(vc); + + // test with an if-else condition + RunAndVerify(ifElseifElseValue, expectedValue, processor, 28); + } + + /// + /// Tests the multiple special elseif's are respected. In this test, the 2nd elseif is special and should have its content uncommented. + /// TODO: make more test with multiple elseif's. + /// + [Fact(DisplayName = nameof(VerifySecondElseifUncomments))] + public void VerifySecondElseifUncomments() + { + string ifElseifElseValue = @"Hello +//#if (VALUE_IF) + //if value + //...if commented in original +////#elseif (VALUE_ELSEIF_ONE) + //elseif value one + //...elseif one commented in original +////#elseif (VALUE_ELSEIF_TWO) + //elseif value two + //...elseif two commented in original +//#else + //else value + //...else commented in original +//#endif +Past endif + ...uncommented in original +// dont uncomment"; + + // all of the above test cases should emit this + string expectedValue = @"Hello + elseif value two + ...elseif two commented in original +Past endif + ...uncommented in original +// dont uncomment"; + + VariableCollection vc = new VariableCollection + { + ["VALUE_IF"] = false, + ["VALUE_ELSEIF_ONE"] = false, + ["VALUE_ELSEIF_TWO"] = true + }; + IProcessor processor = SetupMadeUpStyleProcessor(vc); + + // test with an if-else condition + RunAndVerify(ifElseifElseValue, expectedValue, processor, 28); + } + + [Fact(DisplayName = nameof(VerifyElseIfUncomments))] + public void VerifyElseIfUncomments() + { + string value = @"Hello +//#if (VALUE) + value + another line +////#elseif (ELSEIF_VALUE) + //elseif uncommented + //...hopefully +//#else + //Dont Uncommented the else + //...as expected +//#endif +Past the endif +// dont uncomment"; + + string expected = @"Hello + elseif uncommented + ...hopefully +Past the endif +// dont uncomment"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE"] = false, + ["ELSEIF_VALUE"] = true + }; + IProcessor processor = SetupMadeUpStyleProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.VBStyleEvaluator.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.VBStyleEvaluator.cs new file mode 100644 index 000000000000..5d64fca95c9c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.VBStyleEvaluator.cs @@ -0,0 +1,370 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + [Fact(DisplayName = nameof(VBVerifyIfEndifTrueCondition))] + public void VBVerifyIfEndifTrueCondition() + { + string value = @"Hello + #If (VALUE) Then +value + #End If +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection { ["VALUE"] = true }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifFalseCondition))] + public void VBVerifyIfEndifFalseCondition() + { + string value = @"Hello + #If (VALUE) Then +value + #End If +There"; + string expected = @"Hello +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection { ["VALUE"] = false }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifTrueAndFalseCondition))] + public void VBVerifyIfEndifTrueAndFalseCondition() + { + string value = @"Hello + #If (VALUE1 And Value2) Then +value + #End If +There"; + string expected = @"Hello +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE1"] = true, + ["VALUE2"] = false + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifNotNotTrueAndFalseCondition))] + public void VBVerifyIfEndifNotNotTrueAndFalseCondition() + { + string value = @"Hello + #If (Not (Not VALUE1 And VALUE2)) Then +value + #End If +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE1"] = true, + ["VALUE2"] = false + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifTrueAndAlsoTrueCondition))] + public void VBVerifyIfEndifTrueAndAlsoTrueCondition() + { + string value = @"Hello + #If (VALUE1 AndAlso VALUE2) Then +value + #End If +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE1"] = true, + ["VALUE2"] = true + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifTrueAndAlsoNotFalseCondition))] + public void VBVerifyIfEndifTrueAndAlsoNotFalseCondition() + { + string value = @"Hello + #If (VALUE1 AndAlso Not VALUE2) Then +value + #End If +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE1"] = true, + ["VALUE2"] = false + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifNotFalseAndNotFalseAndNotFalseCondition))] + public void VBVerifyIfEndifNotFalseAndNotFalseAndNotFalseCondition() + { + string value = @"Hello + #If (Not VALUE1 And Not VALUE2 And Not VALUE3) Then +value + #End If +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE1"] = false, + ["VALUE2"] = false, + ["VALUE3"] = false + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifNotFalseAndNotFalseAndNotTrueCondition))] + public void VBVerifyIfEndifNotFalseAndNotFalseAndNotTrueCondition() + { + string value = @"Hello + #If (Not VALUE1 And Not VALUE2 And Not VALUE3) Then +value + #End If +There"; + string expected = @"Hello +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE1"] = false, + ["VALUE2"] = false, + ["VALUE3"] = true + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifNotFalseOrTrueCondition))] + public void VBVerifyIfEndifNotFalseOrTrueCondition() + { + string value = @"Hello + #If (Not VALUE1 Or VALUE2) Then +value + #End If +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE1"] = false, + ["VALUE2"] = true + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifNotFalseOrElseTrueCondition))] + public void VBVerifyIfEndifNotFalseOrElseTrueCondition() + { + string value = @"Hello + #If (Not VALUE1 OrElse VALUE2) Then +value + #End If +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE1"] = false, + ["VALUE2"] = true + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifNotNotFalseAndNotFalseAndNotTrueCondition))] + public void VBVerifyIfEndifNotNotFalseAndNotFalseAndNotTrueCondition() + { + string value = @"Hello + #If (Not (Not VALUE1 And Not VALUE2 And Not VALUE3)) Then +value + #End If +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE1"] = false, + ["VALUE2"] = false, + ["VALUE3"] = true + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifExponentiateEqualsCondition))] + public void VBVerifyIfEndifExponentiateEqualsCondition() + { + string value = @"Hello + #If (2^3 = 8) Then +value + #End If +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE1"] = false, + ["VALUE2"] = false, + ["VALUE3"] = true + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VBVerifyIfEndifNotExponentiateNotEqualsCondition))] + public void VBVerifyIfEndifNotExponentiateNotEqualsCondition() + { + string value = @"Hello + #If (Not (2^3 <> 8)) Then +value + #End If +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VALUE1"] = false, + ["VALUE2"] = false, + ["VALUE3"] = true + }; + IProcessor processor = SetupVBStyleNoCommentsProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.XmlBlockComments.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.XmlBlockComments.cs new file mode 100644 index 000000000000..e6ba784fac31 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.XmlBlockComments.cs @@ -0,0 +1,1371 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests + { + // The emitted value is not valid Xml in this test because the unbalanced comment in the first if + // doesn't cause the pseudo comment to become a real comment. + // The test is to demonstrate that the balance checking gets reset after leaving the #if-#endif block. + // The fact that the comment in the second if gets its final pseudo comment fixed is demonstration of the reset. + [Fact(DisplayName = nameof(VerifyBlockCommentUnbalancedMissingEndCommentsResets))] + public void VerifyBlockCommentUnbalancedMissingEndCommentsResets() + { + string originalValue = @"Start + + + +Intermediate content + + + +End"; + + string expectedValue = @"Start + + + +End"; + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = true, + ["SECOND_IF"] = true + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyBlockCommentUnbalancedExtraEndCommentsResets))] + public void VerifyBlockCommentUnbalancedExtraEndCommentsResets() + { + string originalValue = @"Start + + + +Intermediate content + + + +End"; + + string expectedValue = @"Start + + -- > + +Intermediate content + + + +End"; + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = true, + ["SECOND_IF"] = true + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyBlockCommentedContentStaysCommented))] + public void VerifyBlockCommentedContentStaysCommented() + { + string originalValue = @"Start + + + +End"; + + string expectedValue = @"Start + + + Actual content + + +End"; + VariableCollection vc = new VariableCollection + { + ["VALUE"] = true, + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + /// + /// Temporary test, experimenting with block comments. + /// + [Fact(DisplayName = nameof(VerifyMultipleConsecutiveTrailingCommentsWithinContent))] + public void VerifyMultipleConsecutiveTrailingCommentsWithinContent() + { + string originalValue = @"Start + + +End"; + string expectedValue = @"Start + +End"; + + VariableCollection vc = new VariableCollection + { + ["IF_VALUE"] = true, + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyMultipleEndCommentsOnEndif))] + public void VerifyMultipleEndCommentsOnEndif() + { + string originalValue = @"Start + +End"; + string expectedValue = @"Start + Content: Outer if + Content: Inner if +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // test for 3 levels deep + string threePartOriginalValue = @"Start + +End"; + string threePartExpectedValue = @"Start + Content: Outer if + Content: Inner if + Content: Third if +End"; + + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true, + ["THIRD_IF"] = true + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(threePartOriginalValue, threePartExpectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyMultipleEndCommentsOnElseif))] + public void VerifyMultipleEndCommentsOnElseif() + { + string originalValue = @"Start + + Content: Inner elseif (default) + +End"; + string ifIfExpectedValue = @"Start + Content: Outer if + Content: Inner if +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = true, + ["INNER_ELSEIF"] = false + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, ifIfExpectedValue, processor, 9999); + + string ifElseifExpectedValue = @"Start + Content: Outer if + Content: Inner elseif (default) +End"; + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false, + ["INNER_ELSEIF"] = true + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, ifElseifExpectedValue, processor, 9999); + + string ifElseExpectedValue = @"Start + Content: Outer if + Content: Inner else +End"; + vc = new VariableCollection + { + ["OUTER_IF"] = true, + ["INNER_IF"] = false, + ["INNER_ELSEIF"] = false + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, ifElseExpectedValue, processor, 9999); + } + + // Tests 3-level nesting of if blocks + // Tests multiple elseif's in the same block + [Fact(DisplayName = nameof(VerifyThreeLevelNestedBlockComments))] + public void VerifyThreeLevelNestedBlockComments() + { + string originalValue = @"Start + + #elseif (IF_ELSEIF_LEVEL_2) + Content: If Elseif Level 2 + + #elseif (IF_ELSEIF_TWO_LEVEL_2) + Content: If Elseif Two Level 2 + + #else + Content: If Else Level 2 + + #endif--> +#elseif (ELSEIF_LEVEL_1) + Content: Elseif Level 1 +#else + Content: Else Level 1 +#endif--> +End"; + // if-if-if + string expectedValue = @"Start + Content: If Level 1 + Content: If IF Level 2 + Content: If IF If Level 3 +End"; + VariableCollection vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = true, + ["IF_IF_IF_LEVEL_3"] = true, + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-if-elseif + expectedValue = @"Start + Content: If Level 1 + Content: If IF Level 2 + Content: If If Elseif Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = true, + ["IF_IF_IF_LEVEL_3"] = false, + ["IF_IF_ELSEIF_LEVEL_3"] = true + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-if-elseif2 + expectedValue = @"Start + Content: If Level 1 + Content: If IF Level 2 + Content: If If Elseif Two Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = true, + ["IF_IF_IF_LEVEL_3"] = false, + ["IF_IF_ELSEIF_LEVEL_3"] = false, + ["IF_IF_ELSEIF_TWO_LEVEL_3"] = true + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-if-else + expectedValue = @"Start + Content: If Level 1 + Content: If IF Level 2 + Content: If If Else Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = true, + ["IF_IF_IF_LEVEL_3"] = false, + ["IF_IF_ELSEIF_LEVEL_3"] = false, + ["IF_IF_ELSEIF_TWO_LEVEL_3"] = false + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-elseif-if + expectedValue = @"Start + Content: If Level 1 + Content: If Elseif Level 2 + Content: If Elseif If Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = false, + ["IF_ELSEIF_LEVEL_2"] = true, + ["IF_ELSEIF_IF_LEVEL_3"] = true, + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-elseif-elseif + expectedValue = @"Start + Content: If Level 1 + Content: If Elseif Level 2 + Content: If Elseif Elseif Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = false, + ["IF_ELSEIF_LEVEL_2"] = true, + ["IF_ELSEIF_IF_LEVEL_3"] = false, + ["IF_ELSEIF_ELSEIF_LEVEL_3"] = true, + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-elseif-else + expectedValue = @"Start + Content: If Level 1 + Content: If Elseif Level 2 + Content: If Elseif Else Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = false, + ["IF_ELSEIF_LEVEL_2"] = true, + ["IF_ELSEIF_IF_LEVEL_3"] = false, + ["IF_ELSEIF_ELSEIF_LEVEL_3"] = false, + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-elseif two-if + expectedValue = @"Start + Content: If Level 1 + Content: If Elseif Two Level 2 + Content: If Elseif Two If Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = false, + ["IF_ELSEIF_LEVEL_2"] = false, + ["IF_ELSEIF_TWO_LEVEL_2"] = true, + ["IF_ELSEIF_TWO_IF_LEVEL_3"] = true, + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-elseif two-elseif + expectedValue = @"Start + Content: If Level 1 + Content: If Elseif Two Level 2 + Content: If Elseif Two Elseif Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = false, + ["IF_ELSEIF_LEVEL_2"] = false, + ["IF_ELSEIF_TWO_LEVEL_2"] = true, + ["IF_ELSEIF_TWO_IF_LEVEL_3"] = false, + ["IF_ELSEIF_TWO_ELSEIF_LEVEL_3"] = true + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-elseif two-else + expectedValue = @"Start + Content: If Level 1 + Content: If Elseif Two Level 2 + Content: If Elseif Two Else Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = false, + ["IF_ELSEIF_LEVEL_2"] = false, + ["IF_ELSEIF_TWO_LEVEL_2"] = true, + ["IF_ELSEIF_TWO_IF_LEVEL_3"] = false, + ["IF_ELSEIF_TWO_ELSEIF_LEVEL_3"] = false + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-else-if + expectedValue = @"Start + Content: If Level 1 + Content: If Else Level 2 + Content: If Else If Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = false, + ["IF_ELSEIF_LEVEL_2"] = false, + ["IF_ELSEIF_TWO_LEVEL_2"] = false, + ["IF_ELSE_IF_LEVEL_3"] = true, + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-else-elseif + expectedValue = @"Start + Content: If Level 1 + Content: If Else Level 2 + Content: If Else Elseif Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = false, + ["IF_ELSEIF_LEVEL_2"] = false, + ["IF_ELSEIF_TWO_LEVEL_2"] = false, + ["IF_ELSE_IF_LEVEL_3"] = false, + ["IF_ELSE_ELSEIF_LEVEL_3"] = true + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + + // if-else-else + expectedValue = @"Start + Content: If Level 1 + Content: If Else Level 2 + Content: If Else Else Level 3 +End"; + vc = new VariableCollection + { + ["IF_LEVEL_1"] = true, + ["IF_IF_LEVEL_2"] = false, + ["IF_ELSEIF_LEVEL_2"] = false, + ["IF_ELSEIF_TWO_LEVEL_2"] = false, + ["IF_ELSE_IF_LEVEL_3"] = false, + ["IF_ELSE_ELSEIF_LEVEL_3"] = false + }; + processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + /// + /// Temporary test, experimenting with block comments. + /// + [Fact(DisplayName = nameof(VerifyMultipleNestedBlockComments))] + public void VerifyMultipleNestedBlockComments() + { + // the actual tests for OUTER_IF_CLAUSE = true (inner else also happens because the other inners are false) + string expectedValue = @"Start + content: outer-if + content: inner-else +Trailing stuff +"; + + VariableCollection vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = true, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = false, + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + + // comment spans from inner if to outer endif + // comments are unbalanced + // invalid ??? + string inputValue = @"Start + + content: outer-if + +Trailing stuff +"; + RunAndVerify(inputValue, expectedValue, processor, 9999); + + // comments are balanced + string inputValue2 = @"Start + + content: outer-if + +Trailing stuff +"; + + RunAndVerify(inputValue2, expectedValue, processor, 9999); + + // inner elseif is default, comments are balanced and nesting-balanced. + string inputValue3 = @"Start + + content: inner-elseif + + +Trailing stuff +"; + RunAndVerify(inputValue3, expectedValue, processor, 9999); + } + + [Fact(DisplayName = nameof(VerifyXmlBlockCommentsNestedInIf_ProperComments))] + public void VerifyXmlBlockCommentsNestedInIf_ProperComments() + { + string originalValue = @"Start + + +End"; + + string expectedValue = @"Start + content: outer if + content: inner if + +End"; + VariableCollection vc = new VariableCollection + { + ["OUTER_IF_VALUE"] = true, + ["INNER_IF_VALUE"] = true, + ["INNER_ELSEIF_VALUE"] = true, // irrelevant + ["OUTER_ELSEIF_VALUE"] = true, // irrelevant + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + // Below tests may not have properly formatted comments, but still work + + [Fact(DisplayName = nameof(XmlBlockCommentBasicTest))] + public void XmlBlockCommentBasicTest() + { + IList testCases = new List(); + + string basicValue = @"Start + +Trailing stuff"; + testCases.Add(basicValue); + + string basicWithDefault = @"Start + + content: if + +Trailing stuff"; + testCases.Add(basicWithDefault); + + string expectedValueIfEmits = @"Start + content: if +Trailing stuff"; + + VariableCollection vc = new VariableCollection + { + ["CLAUSE"] = true, + ["CLAUSE_2"] = true, // irrelevant + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + + foreach (string test in testCases) + { + RunAndVerify(test, expectedValueIfEmits, processor, 9999); + } + + // change the clause values so the elseif emits + string expectedValueElseifEmits = @"Start + content: elseif +Trailing stuff"; + + vc = new VariableCollection + { + ["CLAUSE"] = false, + ["CLAUSE_2"] = true, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, expectedValueElseifEmits, processor, 9999); + } + + // change the clause values so the else emits + string expectedValueElseEmits = @"Start + content: else +Trailing stuff"; + vc = new VariableCollection + { + ["CLAUSE"] = false, + ["CLAUSE_2"] = false, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, expectedValueElseEmits, processor, 9999); + } + } + + /// + /// This test fails + /// + /// Test cases for conditionals in xml block comments. + /// Comment stripping is needed for some of these. + /// + [Fact(DisplayName = nameof(XmlBlockCommentIfElseifElseTestWithCommentStripping))] + public void XmlBlockCommentIfElseifElseTestWithCommentStripping() + { + IList testCases = new List(); + + string originalValue = @"Start + + content: default stuff in the elseif + content: default line 2 (elseif) + +Trailing stuff +"; + testCases.Add(originalValue); + + string fullerCommentsOnlyValue = @"Start + + content: default stuff in the elseif + content: default line 2 (elseif) + +Trailing stuff +"; + testCases.Add(fullerCommentsOnlyValue); + + // note that there is no default here + string oneBigCommentValue = @"Start + +Trailing stuff +"; + testCases.Add(oneBigCommentValue); + + string ifTrueExpectedValue = @"Start + content: if stuff, line 1 + content: if stuff line 2 +Trailing stuff +"; + VariableCollection vc = new VariableCollection + { + ["IF_CLAUSE"] = true, + ["ELSEIF_CLAUSE"] = false, + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, ifTrueExpectedValue, processor, 9999); + } + + string elseifTrueExpectedValue = @"Start + content: default stuff in the elseif + content: default line 2 (elseif) +Trailing stuff +"; + vc = new VariableCollection + { + ["IF_CLAUSE"] = false, + ["ELSEIF_CLAUSE"] = true, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, elseifTrueExpectedValue, processor, 9999); + } + + string elseHappensExpectedValue = @"Start + content: else stuff, line 1 + content: default stuff in the else + content: trailing else stuff, not default +Trailing stuff +"; + vc = new VariableCollection + { + ["IF_CLAUSE"] = false, + ["ELSEIF_CLAUSE"] = false, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, elseHappensExpectedValue, processor, 9999); + } + } + + /// + /// Tests basic conditional embedding for block XML comments. + /// + [Fact(DisplayName = nameof(VerifyXmlBlockCommentEmbeddedInIfTest))] + public void VerifyXmlBlockCommentEmbeddedInIfTest() + { + IList testCases = new List(); + + string noDefaultValue = @"Start + +Trailing stuff +"; + testCases.Add(noDefaultValue); + + string outerIfDefaultValue = @"Start + + content: outer-if + +Trailing stuff +"; + testCases.Add(outerIfDefaultValue); + + string innerIfDefaultValue = @"Start + + content: inner-if + +Trailing stuff +"; + testCases.Add(innerIfDefaultValue); + + string innerElseifDefaultValue = @"Start + + content: outer-if + + content: inner-elseif + +Trailing stuff +"; + testCases.Add(innerElseifDefaultValue); + + string innerElseDefaultValue = @"Start + + content: inner-else + +Trailing stuff +"; + testCases.Add(innerElseDefaultValue); + + string outerElseifDefaultValue = @"Start + + content: outer-elseif +#else + content: outer-else +#endif--> +Trailing stuff +"; + testCases.Add(outerElseifDefaultValue); + + string outerElseDefaultValue = @"Start + + content: outer-else +#endif--> +Trailing stuff +"; + testCases.Add(outerElseDefaultValue); + + // the actual tests for OUTER_IF_CLAUSE = true (inner else also happens because the other inners are false) + string outerIfTrueExpectedValue = @"Start + content: outer-if + content: inner-else +Trailing stuff +"; + + VariableCollection vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = true, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = false, + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, outerIfTrueExpectedValue, processor, 9999); + } + + // the actual tests for INNER_IF_CLAUSE = true (the OUTER_IF_CLAUSE must be true for this to matter) + string innerIfTrueExpectedValue = @"Start + content: outer-if + content: inner-if +Trailing stuff +"; + + vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = true, + ["INNER_IF_CLAUSE"] = true, + ["INNER_ELSEIF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = false, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, innerIfTrueExpectedValue, processor, 9999); + } + + // the actual tests for INNER_ELSEIF_CLAUSE = true (the OUTER_IF_CLAUSE must be true for this to matter) + string innerElseifTrueExpectedValue = @"Start + content: outer-if + content: inner-elseif +Trailing stuff +"; + + vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = true, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = true, + ["OUTER_ELSEIF_CLAUSE"] = false, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, innerElseifTrueExpectedValue, processor, 9999); + } + + // the actual tests for OUTER_ELSEIF_CLAUSE = true (the OUTER_IF_CLAUSE must be false for this to matter) + string outerElseifTrueExpectedValue = @"Start + content: outer-elseif +Trailing stuff +"; + + vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = false, // irrelevant + ["INNER_ELSEIF_CLAUSE"] = false, // irrelevant + ["OUTER_ELSEIF_CLAUSE"] = true, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, outerElseifTrueExpectedValue, processor, 9999); + } + + // the actual tests for when the outer else happens + string outerElseTrueExpectedValue = @"Start + content: outer-else +Trailing stuff +"; + + vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = false, // irrelevant + ["INNER_ELSEIF_CLAUSE"] = false, // irrelevant + ["OUTER_ELSEIF_CLAUSE"] = false, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, outerElseTrueExpectedValue, processor, 9999); + } + } + + /// + /// Temporary test for isolating bugs. + /// + [Fact(DisplayName = nameof(MinimalXmlElseifEmbeddingTest))] + public void MinimalXmlElseifEmbeddingTest() + { + string testValue = @"Start + +Trailing stuff +"; + + string outerIfTrueExpectedValue = @"Start + content: outer-else +Trailing stuff +"; + + VariableCollection vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false, + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + RunAndVerify(testValue, outerIfTrueExpectedValue, processor, 9999); + } + + /// + /// Tests block comment embedding of conditionals in the elseif. + /// + [Fact(DisplayName = nameof(VerifyXmlBlockCommentEmbeddedInElseifTest))] + public void VerifyXmlBlockCommentEmbeddedInElseifTest() + { + IList testCases = new List(); + string noDefaultValue = @"Start + +Trailing stuff +"; + testCases.Add(noDefaultValue); + + string outerIfDefaultValue = @"Start + + content: outer-if + +Trailing stuff +"; + testCases.Add(outerIfDefaultValue); + + string innerIfDefaultValue = @"Start + + content: inner-if + +Trailing stuff +"; + testCases.Add(innerIfDefaultValue); + + string innerElseifDefaultValue = @"Start + + content: inner-elseif + +Trailing stuff +"; + testCases.Add(innerElseifDefaultValue); + + string innerElseDefaultValue = @"Start + + content: inner-else + +Trailing stuff +"; + testCases.Add(innerElseDefaultValue); + + string outerElseifDefaultValue = @"Start + + content: outer-elseif + +Trailing stuff +"; + testCases.Add(outerElseifDefaultValue); + + string outerElseDefaultValue = @"Start + + content: outer-else +#endif--> +Trailing stuff +"; + testCases.Add(outerElseDefaultValue); + + // the actual tests for OUTER_IF_CLAUSE = true + string outerIfTrueExpectedValue = @"Start + content: outer-if +Trailing stuff +"; + + VariableCollection vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = true, + ["OUTER_ELSEIF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false, + }; + IProcessor processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, outerIfTrueExpectedValue, processor, 9999); + } + + // the actual tests for OUTER_ELSEIF_CLAUSE = true (inner else happens too) + string outerElseifTrueExpectedValue = @"Start + content: outer-elseif + content: inner-else +Trailing stuff +"; + + vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = true, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, outerElseifTrueExpectedValue, processor, 9999); + } + + // the actual tests for OUTER_ELSEIF_CLAUSE = true and INNER_IF_CLAUSE = true + string outerElseifTrueInnerIfTrueExpectedValue = @"Start + content: outer-elseif + content: inner-if +Trailing stuff +"; + + vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = true, + ["INNER_IF_CLAUSE"] = true, + ["INNER_ELSEIF_CLAUSE"] = false, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, outerElseifTrueInnerIfTrueExpectedValue, processor, 9999); + } + + // the actual tests for OUTER_ELSEIF_CLAUSE = true and INNER_ELSEIF_CLAUSE = true + string outerElseifTrueInnerElseifTrueExpectedValue = @"Start + content: outer-elseif + content: inner-elseif +Trailing stuff +"; + + vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = true, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = true, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, outerElseifTrueInnerElseifTrueExpectedValue, processor, 9999); + } + + // the actual tests for the outer else happening + string outerElseHappensExpectedValue = @"Start + content: outer-else +Trailing stuff +"; + + vc = new VariableCollection + { + ["OUTER_IF_CLAUSE"] = false, + ["OUTER_ELSEIF_CLAUSE"] = false, + ["INNER_IF_CLAUSE"] = false, + ["INNER_ELSEIF_CLAUSE"] = false, + }; + processor = SetupXmlStyleProcessor(vc); + foreach (string test in testCases) + { + RunAndVerify(test, outerElseHappensExpectedValue, processor, 9999); + } + } + +#pragma warning disable xUnit1004 // Test methods should not be skipped + [Fact(Skip = "https://github.com/dotnet/templating/issues/4988")] +#pragma warning restore xUnit1004 // Test methods should not be skipped + public void VerifyXMLConditionAtEnd() + { + string value = @"Hello + +"; + string expected = @"Hello +"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection(); + IProcessor processor = SetupXmlStyleProcessor(vc); + + //Changes should be made + bool changed = processor.Run(input, output, 50); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.cs new file mode 100644 index 000000000000..e5eaba45fc35 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ConditionalTests.cs @@ -0,0 +1,394 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Expressions.Cpp; +using Microsoft.TemplateEngine.Core.Expressions.VisualBasic; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public partial class ConditionalTests : TestBase, IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public ConditionalTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: this.GetType().Name, virtualize: true); + } + + /// + /// Second attempt at xml comment processing. + /// + private static IOperationProvider[] XmlStyleCommentConditionalsOperations + { + get + { + // This is the operationId (flag) for the balanced nesting + string commentFixingOperationId = "Fix pseudo comments"; + + // This is not an operationId (flag), it does not toggle the operation. + // But conditional doesn't care, it takes the flags its given and sets them as appropriate. + // It lets BalanceNesting know it's been reset + string commentFixingResetId = "Reset pseudo comment fixer"; + + ConditionalTokens tokenVariants = new ConditionalTokens + { + EndIfTokens = new[] { "#endif", "".TokenConfig(), "-- >".TokenConfig(), commentFixingOperationId, commentFixingResetId, false) + }; + + return operations; + } + } + + /// + /// Returns an IOperationProvider setup for razor style comment processing. + /// + private static IOperationProvider[] RazorStyleCommentConditionalsOperations + { + get + { + // This is the operationId (flag) for the balanced nesting + string commentFixingOperationId = "Fix pseudo comments"; + + // This is not an operationId (flag), it does not toggle the operation. + // But conditional doesn't care, it takes the flags its given and sets them as appropriate. + // Tt lets BalanceNesting know it's been reset + string commentFixingResetId = "Reset pseudo comment fixer"; + + ConditionalTokens tokenVariants = new ConditionalTokens + { + EndIfTokens = new[] { "#endif", "@*#endif" }.TokenConfigs(), + ActionableIfTokens = new[] { "@*#if" }.TokenConfigs(), + ActionableElseTokens = new[] { "#else", "@*#else" }.TokenConfigs(), + ActionableElseIfTokens = new[] { "#elseif", "@*#elseif", "#elif", "@*#elif" }.TokenConfigs(), + ActionableOperations = new[] { commentFixingOperationId, commentFixingResetId } + }; + + IOperationProvider[] operations = + { + new Conditional(tokenVariants, true, true, CppStyleEvaluatorDefinition.Evaluate, null, true), + new BalancedNesting("@*".TokenConfig(), "*@".TokenConfig(), "* @".TokenConfig(), commentFixingOperationId, commentFixingResetId, false) + }; + + return operations; + } + } + + /// + /// This started as a proof-of-concept / demonstration of having multiple tokens of each type, + /// not to mention the arbitrariness of the conditional tokens. + /// It could go away, in conjunction with updating the unit tests that use it. + /// + private static IOperationProvider[] MadeUpConditionalsOperations + { + get + { + // this is normally handled in the config setup + string replaceOperationId = "Replacement (//) ()"; + string uncommentOperationId = "Uncomment (////) -> (//)"; + + ConditionalTokens tokenVariants = new ConditionalTokens + { + IfTokens = new[] { "//#if", "//#check" }.TokenConfigs(), + ElseTokens = new[] { "//#else", "//#otherwise" }.TokenConfigs(), + ElseIfTokens = new[] { "//#elseif", "//#nextcheck" }.TokenConfigs(), + EndIfTokens = new[] { "//#endif", "//#stop", "//#done", "//#nomore" }.TokenConfigs(), + ActionableIfTokens = new[] { "////#if", "////#check", "//#Z_if" }.TokenConfigs(), + ActionableElseTokens = new[] { "////#else", "////#otherwise", "//#Z_else" }.TokenConfigs(), + ActionableElseIfTokens = new[] { "////#elseif", "////#nextcheck", "//#Z_elseif" }.TokenConfigs(), + ActionableOperations = new[] { replaceOperationId, uncommentOperationId } + }; + + IOperationProvider[] operations = + { + new Conditional(tokenVariants, true, true, CppStyleEvaluatorDefinition.Evaluate, null, true), + new Replacement("////".TokenConfig(), "//", uncommentOperationId, false), + new Replacement("//".TokenConfig(), string.Empty, replaceOperationId, false) + }; + + return operations; + } + } + + private static IOperationProvider[] CStyleWithCommentsConditionalOperations + { + get + { + // this is normally handled in the config setup + string replaceOperationId = "Replacement: (//) ()"; + string uncommentOperationId = "Uncomment (////) -> (//)"; + + ConditionalTokens tokenVariants = new ConditionalTokens + { + IfTokens = new[] { "//#if" }.TokenConfigs(), + ElseTokens = new[] { "//#else" }.TokenConfigs(), + ElseIfTokens = new[] { "//#elseif", "//#elif" }.TokenConfigs(), + EndIfTokens = new[] { "//#endif", "////#endif" }.TokenConfigs(), + ActionableIfTokens = new[] { "////#if" }.TokenConfigs(), + ActionableElseIfTokens = new[] { "////#elseif", "////#elif" }.TokenConfigs(), + ActionableElseTokens = new[] { "////#else" }.TokenConfigs(), + ActionableOperations = new[] { replaceOperationId, uncommentOperationId } + }; + + IOperationProvider[] operations = + { + new Conditional(tokenVariants, true, true, CppStyleEvaluatorDefinition.Evaluate, null, true), + new Replacement("////".TokenConfig(), "//", uncommentOperationId, false), + new Replacement("//".TokenConfig(), string.Empty, replaceOperationId, false) + }; + + return operations; + } + } + + private static IOperationProvider[] CStyleNoCommentsConditionalOperations + { + get + { + ConditionalTokens tokenVariants = new ConditionalTokens + { + IfTokens = new[] { "#if" }.TokenConfigs(), + ElseTokens = new[] { "#else" }.TokenConfigs(), + ElseIfTokens = new[] { "#elseif", "#elif" }.TokenConfigs(), + EndIfTokens = new[] { "#endif" }.TokenConfigs() + }; + + IOperationProvider[] operations = + { + new Conditional(tokenVariants, true, true, CppStyleEvaluatorDefinition.Evaluate, null, true) + }; + + return operations; + } + } + + private static IOperationProvider[] VBStyleNoCommentsConditionalOperations + { + get + { + ConditionalTokens tokenVariants = new ConditionalTokens + { + IfTokens = new[] { "#If" }.TokenConfigs(), + ElseTokens = new[] { "#Else" }.TokenConfigs(), + ElseIfTokens = new[] { "#ElseIf" }.TokenConfigs(), + EndIfTokens = new[] { "#End If" }.TokenConfigs() + }; + + IOperationProvider[] operations = + { + new Conditional(tokenVariants, true, true, VisualBasicStyleEvaluatorDefintion.Evaluate, null, true) + }; + + return operations; + } + } + + private static IOperationProvider[] HashSignLineCommentConditionalOperations + { + get + { + string uncommentOperationId = "Uncomment (hash line): (##) -> (#)"; + string replaceOperationId = "Replacement (hash line): (#) -> ()"; + + ConditionalTokens tokens = new ConditionalTokens + { + IfTokens = new[] { "#if" }.TokenConfigs(), + ElseTokens = new[] { "#else" }.TokenConfigs(), + ElseIfTokens = new[] { "#elseif", "#elif" }.TokenConfigs(), + EndIfTokens = new[] { "#endif", "##endif" }.TokenConfigs(), + ActionableIfTokens = new[] { "##if" }.TokenConfigs(), + ActionableElseIfTokens = new[] { "##elseif", "##elif" }.TokenConfigs(), + ActionableElseTokens = new[] { "##else" }.TokenConfigs(), + ActionableOperations = new[] { replaceOperationId, uncommentOperationId } + }; + + IOperationProvider[] operations = + { + new Conditional(tokens, true, true, CppStyleEvaluatorDefinition.Evaluate, null, true), + new Replacement("##".TokenConfig(), "#", uncommentOperationId, false), + new Replacement("#".TokenConfig(), string.Empty, replaceOperationId, false), + }; + + return operations; + } + } + + private static IOperationProvider[] BatFileRemLineCommentConditionalOperations + { + get + { + string uncommentOperationId = "Uncomment (bat rem): (rem rem) -> (rem)"; + string replaceOperationId = "Replacement (bat rem): (rem) -> ()"; + + ConditionalTokens tokens = new ConditionalTokens + { + IfTokens = new[] { "rem #if" }.TokenConfigs(), + ElseTokens = new[] { "rem #else" }.TokenConfigs(), + ElseIfTokens = new[] { "rem #elseif", "rem #elif" }.TokenConfigs(), + EndIfTokens = new[] { "rem #endif", "rem rem #endif" }.TokenConfigs(), + ActionableIfTokens = new[] { "rem rem #if" }.TokenConfigs(), + ActionableElseIfTokens = new[] { "rem rem #elseif", "rem rem #elif" }.TokenConfigs(), + ActionableElseTokens = new[] { "rem rem #else" }.TokenConfigs(), + ActionableOperations = new[] { replaceOperationId, uncommentOperationId } + }; + + IOperationProvider[] operations = + { + new Conditional(tokens, true, true, CppStyleEvaluatorDefinition.Evaluate, null, true), + new Replacement("rem rem".TokenConfig(), "rem", uncommentOperationId, false), + new Replacement("rem".TokenConfig(), string.Empty, replaceOperationId, false) + }; + + return operations; + } + } + + private static IOperationProvider[] HamlLineCommentConditionalOperations + { + get + { + string reduceCommentOperationId = "Reduce comment (line): (-#-#) -> (-#)"; + string uncommentOperationId = "Uncomment (line): (-#) -> ()"; + + ConditionalTokens tokens = new ConditionalTokens + { + IfTokens = new[] { "-#if" }.TokenConfigs(), + ElseTokens = new[] { "-#else" }.TokenConfigs(), + ElseIfTokens = new[] { "-#elseif", "-#elif" }.TokenConfigs(), + EndIfTokens = new[] { "-#endif", "-#-#endif" }.TokenConfigs(), + ActionableIfTokens = new[] { "-#-#if" }.TokenConfigs(), + ActionableElseIfTokens = new[] { "-#-#elseif", "-#-#elif" }.TokenConfigs(), + ActionableElseTokens = new[] { "-#-#else" }.TokenConfigs(), + ActionableOperations = new[] { uncommentOperationId, reduceCommentOperationId } + }; + + IOperationProvider[] operations = + { + new Conditional(tokens, true, true, CppStyleEvaluatorDefinition.Evaluate, null, true), + new Replacement("-#-#".TokenConfig(), "-#", reduceCommentOperationId, false), + new Replacement("-#".TokenConfig(), string.Empty, uncommentOperationId, false), + }; + + return operations; + } + } + + /// + /// Returns an IOperationProvider setup for razor style comment processing. + /// + private static IOperationProvider[] JsxBlockCommentConditionalsOperations + { + get + { + // This is the operationId (flag) for the balanced nesting + string commentFixingOperationId = "Fix pseudo comments"; + + // This is not an operationId (flag), it does not toggle the operation. + // But conditional doesn't care, it takes the flags its given and sets them as appropriate. + // It lets BalanceNesting know it's been reset + string commentFixingResetId = "Reset pseudo comment fixer"; + + ConditionalTokens tokenVariants = new ConditionalTokens + { + EndIfTokens = new[] { "#endif", "{/*#endif" }.TokenConfigs(), + ActionableIfTokens = new[] { "{/*#if" }.TokenConfigs(), + ActionableElseTokens = new[] { "#else", "{/*#else" }.TokenConfigs(), + ActionableElseIfTokens = new[] { "#elseif", "{/*#elseif", "#elif", "{/*#elif" }.TokenConfigs(), + ActionableOperations = new[] { commentFixingOperationId, commentFixingResetId } + }; + + IOperationProvider[] operations = + { + new Conditional(tokenVariants, true, true, CppStyleEvaluatorDefinition.Evaluate, null, true), + new BalancedNesting("{/*".TokenConfig(), "*/}".TokenConfig(), "*/ }".TokenConfig(), commentFixingOperationId, commentFixingResetId, false) + }; + + return operations; + } + } + + internal IProcessor SetupCStyleWithCommentsProcessor(VariableCollection vc) + { + IOperationProvider[] operations = CStyleWithCommentsConditionalOperations; + return SetupTestProcessor(operations, vc); + } + + protected IProcessor SetupRazorStyleProcessor(VariableCollection vc) + { + IOperationProvider[] operations = RazorStyleCommentConditionalsOperations; + return SetupTestProcessor(operations, vc); + } + + protected IProcessor SetupMadeUpStyleProcessor(VariableCollection vc) + { + IOperationProvider[] operations = MadeUpConditionalsOperations; + return SetupTestProcessor(operations, vc); + } + + protected IProcessor SetupXmlStyleProcessor(VariableCollection vc) + { + IOperationProvider[] operations = XmlStyleCommentConditionalsOperations; + return SetupTestProcessor(operations, vc); + } + + protected IProcessor SetupCStyleNoCommentsProcessor(VariableCollection vc) + { + IOperationProvider[] operations = CStyleNoCommentsConditionalOperations; + return SetupTestProcessor(operations, vc); + } + + protected IProcessor SetupVBStyleNoCommentsProcessor(VariableCollection vc) + { + IOperationProvider[] operations = VBStyleNoCommentsConditionalOperations; + return SetupTestProcessor(operations, vc); + } + + protected IProcessor SetupHashSignLineCommentsProcessor(VariableCollection vc) + { + IOperationProvider[] operations = HashSignLineCommentConditionalOperations; + return SetupTestProcessor(operations, vc); + } + + protected IProcessor SetupBatFileRemLineCommentsProcessor(VariableCollection vc) + { + IOperationProvider[] operations = BatFileRemLineCommentConditionalOperations; + return SetupTestProcessor(operations, vc); + } + + protected IProcessor SetupHamlLineCommentsProcessor(VariableCollection vc) + { + IOperationProvider[] operations = HamlLineCommentConditionalOperations; + return SetupTestProcessor(operations, vc); + } + + protected IProcessor SetupJsxBlockCommentsProcessor(VariableCollection vc) + { + IOperationProvider[] operations = JsxBlockCommentConditionalsOperations; + return SetupTestProcessor(operations, vc); + } + + /// + /// Sets up a processor with the input params. + /// + private IProcessor SetupTestProcessor(IOperationProvider[] operations, VariableCollection vc) + { + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, vc); + return Processor.Create(cfg, operations); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/Cpp2EvaluatorTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/Cpp2EvaluatorTests.cs new file mode 100644 index 000000000000..2f686f99ef43 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/Cpp2EvaluatorTests.cs @@ -0,0 +1,246 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Core.Expressions.Cpp2; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class Cpp2EvaluatorTests : TestBase, IClassFixture + { + private readonly ILogger _logger; + + public Cpp2EvaluatorTests(TestLoggerFactory testLoggerFactory) + { + _logger = testLoggerFactory.CreateLogger(); + } + + [Fact] + public void VerifyCpp2EvaluatorTrueLiteral() + { + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "true", new VariableCollection(), out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.True(result); + } + + [Fact] + public void VerifyCpp2EvaluatorFalseLiteral() + { + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "false", new VariableCollection(), out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.False(result); + } + + [Fact] + public void VerifyCpp2EvaluatorStringLiteral() + { + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "blah blah", new VariableCollection(), out string? faultedMessage); + Assert.NotNull(faultedMessage); + Assert.False(result); + Assert.Contains("was not recognized as a valid Boolean", faultedMessage); + } + + [Fact] + public void VerifyCpp2EvaluatorUnknownVariable() + { + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "FIRST_IF == true", new VariableCollection(), out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.False(result); + } + + [Fact] + public void VerifyCpp2EvaluatorUnknownVariableEroneousExpression() + { + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "FIRST_IF == true blah blah", new VariableCollection(), out string? faultedMessage); + Assert.NotNull(faultedMessage); + Assert.False(result); + } + + [Fact] + public void VerifyCpp2EvaluatorTrueStringVariable() + { + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = "true" + }; + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "FIRST_IF", vc, out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.True(result); + } + + [Fact] + public void VerifyCpp2EvaluatorTrueBooleanVariable() + { + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = true + }; + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "FIRST_IF", vc, out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.True(result); + } + + [Fact] + public void VerifyCpp2EvaluatorTrueVariableErroneousExpression() + { + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = true + }; + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "FIRST_IF = false", vc, out string? faultedMessage); + Assert.NotNull(faultedMessage); + Assert.True(result); + } + + [Fact(DisplayName = nameof(VerifyCpp2EvaluatorFalse))] + public void VerifyCpp2EvaluatorFalse() + { + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = false + }; + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "FIRST_IF", vc, out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.False(result); + } + + [Fact(DisplayName = nameof(VerifyCpp2EvaluatorAndEqualsNot))] + public void VerifyCpp2EvaluatorAndEqualsNot() + { + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = false, + ["SECOND_IF"] = false + }; + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "FIRST_IF == SECOND_IF && !FIRST_IF ", vc, out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.True(result); + } + + [Fact(DisplayName = nameof(VerifyCpp2EvaluatorUsedVariablesSet))] + public void VerifyCpp2EvaluatorUsedVariablesSet() + { + VariableCollection vc = new VariableCollection + { + ["FIRST_IF2"] = false, + ["FIRST_IF3"] = false, + ["FIRST_IF0"] = false, + ["FIRST_IF"] = false, + ["BB"] = false, + ["0SECOND_IF"] = false, + ["SECOND_IF"] = false, + ["5SECOND_IF"] = false, + ["BB2"] = false, + ["BB3"] = false, + + }; + HashSet keys = new HashSet(); + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "FIRST_IF == SECOND_IF && !FIRST_IF", vc, out string? faultedMessage, keys); + Assert.Null(faultedMessage); + Assert.True(result); + Assert.Equal(2, keys.Count); + Assert.True(keys.SequenceEqual(new[] { "FIRST_IF", "SECOND_IF" })); + } + + [Fact(DisplayName = nameof(VerifyCpp2EvaluatorBitShiftAddEquals))] + public void VerifyCpp2EvaluatorBitShiftAddEquals() + { + VariableCollection vc = new VariableCollection + { + ["FIRST"] = 8, + ["SECOND"] = 5 + }; + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "FIRST >> 1 + 2 == 1 + SECOND", vc, out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.True(result); + } + + [Fact(DisplayName = nameof(VerifyCpp2EvaluatorMultipleNotEqualsAnd))] + public void VerifyCpp2EvaluatorMultipleNotEqualsAnd() + { + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = false, + ["SECOND_IF"] = false + }; + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "!!!FIRST_IF && !SECOND_IF == !FIRST_IF", vc, out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.True(result); + } + + [Fact(DisplayName = nameof(VerifyCpp2EvaluatorStringEquals))] + public void VerifyCpp2EvaluatorStringEquals() + { + VariableCollection vc = new VariableCollection + { + ["FIRST_IF"] = "1.2.3" + }; + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "FIRST_IF == '1.2.3'", vc, out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.True(result); + } + + [Fact(DisplayName = nameof(VerifyCpp2EvaluatorNumerics))] + public void VerifyCpp2EvaluatorNumerics() + { + VariableCollection vc = new VariableCollection(); + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "0x20 == '32'", vc, out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.True(result); + } + + [Fact(DisplayName = nameof(VerifyCpp2EvaluatorShifts))] + public void VerifyCpp2EvaluatorShifts() + { + VariableCollection vc = new VariableCollection + { + ["FIRST"] = "4", + ["SECOND"] = "64" + }; + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "FIRST << 2 == SECOND >> 2", vc, out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.True(result); + } + + [Fact(DisplayName = nameof(VerifyCpp2EvaluatorMath))] + public void VerifyCpp2EvaluatorMath() + { + VariableCollection vc = new VariableCollection(); + bool result = Cpp2StyleEvaluatorDefinition.EvaluateFromString(_logger, "4 + 9 / (2 + 1) == (0x38 >> 2) / (1 << 0x01)", vc, out string? faultedMessage); + Assert.Null(faultedMessage); + Assert.True(result); + } + + [Fact(DisplayName = nameof(VerifyEvaluableExpressionNoVarsCollectionProvided))] + public void VerifyEvaluableExpressionNoVarsCollectionProvided() + { + VariableCollection vc = new VariableCollection(); + HashSet referencedVariablesKeys = new HashSet(); + var result = Cpp2StyleEvaluatorDefinition.GetEvaluableExpression( + _logger, "navigate == true", vc, out string? faultedMessage, referencedVariablesKeys); + + Assert.Null(faultedMessage); + Assert.NotNull(result); + Assert.Empty(vc); + } + + [Fact(DisplayName = nameof(VerifyEvaluableExpressionVarsCollectionProvided))] + public void VerifyEvaluableExpressionVarsCollectionProvided() + { + VariableCollection vc = new VariableCollection() + { + { "navigate", "true" } + }; + HashSet referencedVariablesKeys = new HashSet(); + var result = Cpp2StyleEvaluatorDefinition.GetEvaluableExpression( + _logger, "navigate == true", vc, out string? faultedMessage, referencedVariablesKeys); + + Assert.Null(faultedMessage); + Assert.NotNull(result); + Assert.Single(referencedVariablesKeys); + Assert.Equal("navigate", referencedVariablesKeys.First()); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/LookaroundTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/LookaroundTests.cs new file mode 100644 index 000000000000..cd7c9a9962f9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/LookaroundTests.cs @@ -0,0 +1,299 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class LookaroundTests : TestBase, IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public LookaroundTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: this.GetType().Name, virtualize: true); + } + + [Fact(DisplayName = nameof(TestLookBehindMatches))] + public void TestLookBehindMatches() + { + string value = @"aababcabcacc"; + string expected = @"aababca!cacc"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Replacement("b".TokenConfigBuilder().OnlyIfAfter("ca"), "!", null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(TestLookAheadMatches))] + public void TestLookAheadMatches() + { + string value = @"aababcabcacc"; + string expected = @"aaba!cabcacc"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Replacement("b".TokenConfigBuilder().OnlyIfBefore("cab"), "!", null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(TestLookAroundMatches))] + public void TestLookAroundMatches() + { + string value = @"aababcabcacc"; + string expected = @"aaba!cabcacc"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Replacement("b".TokenConfigBuilder().OnlyIfBefore("cab").OnlyIfAfter("ba"), "!", null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(TestLookaroundMatchLengthBehavior))] + public void TestLookaroundMatchLengthBehavior() + { + string value = @"background-color:white; +color:white;"; + string expected = @"background-color:blue; +color:red;"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = + { + new Replacement("white".TokenConfigBuilder().OnlyIfBefore(";").OnlyIfAfter("background-color:"), "blue", null, true), + new Replacement("white".TokenConfigBuilder().OnlyIfBefore(";").OnlyIfAfter("color:"), "red", null, true) + }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(TestFullyOverlappedMatchBehavior))] + public void TestFullyOverlappedMatchBehavior() + { + string value = @"foobarbaz"; + string expected = @"abc"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = + { + new Replacement("foo".TokenConfigBuilder().OnlyIfBefore("bar"), "a", null, true), + new Replacement("bar".TokenConfigBuilder().OnlyIfBefore("baz").OnlyIfAfter("foo"), "b", null, true), + new Replacement("baz".TokenConfigBuilder().OnlyIfAfter("bar"), "c", null, true) + }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(TestLookbehindOverlappedMatchBehavior))] + public void TestLookbehindOverlappedMatchBehavior() + { + string value = @"foobarxaz"; + string expected = @"abc"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = + { + new Replacement("foo".TokenConfigBuilder().OnlyIfBefore("bar"), "a", null, true), + new Replacement("bar".TokenConfigBuilder().OnlyIfAfter("foo"), "b", null, true), + new Replacement("xaz".TokenConfigBuilder().OnlyIfAfter("bar"), "c", null, true) + }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(TestLookaheadOverlappedMatchBehavior))] + public void TestLookaheadOverlappedMatchBehavior() + { + string value = @"foobarbaz"; + string expected = @"abc"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = + { + new Replacement("foo".TokenConfigBuilder().OnlyIfBefore("bar"), "a", null, true), + new Replacement("bar".TokenConfigBuilder().OnlyIfBefore("baz"), "b", null, true), + new Replacement("baz".TokenConfigBuilder().OnlyIfAfter("bar"), "c", null, true) + }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(TestReadAheadBreaksLookBehinds))] + public void TestReadAheadBreaksLookBehinds() + { + string value = @"footbarbaz"; + string expected = @"barbaz"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = + { + new MockOperationProvider(new MockOperation(null, ReadaheadOneByte, true, "foo"u8.ToArray())), + new Replacement("bar".TokenConfigBuilder().OnlyIfAfter("foot"), "b", null, true) + }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(TestLookBehindWithValueOverlappingPriorMatchGetsSkipped))] + public void TestLookBehindWithValueOverlappingPriorMatchGetsSkipped() + { + string value = @"foobarbaz"; + string expected = @"aarc"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = + { + new Replacement("foob".TokenConfigBuilder(), "a", null, true), + new Replacement("bar".TokenConfigBuilder().OnlyIfAfter("foo"), "b", null, true), + new Replacement("baz".TokenConfigBuilder().OnlyIfAfter("bar"), "c", null, true) + }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(TestLookBehindCoveringMatchedValueGetsMatched))] + public void TestLookBehindCoveringMatchedValueGetsMatched() + { + string value = @"foobarbaz"; + string expected = @"fooba"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = + { + new Replacement("baz".TokenConfigBuilder().OnlyIfAfter("foobar"), "a", null, true), + new Replacement("bar".TokenConfigBuilder().OnlyIfAfter("foo"), "b", null, true) + }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(TestLookAroundsCanBeUsedForInsertion))] + public void TestLookAroundsCanBeUsedForInsertion() + { + string value = @"foobaz"; + string expected = @"foobarbaz"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = + { + new Replacement(string.Empty.TokenConfigBuilder().OnlyIfAfter("foo").OnlyIfBefore("baz"), "bar", null, true) + }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(TestLongestActualWins))] + public void TestLongestActualWins() + { + string value = @"foobarbaz"; + string expected = @"testarbaz"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = + { + new Replacement("foob".TokenConfigBuilder().OnlyIfBefore("arbaz"), "test", null, true), + new Replacement("foo".TokenConfigBuilder().OnlyIfBefore("barbaz"), "test2", null, true) + }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + private static int ReadaheadOneByte(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + ++currentBufferPosition; + return 0; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/Microsoft.TemplateEngine.Core.UnitTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/Microsoft.TemplateEngine.Core.UnitTests.csproj new file mode 100644 index 000000000000..24c1d04d7009 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/Microsoft.TemplateEngine.Core.UnitTests.csproj @@ -0,0 +1,19 @@ + + + + $(NetCurrent);$(NetFrameworkCurrent) + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/OperationTrieTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/OperationTrieTests.cs new file mode 100644 index 000000000000..ecc67a48cd55 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/OperationTrieTests.cs @@ -0,0 +1,186 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Matching; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class OperationTrieTests + { + private delegate int MatchHandler(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token); + + [Fact(DisplayName = nameof(VerifyOperationTrieFindsTokenAtStart))] + public void VerifyOperationTrieFindsTokenAtStart() + { + OperationTrie trie = OperationTrie.Create(new IOperation[] + { + new MockOperation("Test1", null, true, TokenConfig.LiteralToken(new byte[] { 1, 2, 3, 4 })), + new MockOperation("Test2", null, true, TokenConfig.LiteralToken(new byte[] { 2, 3 })) + }); + + byte[] buffer = { 1, 2, 3, 4, 5 }; + int currentBufferPosition = 0; + IOperation? match = trie.GetOperation(buffer, buffer.Length, ref currentBufferPosition, out int token); + + Assert.NotNull(match); + Assert.Equal("Test1", match.Id); + Assert.Equal(0, token); + Assert.Equal(4, currentBufferPosition); + } + + [Fact(DisplayName = nameof(VerifyOperationTrieFindsTokenAfterStart))] + public void VerifyOperationTrieFindsTokenAfterStart() + { + OperationTrie trie = OperationTrie.Create(new IOperation[] + { + new MockOperation("Test1", null, true, TokenConfig.LiteralToken(new byte[] { 5, 2, 3, 4 })), + new MockOperation("Test2", null, true, TokenConfig.LiteralToken(new byte[] { 4, 6 }), TokenConfig.LiteralToken(new byte[] { 2, 3 })) + }); + + byte[] buffer = { 1, 2, 3, 4, 5 }; + int currentBufferPosition = 0; + IOperation? match = trie.GetOperation(buffer, buffer.Length, ref currentBufferPosition, out _); + + Assert.Null(match); + Assert.Equal(0, currentBufferPosition); + currentBufferPosition = 1; + match = trie.GetOperation(buffer, buffer.Length, ref currentBufferPosition, out int token); + + Assert.NotNull(match); + Assert.Equal("Test2", match.Id); + Assert.Equal(1, token); + Assert.Equal(3, currentBufferPosition); + } + + [Fact(DisplayName = nameof(VerifyOperationTrieFindsTokenAtEnd))] + public void VerifyOperationTrieFindsTokenAtEnd() + { + OperationTrie trie = OperationTrie.Create(new IOperation[] + { + new MockOperation("Test1", null, true, TokenConfig.LiteralToken(new byte[] { 5, 2, 3, 4 })), + new MockOperation("Test2", null, true, TokenConfig.LiteralToken(new byte[] { 4, 5 }), TokenConfig.LiteralToken(new byte[] { 2, 3 })) + }); + + byte[] buffer = { 1, 2, 3, 4, 5 }; + int currentBufferPosition = 3; + IOperation? match = trie.GetOperation(buffer, buffer.Length, ref currentBufferPosition, out int token); + + Assert.NotNull(match); + Assert.Equal("Test2", match.Id); + Assert.Equal(0, token); + Assert.Equal(buffer.Length, currentBufferPosition); + } + + [Fact(DisplayName = nameof(VerifyLastInWinsForIdenticalMatching))] + public void VerifyLastInWinsForIdenticalMatching() + { +#pragma warning disable IDE0230 // Use UTF-8 string literal + OperationTrie trie = OperationTrie.Create(new IOperation[] + { + new MockOperation("TestOp1", null, true, TokenConfig.LiteralToken(new byte[] { 5, 5, 5 })), + new MockOperation("TestOp2", null, true, TokenConfig.LiteralToken(new byte[] { 2, 3, 4, 5 })), + new MockOperation("TestOp3", null, true, TokenConfig.LiteralToken(new byte[] { 7, 7, 7 })), + new MockOperation("TestOp4", null, true, TokenConfig.LiteralToken(new byte[] { 9, 9, 9, 9 }), TokenConfig.LiteralToken(new byte[] { 2, 3, 4, 5 })), + }); +#pragma warning restore IDE0230 // Use UTF-8 string literal + + byte[] buffer = { 9, 8, 9, 8, 7, 2, 3, 4, 5 }; + int currentBufferPosition = 0; + IOperation? match = trie.GetOperation(buffer, buffer.Length, ref currentBufferPosition, out int token); + + Assert.NotNull(match); + Assert.Equal("TestOp4", match.Id); + Assert.Equal(1, token); + Assert.Equal(buffer.Length, currentBufferPosition); + } + + private class MockOperation : IOperation + { + private readonly MatchHandler? _onMatch; + + public MockOperation(string id, MatchHandler? onMatch, bool initialState, params IToken[] tokens) + { + Tokens = tokens; + Id = id; + _onMatch = onMatch; + IsInitialStateOn = initialState; + } + + public IReadOnlyList Tokens { get; } + + public string Id { get; } + + public bool IsInitialStateOn { get; } + + public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + return _onMatch?.Invoke(processor, bufferLength, ref currentBufferPosition, token) ?? 0; + } + } + + private class OperationTrie : Trie + { + public static OperationTrie Create(IEnumerable operations) + { + OperationTrie trie = new OperationTrie(); + + foreach (IOperation operation in operations) + { + int tokenNumber = 0; + foreach (IToken? token in operation.Tokens) + { + trie.AddPath(token!.Value, new OperationTerminal(operation, tokenNumber++, token.Length, token.Start, token.End)); + } + } + + return trie; + } + + public IOperation? GetOperation(byte[] buffer, int bufferLength, ref int bufferPosition, out int token) + { + int originalPosition = bufferPosition; + TrieEvaluator evaluator = new TrieEvaluator(this); + int sn = originalPosition; + + for (; bufferPosition < bufferLength; ++bufferPosition) + { + if (evaluator.Accept(buffer[bufferPosition], ref sn, out TerminalLocation? terminal)) + { + if (terminal!.Location == originalPosition) + { + bufferPosition -= sn - terminal.Location - terminal.Terminal.End; + token = terminal.Terminal.Token; + return terminal.Terminal.Operation; + } + else + { + token = -1; + bufferPosition = originalPosition; + return null; + } + } + + ++sn; + } + + if (bufferPosition == bufferLength) + { + evaluator.FinalizeMatchesInProgress(ref sn, out TerminalLocation? terminal); + + if (terminal != null) + { + bufferPosition -= sn - terminal.Location - terminal.Terminal.End; + token = terminal.Terminal.Token; + return terminal.Terminal.Operation; + } + } + + bufferPosition = originalPosition; + token = -1; + return null; + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/OrchestratorTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/OrchestratorTests.cs new file mode 100644 index 000000000000..59f9d2d717f4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/OrchestratorTests.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class OrchestratorTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + private readonly ILogger _logger; + + public OrchestratorTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + _logger = _engineEnvironmentSettings.Host.Logger; + } + + [Fact(DisplayName = nameof(VerifyRun))] + public void VerifyRun() + { + Util.Orchestrator orchestrator = new Util.Orchestrator(_logger, new MockFileSystem()); + MockMountPoint mnt = new MockMountPoint(_engineEnvironmentSettings); + mnt.MockRoot.AddDirectory("subdir").AddFile("test.file", []); + orchestrator.Run(new MockGlobalRunSpec(), mnt.Root, @"c:\temp"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/PhasedOperationTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/PhasedOperationTests.cs new file mode 100644 index 000000000000..ba14e480a20c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/PhasedOperationTests.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class PhasedOperationTests : TestBase, IClassFixture + { + private readonly ILogger _logger; + + public PhasedOperationTests(TestLoggerFactory testLoggerFactory) + { + _logger = testLoggerFactory.CreateLogger(); + } + + [Fact(DisplayName = nameof(VerifyPhasedOperationStateProgression))] + public void VerifyPhasedOperationStateProgression() + { + string originalValue = @" + + + + + +"; + + string expectedValue = @" + + + + + +"; + + VariableCollection vc = new VariableCollection(); + + Phase root = new Phase("package".TokenConfig(), new[] { "/>" }.TokenConfigs()); + + //package > name + Phase name = new Phase("name".TokenConfig(), new[] { "/>" }.TokenConfigs()); + root.Next.Add(name); + + //package > name > test > 1.0.0 + Phase test = new Phase("test".TokenConfig(), new[] { "/>" }.TokenConfigs()); + name.Next.Add(test); + Phase testValue = new Phase("1.0.0".TokenConfig(), "9.9.9", new[] { "/>" }.TokenConfigs()); + test.Next.Add(testValue); + + //package > name > foo > 1.0.0 + Phase foo = new Phase("foo".TokenConfig(), new[] { "/>" }.TokenConfigs()); + name.Next.Add(foo); + Phase fooValue = new Phase("1.0.0".TokenConfig(), "1.2.3", new[] { "/>" }.TokenConfigs()); + foo.Next.Add(fooValue); + + IProcessor processor = SetupConfig(vc, root); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + private IProcessor SetupConfig(IVariableCollection vc, params Phase[] phases) + { + EngineConfig cfg = new EngineConfig(_logger, vc, "$({0})"); + return Processor.Create(cfg, new PhasedOperation(null, phases, true)); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/RegionTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/RegionTests.cs new file mode 100644 index 000000000000..f5abe736c6ba --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/RegionTests.cs @@ -0,0 +1,346 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class RegionTests : TestBase, IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public RegionTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: this.GetType().Name, virtualize: true); + } + + [Fact(DisplayName = nameof(VerifyRegionExclude))] + public void VerifyRegionExclude() + { + string value = @"test value value x test foo bar"; + string expected = @"test bar"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("value".TokenConfig(), "foo".TokenConfig(), false, false, false, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyRegionInclude))] + public void VerifyRegionInclude() + { + string value = @"test value value x test foo bar"; + string expected = @"test x test bar"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("value".TokenConfig(), "foo".TokenConfig(), true, false, false, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyRegionStrayEnd))] + public void VerifyRegionStrayEnd() + { + string value = @"test foo value bar foo"; + string expected = @"test bar "; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("value".TokenConfig(), "foo".TokenConfig(), true, false, false, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyRegionIncludeToggle))] + public void VerifyRegionIncludeToggle() + { + string value = @"test region value x test region bar"; + string expected = @"test value x test bar"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("region".TokenConfig(), "region".TokenConfig(), true, false, false, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyRegionExcludeToggle))] + public void VerifyRegionExcludeToggle() + { + string value = @"test region value x test region bar"; + string expected = @"test bar"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("region".TokenConfig(), "region".TokenConfig(), false, false, false, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyRegionIncludeWhitespaceFixup))] + public void VerifyRegionIncludeWhitespaceFixup() + { + string value = @"test value value x test foo bar"; + string expected = @"testx testbar"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("value".TokenConfig(), "foo".TokenConfig(), true, false, true, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyRegionIncludeWhitespaceFixup2))] + public void VerifyRegionIncludeWhitespaceFixup2() + { + string value = @"Hello + #begin foo +value + #end +There"; + string expected = @"Hello +foo +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("#begin".TokenConfig(), "#end".TokenConfig(), true, false, true, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyRegionIncludeWholeLine))] + public void VerifyRegionIncludeWholeLine() + { + string value = @"Hello + #begin foo +value + #end +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("#begin".TokenConfig(), "#end".TokenConfig(), true, true, true, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyNoRegion))] + public void VerifyNoRegion() + { + string value = @"Hello + #begin foo +value + #end +There"; + string expected = @"Hello + #begin foo +value + #end +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("#begin2".TokenConfig(), "#end2".TokenConfig(), true, true, true, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyTornRegion1))] + public void VerifyTornRegion1() + { + string value = @"Hello + #begin foo +value + #end +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("#begin".TokenConfig(), "#end".TokenConfig(), true, true, true, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 14); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyTornRegion2))] + public void VerifyTornRegion2() + { + string value = @"Hello + #begin foo +value + #end +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("#begin".TokenConfig(), "#end".TokenConfig(), true, true, true, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 36); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyTornRegion3))] + public void VerifyTornRegion3() + { + string value = @"Hello + #begin foo +value + #end +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("#begin".TokenConfig(), "#end".TokenConfig(), true, true, true, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyTinyPageRegion))] + public void VerifyTinyPageRegion() + { + string value = @"Hello + #begin foo +value + #end +There"; + string expected = @"Hello +value +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("#begin".TokenConfig(), "#end".TokenConfig(), true, true, true, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyTornPageInCloseSeekRegion))] + public void VerifyTornPageInCloseSeekRegion() + { + string value = @"Hello + #begin foo +value +value +value +value +value +value +value +value + #end +There"; + string expected = @"Hello +There"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Region("#begin".TokenConfig(), "#end".TokenConfig(), false, true, true, null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 28); + Verify(Encoding.UTF8, output, changed, value, expected); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ReplacementTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ReplacementTests.cs new file mode 100644 index 000000000000..643d0fb04fcb --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/ReplacementTests.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class ReplacementTests : TestBase, IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public ReplacementTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: this.GetType().Name, virtualize: true); + } + + [Fact(DisplayName = nameof(VerifyReplacement))] + public void VerifyReplacement() + { + string value = @"test value test"; + string expected = @"test foo test"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Replacement("value".TokenConfig(), "foo", null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyNoReplacement))] + public void VerifyNoReplacement() + { + string value = @"test value test"; + string expected = @"test value test"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Replacement("value2".TokenConfig(), "foo", null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyTornReplacement))] + public void VerifyTornReplacement() + { + string value = @"test value test"; + string expected = @"test foo test"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Replacement("value".TokenConfig(), "foo", null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 6); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact(DisplayName = nameof(VerifyTinyPageReplacement))] + public void VerifyTinyPageReplacement() + { + string value = @"test value test"; + string expected = @"test foo test"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + IOperationProvider[] operations = { new Replacement("value".TokenConfig(), "foo", null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, VariableCollection.Root()); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output, 1); + Verify(Encoding.UTF8, output, changed, value, expected); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/SetFlagTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/SetFlagTests.cs new file mode 100644 index 000000000000..022f667ee96c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/SetFlagTests.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class SetFlagTests : TestBase, IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public SetFlagTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + // Demonstrate that conditional operations processing is correctly enabled & disabled with Emit flags. + [Fact(DisplayName = nameof(TurnOffConditionalTest))] + public void TurnOffConditionalTest() + { + string originalValue = @"lead stuff +//#if (true) + true stuff +//#else + false stuff +//#endif +Entering Conditional processing = off +//-:cnd +...in +//#if (true) + in-conditional true (IF) stuff +//#else + in-conditional ELSE stuff +//#endif +...about to exit +//+:cnd +After Conditional processing = off +//#if (false) + After conditional IF(false) stuff +//#else + After conditional ELSE(true) stuff +//#endif +Final stuff"; + + string expectedValue = @"lead stuff + true stuff +Entering Conditional processing = off +//-:cnd +...in +//#if (true) + in-conditional true (IF) stuff +//#else + in-conditional ELSE stuff +//#endif +...about to exit +//+:cnd +After Conditional processing = off + After conditional ELSE(true) stuff +Final stuff"; + + VariableCollection vc = new VariableCollection(); + IProcessor partialProcessor = new ConditionalTests(_environmentSettingsHelper).SetupCStyleWithCommentsProcessor(vc); + + string on = "//+:cnd"; + string off = "//-:cnd"; + List flagOperations = new List + { + new SetFlag(Conditional.OperationName, on.TokenConfig(), off.TokenConfig(), string.Empty.TokenConfig(), string.Empty.TokenConfig(), null, true) + }; + + IProcessor processor = partialProcessor.CloneAndAppendOperations(flagOperations); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + // Demonstrate that conditional operations processing is correctly enabled & disabled with the noEmit flags. + [Fact(DisplayName = nameof(TurnOffConditionalAndFlagsDontEmitTest))] + public void TurnOffConditionalAndFlagsDontEmitTest() + { + string originalValue = @"lead stuff +//#if (true) + true stuff +//#else + false stuff +//#endif +Entering Conditional processing = off +//-:cnd:noEmit +...in +//#if (true) + in-conditional true (IF) stuff +//#else + in-conditional ELSE stuff +//#endif +...about to exit +//+:cnd:noEmit +After Conditional processing = off +//#if (false) + After conditional IF(false) stuff +//#else + After conditional ELSE(true) stuff +//#endif +Final stuff"; + + string expectedValue = @"lead stuff + true stuff +Entering Conditional processing = off +...in +//#if (true) + in-conditional true (IF) stuff +//#else + in-conditional ELSE stuff +//#endif +...about to exit +After Conditional processing = off + After conditional ELSE(true) stuff +Final stuff"; + + VariableCollection vc = new VariableCollection(); + IProcessor partialProcessor = new ConditionalTests(_environmentSettingsHelper).SetupCStyleWithCommentsProcessor(vc); + + string on = "//+:cnd"; + string onNoEmit = on + ":noEmit"; + string off = "//-:cnd"; + string offNoEmit = off + ":noEmit"; + List flagOperations = new List + { + new SetFlag(Conditional.OperationName, on.TokenConfig(), off.TokenConfig(), onNoEmit.TokenConfig(), offNoEmit.TokenConfig(), null, true) + }; + + IProcessor processor = partialProcessor.CloneAndAppendOperations(flagOperations); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + // Demonstrate that the newlines for the noEmit flags do not get emitted. + [Fact(DisplayName = nameof(ValidateDontEmitFlagDoesntAddNewline))] + public void ValidateDontEmitFlagDoesntAddNewline() + { + string originalValue = @"Start +//-:cnd:noEmit +//+:cnd:noEmit +End"; + + string expectedValue = @"Start +End"; + + VariableCollection vc = new VariableCollection(); + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + EngineConfig engineConfig = new EngineConfig(environmentSettings.Host.Logger, vc); + + string on = "//+:cnd"; + string onNoEmit = on + ":noEmit"; + string off = "//-:cnd"; + string offNoEmit = off + ":noEmit"; + IOperationProvider[] operations = + { + new SetFlag(Conditional.OperationName, on.TokenConfig(), off.TokenConfig(), onNoEmit.TokenConfig(), offNoEmit.TokenConfig(), null, true), + }; + IProcessor processor = Processor.Create(engineConfig, operations); + RunAndVerify(originalValue, expectedValue, processor, 9999); + } + + // Using //-:cnd:noEmit on the first line of *.cs file corrupts file content when templating new project + // https://github.com/dotnet/templating/issues/2913 + [Fact] + public void ValidateConditionOnStartOfFileWithBOM() + { + string originalValue = @"//-:cnd:noEmit +#if DEBUG +#endif +//+:cnd:noEmit"; + + string expectedValue = @"#if DEBUG +#endif +"; + + VariableCollection vc = new(); + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + EngineConfig engineConfig = new(environmentSettings.Host.Logger, vc); + + ConditionalTokens tokens = new() + { + IfTokens = new[] { "#if" }.TokenConfigs() + }; + + string on = "//+:cnd"; + string onNoEmit = on + ":noEmit"; + string off = "//-:cnd"; + string offNoEmit = off + ":noEmit"; + IOperationProvider[] operations = + { + new SetFlag(Conditional.OperationName, on.TokenConfig(), off.TokenConfig(), onNoEmit.TokenConfig(), offNoEmit.TokenConfig(), null, true), + new Conditional(tokens, true, true, Expressions.Cpp.CppStyleEvaluatorDefinition.Evaluate, null, true) + }; + IProcessor processor = Processor.Create(engineConfig, operations); + RunAndVerify(originalValue, expectedValue, processor, 9999, emitBOM: true); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/StreamProxyTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/StreamProxyTests.cs new file mode 100644 index 000000000000..768a6f104b4f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/StreamProxyTests.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.TemplateEngine.Core.Util; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class StreamProxyTests + { + [Fact] + public void BytesBufferedIfSizeUnderThreshold() + { + byte[] bytes = new byte[256]; + MemoryStream ms = new MemoryStream(bytes); + + StreamProxy proxy = new StreamProxy(ms, 10, 100); + + int numBytesFirstBatch = 15; + byte[] bytesToWrite = Enumerable.Range(0, numBytesFirstBatch).Select(i => (byte)i).ToArray(); + proxy.Write(bytesToWrite, 0, numBytesFirstBatch); + + ms.Position.Should().Be(0); + bytes.Should().AllBeEquivalentTo(0); + proxy.Position.Should().Be(numBytesFirstBatch); + proxy.Position = 0; + byte[] tmp = new byte[numBytesFirstBatch]; + proxy.Read(tmp, 0, numBytesFirstBatch).Should().Be(numBytesFirstBatch); + tmp.Should().BeEquivalentTo(bytesToWrite, options => options.WithStrictOrdering()); + proxy.Position = numBytesFirstBatch; + + int numBytesSecondBatch = 30; + byte[] bytesToWrite2 = Enumerable.Range(numBytesFirstBatch, numBytesSecondBatch).Select(i => (byte)i).ToArray(); + proxy.Write(bytesToWrite2, 0, numBytesSecondBatch); + + ms.Position.Should().Be(0); + bytes.Should().AllBeEquivalentTo(0); + proxy.Position.Should().Be(numBytesFirstBatch + numBytesSecondBatch); + proxy.Position = 0; + byte[] tmp2 = new byte[numBytesFirstBatch + numBytesSecondBatch]; + proxy.Read(tmp2, 0, numBytesFirstBatch + numBytesSecondBatch).Should().Be(numBytesFirstBatch + numBytesSecondBatch); + tmp2.Should().BeEquivalentTo(bytesToWrite.Concat(bytesToWrite2), options => options.WithStrictOrdering()); + proxy.Position = numBytesFirstBatch + numBytesSecondBatch; + + proxy.Flush(); + ms.Position.Should().Be(0); + bytes.Should().AllBeEquivalentTo(0); + + proxy.FlushToTarget(); + ms.Position.Should().Be(numBytesFirstBatch + numBytesSecondBatch); + bytes.AsSpan(0, numBytesFirstBatch + numBytesSecondBatch).ToArray() + .Should().BeEquivalentTo(bytesToWrite.Concat(bytesToWrite2), options => options.WithStrictOrdering()); + } + + [Fact] + public void BytesSendToTagetIfSizeOverThreshold() + { + byte[] bytes = new byte[256]; + MemoryStream ms = new MemoryStream(bytes); + + StreamProxy proxy = new StreamProxy(ms, 10, 16); + + int numBytesFirstBatch = 15; + byte[] bytesToWrite = Enumerable.Range(0, numBytesFirstBatch).Select(i => (byte)i).ToArray(); + proxy.Write(bytesToWrite, 0, numBytesFirstBatch); + + ms.Position.Should().Be(0); + bytes.Should().AllBeEquivalentTo(0); + proxy.Position.Should().Be(numBytesFirstBatch); + proxy.Position = 0; + byte[] tmp = new byte[numBytesFirstBatch]; + proxy.Read(tmp, 0, numBytesFirstBatch).Should().Be(numBytesFirstBatch); + tmp.Should().BeEquivalentTo(bytesToWrite, options => options.WithStrictOrdering()); + proxy.Position = numBytesFirstBatch; + + int numBytesSecondBatch = 30; + byte[] bytesToWrite2 = Enumerable.Range(numBytesFirstBatch, numBytesSecondBatch).Select(i => (byte)i).ToArray(); + proxy.Write(bytesToWrite2, 0, numBytesSecondBatch); + + ms.Position.Should().Be(numBytesFirstBatch + numBytesSecondBatch); + bytes.AsSpan(0, numBytesFirstBatch + numBytesSecondBatch).ToArray() + .Should().BeEquivalentTo(bytesToWrite.Concat(bytesToWrite2), options => options.WithStrictOrdering()); + + proxy.Position.Should().Be(numBytesFirstBatch + numBytesSecondBatch); + proxy.Position = 0; + byte[] tmp2 = new byte[numBytesFirstBatch + numBytesSecondBatch]; + proxy.Read(tmp2, 0, numBytesFirstBatch + numBytesSecondBatch).Should().Be(numBytesFirstBatch + numBytesSecondBatch); + tmp2.Should().BeEquivalentTo(bytesToWrite.Concat(bytesToWrite2), options => options.WithStrictOrdering()); + proxy.Position = numBytesFirstBatch + numBytesSecondBatch; + + proxy.FlushToTarget(); + ms.Position.Should().Be(numBytesFirstBatch + numBytesSecondBatch); + bytes.AsSpan(0, numBytesFirstBatch + numBytesSecondBatch).ToArray() + .Should().BeEquivalentTo(bytesToWrite.Concat(bytesToWrite2), options => options.WithStrictOrdering()); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/TestBase.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/TestBase.cs new file mode 100644 index 000000000000..8cddbe30be68 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/TestBase.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public abstract class TestBase + { + protected static void RunAndVerify(string originalValue, string expectedValue, IProcessor processor, int bufferSize, bool? changeOverride = null, bool emitBOM = false) + { + byte[] valueBytes = Encoding.UTF8.GetBytes(originalValue); + using MemoryStream input = new MemoryStream(); + if (emitBOM) + { + byte[] preamble = new UTF8Encoding(true).GetPreamble(); + input.Write(preamble, 0, preamble.Length); + } + input.Write(valueBytes, 0, valueBytes.Length); + input.Position = 0; + using MemoryStream output = new MemoryStream(); + bool changed = processor.Run(input, output, bufferSize); + Verify(new UTF8Encoding(emitBOM), output, changed, originalValue, expectedValue, changeOverride, emitBOM); + } + + protected static void Verify(Encoding encoding, Stream output, bool changed, string source, string expected, bool? changeOverride = null, bool checkBOM = false) + { + output.Position = 0; + + if (checkBOM) + { + byte[] preamble = encoding.GetPreamble(); + if (preamble.Length > 0) + { + byte[] readPreamble = new byte[preamble.Length]; + int totalBytesRead = 0; + while (totalBytesRead < readPreamble.Length) + { + int bytesRead = output.Read(readPreamble, totalBytesRead, readPreamble.Length - totalBytesRead); + if (bytesRead == 0) + { + break; + } + totalBytesRead += bytesRead; + } + Assert.Equal(preamble, readPreamble); + } + } + + byte[] resultBytes = new byte[output.Length - output.Position]; + int totalRead = 0; + while (totalRead < resultBytes.Length) + { + int bytesRead = output.Read(resultBytes, totalRead, resultBytes.Length - totalRead); + if (bytesRead == 0) + { + break; + } + totalRead += bytesRead; + } + + string actual = encoding.GetString(resultBytes); + Assert.Equal(expected, actual); + + bool expectedChange = changeOverride ?? !string.Equals(expected, source, StringComparison.Ordinal); + string modifier = expectedChange ? string.Empty : "not "; + if (expectedChange ^ changed) + { + Assert.Fail($"Expected value to {modifier} be changed"); + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/TokenTrieTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/TokenTrieTests.cs new file mode 100644 index 000000000000..ec2f09ac7cef --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/TokenTrieTests.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Util; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class TokenTrieTests + { + [Theory(DisplayName = nameof(VerifyTokenTrieLookArounds))] + [InlineData("Hello There!", 0, 5, true, null, "Hello", null)] + [InlineData("Hello There!", 0, 6, true, "Hello", " ", null)] + [InlineData("Hello There!", 1, 1, false, "Hello", " ", null)] + [InlineData("Hello There!", 5, 6, true, null, " ", "There!")] + [InlineData("Hello There!", 0, 6, true, "Hello", " ", "There!")] + public void VerifyTokenTrieLookArounds(string original, int checkPosition, int expectedPosition, bool success, string? after, string value, string? before) + { + byte[] data = Encoding.UTF8.GetBytes(original); + + TokenTrie t = new TokenTrie(); + TokenConfig builder = (value ?? string.Empty).TokenConfigBuilder(); + + if (!string.IsNullOrEmpty(after)) + { + builder = builder.OnlyIfAfter(after); + } + + if (!string.IsNullOrEmpty(before)) + { + builder = builder.OnlyIfBefore(before); + } + + t.AddToken(builder.ToToken(Encoding.UTF8)); + + int pos = checkPosition; + Assert.Equal(success, t.GetOperation(data, data.Length, ref pos, out _)); + Assert.Equal(expectedPosition, pos); + } + + [Fact(DisplayName = nameof(VerifyTokenTrieAtBegin))] + public void VerifyTokenTrieAtBegin() + { + byte[] hello = "hello"u8.ToArray(); + byte[] helloBang = "hello!"u8.ToArray(); + byte[] hi = "hi"u8.ToArray(); + + TokenTrie t = new TokenTrie(); + t.AddToken(hello); + t.AddToken(helloBang); + t.AddToken(hi); + + byte[] source1 = "hello there"u8.ToArray(); + byte[] source2 = "hello1 there"u8.ToArray(); + byte[] source3 = "hello! there"u8.ToArray(); + byte[] source4 = "hi there"u8.ToArray(); + byte[] source5 = "hi"u8.ToArray(); + byte[] source6 = "he"u8.ToArray(); + + int pos = 0; + Assert.True(t.GetOperation(source1, source1.Length, ref pos, out int token)); + Assert.Equal(0, token); + + pos = 0; + Assert.True(t.GetOperation(source2, source2.Length, ref pos, out token)); + Assert.Equal(0, token); + + pos = 0; + Assert.True(t.GetOperation(source3, source3.Length, ref pos, out token)); + Assert.Equal(1, token); + + pos = 0; + Assert.True(t.GetOperation(source4, source4.Length, ref pos, out token)); + Assert.Equal(2, token); + + pos = 0; + Assert.True(t.GetOperation(source5, source5.Length, ref pos, out token)); + Assert.Equal(2, token); + + pos = 0; + Assert.False(t.GetOperation(source6, source6.Length, ref pos, out token)); + Assert.Equal(-1, token); + } + + [Fact(DisplayName = nameof(VerifyTokenTrieNotEnoughBufferLeft))] + public void VerifyTokenTrieNotEnoughBufferLeft() + { + byte[] hello = "hello"u8.ToArray(); + byte[] helloBang = "hello!"u8.ToArray(); + + TokenTrie t = new TokenTrie(); + t.AddToken(hello); + t.AddToken(helloBang); + + byte[] source1 = "hi"u8.ToArray(); + byte[] source2 = " hello"u8.ToArray(); + + int pos = 0; + Assert.False(t.GetOperation(source1, source1.Length, ref pos, out int token)); + Assert.Equal(-1, token); + + pos = 1; + Assert.True(t.GetOperation(source2, source2.Length, ref pos, out token)); + Assert.Equal(0, token); + + pos = 2; + Assert.False(t.GetOperation(source2, source2.Length, ref pos, out token)); + Assert.Equal(-1, token); + } + + [Fact(DisplayName = nameof(VerifyTokenTrieCombine))] + public void VerifyTokenTrieCombine() + { + byte[] hello = "hello"u8.ToArray(); + byte[] helloBang = "hello!"u8.ToArray(); + byte[] hi = "hi"u8.ToArray(); + byte[] there = "there!"u8.ToArray(); + + TokenTrie t = new TokenTrie(); + t.AddToken(hello); + t.AddToken(helloBang); + + TokenTrie t2 = new TokenTrie(); + t.AddToken(hi); + t.AddToken(there); + + TokenTrie combined = new TokenTrie(); + combined.Append(t); + combined.Append(t2); + + byte[] source1 = "hello there"u8.ToArray(); + byte[] source2 = "hello! there"u8.ToArray(); + byte[] source3 = "hi there"u8.ToArray(); + byte[] source4 = "there!"u8.ToArray(); + + int pos = 0; + Assert.True(t.GetOperation(source1, source1.Length, ref pos, out int token)); + Assert.Equal(0, token); + + pos = 0; + Assert.True(t.GetOperation(source2, source2.Length, ref pos, out token)); + Assert.Equal(1, token); + + pos = 0; + Assert.True(t.GetOperation(source3, source3.Length, ref pos, out token)); + Assert.Equal(2, token); + + pos = 0; + Assert.True(t.GetOperation(source4, source4.Length, ref pos, out token)); + Assert.Equal(3, token); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/TrieTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/TrieTests.cs new file mode 100644 index 000000000000..92a582de2f40 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/TrieTests.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class TrieTests : TestBase, IClassFixture + { + private readonly ILogger _logger; + + public TrieTests(TestLoggerFactory testLoggerFactory) + { + _logger = testLoggerFactory.CreateLogger(); + } + + [Fact] + public void VerifyThatTrieMatchesAtTheBeginning() + { + bool testActivated = false; + IProcessor p = SetupTestProcessor( + new IOperationProvider[] + { + new MockOperationProvider( + new MockOperation( + null, + (processor, bufferLength, ref currentBufferPosition, token) => + { + testActivated = true; + return 0; + }, + true, + new byte[] { 1, 2, 3 })) + }, + VariableCollection.Root()); + + byte[] data = new byte[] { 1, 2, 3, 4, 5 }; + MemoryStream source = new MemoryStream(data) + { + Position = 0 + }; + p.Run(source, new MemoryStream()); + Assert.True(testActivated); + } + + [Fact] + public void VerifyThatTrieMatchesAtTheEnd() + { + bool testActivated = false; + IProcessor p = SetupTestProcessor( + new IOperationProvider[] + { + new MockOperationProvider( + new MockOperation( + null, + (processor, bufferLength, ref currentBufferPosition, token) => + { + testActivated = true; + return 0; + }, + true, + new byte[] { 1, 2, 3 })) + }, + VariableCollection.Root()); + + byte[] data = new byte[] { 4, 5, 1, 2, 3 }; + p.Run(new MemoryStream(data), new MemoryStream()); + Assert.True(testActivated); + } + + [Fact] + public void VerifyThatTrieMatchesInTheInterior() + { + bool testActivated = false; + IProcessor p = SetupTestProcessor( + new IOperationProvider[] + { + new MockOperationProvider( + new MockOperation( + null, + (processor, bufferLength, ref currentBufferPosition, token) => + { + testActivated = true; + return 0; + }, + true, + new byte[] { 1, 2, 3 })) + }, + VariableCollection.Root()); + + byte[] data = new byte[] { 4, 5, 1, 2, 3, 6, 7 }; + p.Run(new MemoryStream(data), new MemoryStream()); + Assert.True(testActivated); + } + + [Fact] + public void VerifyThatTrieMatchesAsTheWholeContents() + { + bool testActivated = false; + IProcessor p = SetupTestProcessor( + new IOperationProvider[] + { + new MockOperationProvider( + new MockOperation( + null, + (processor, bufferLength, ref currentBufferPosition, token) => + { + testActivated = true; + return 0; + }, + true, + new byte[] { 1, 2, 3 })) + }, + VariableCollection.Root()); + + byte[] data = new byte[] { 1, 2, 3 }; + p.Run(new MemoryStream(data), new MemoryStream()); + Assert.True(testActivated); + } + + private IProcessor SetupTestProcessor(IOperationProvider[] operations, VariableCollection vc) + { + EngineConfig cfg = new EngineConfig(_logger, vc); + return Processor.Create(cfg, operations); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/VariablesTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/VariablesTests.cs new file mode 100644 index 000000000000..000bde3b0b49 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Core.UnitTests/VariablesTests.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Core.Util; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Core.UnitTests +{ + public class VariablesTests : TestBase, IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public VariablesTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: this.GetType().Name, virtualize: true); + } + + [Fact(DisplayName = nameof(VerifyVariables))] + public void VerifyVariables() + { + string value = @"test VAL test"; + string expected = @"test testValue test"; + + byte[] valueBytes = Encoding.UTF8.GetBytes(value); + MemoryStream input = new MemoryStream(valueBytes); + MemoryStream output = new MemoryStream(); + + VariableCollection vc = new VariableCollection + { + ["VAL"] = "testValue" + }; + + IOperationProvider[] operations = { new ExpandVariables(null, true) }; + EngineConfig cfg = new EngineConfig(_engineEnvironmentSettings.Host.Logger, vc); + IProcessor processor = Processor.Create(cfg, operations); + + //Changes should be made + bool changed = processor.Run(input, output); + Verify(Encoding.UTF8, output, changed, value, expected); + } + + [Fact] + public void VerifyVariablesNull() + { + Assert.Throws(() => + { + VariableCollection vc = new() + { + ["NULL"] = null! + }; + }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/AllComponents.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/AllComponents.cs new file mode 100644 index 000000000000..bb3387a2b92f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/AllComponents.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class AllComponents + { + [Fact] + public void TestAllEdgeComponentsAdded() + { + var assemblyCatalog = new AssemblyComponentCatalog(new[] { typeof(Components).Assembly }); + + var expectedTypeNames = assemblyCatalog.Select(pair => pair.Item1.FullName + ";" + pair.Item2.GetType().FullName).OrderBy(name => name); + var actualTypeNames = Components.AllComponents.Select(t => t.Type.FullName + ";" + t.Instance.GetType().FullName).OrderBy(name => name); + + Assert.Equal(expectedTypeNames, actualTypeNames); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/Fakes/FakeManagedPackageProvider.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/Fakes/FakeManagedPackageProvider.cs new file mode 100644 index 000000000000..a87211d244a4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/Fakes/FakeManagedPackageProvider.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FakeItEasy; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; + +namespace Microsoft.TemplateEngine.Edge.UnitTests.Fakes +{ + internal class FakeManagedPackageProviderFactory : ITemplatePackageProviderFactory + { + private static readonly List> AllCreatedProviders = new List>(); + + public string DisplayName => nameof(FakeManagedPackageProviderFactory); + + public Guid Id { get; } = new Guid("{61CFA828-97B6-44EB-A44D-0AE673D6DF58}"); + + public ITemplatePackageProvider CreateProvider(IEngineEnvironmentSettings settings) + { + var managedTemplatePackageProvider = new FakeManagedPackageProvider(); + AllCreatedProviders.Add(new WeakReference(managedTemplatePackageProvider)); + return managedTemplatePackageProvider; + } + } + + internal class FakeManagedPackageProvider : ITemplatePackageProvider + { + private const string ManagedPackageMountPoint = "ManagedMount"; + private const string ManagedPackageIdentifier = "ManagedPackage"; + + public ITemplatePackageProviderFactory Factory => new FakeManagedPackageProviderFactory(); + + public event Action? TemplatePackagesChanged + { + add { } + remove { } + } + + public Task> GetAllTemplatePackagesAsync(CancellationToken cancellationToken) + { + var managedTemplatePackage = A.Fake(); + A.CallTo(() => managedTemplatePackage.MountPointUri).Returns(ManagedPackageMountPoint); + A.CallTo(() => managedTemplatePackage.Identifier).Returns(ManagedPackageIdentifier); + A.CallTo(() => managedTemplatePackage.Version).Returns("1.0.0"); + + IReadOnlyList managedPackages = new List { managedTemplatePackage }; + return Task.FromResult(managedPackages); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/FolderInstallerTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/FolderInstallerTests.cs new file mode 100644 index 000000000000..ed96457a152b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/FolderInstallerTests.cs @@ -0,0 +1,197 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Edge.Installers.Folder; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class FolderInstallerTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public FolderInstallerTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Fact] + public async Task CanInstall_Directory() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + FolderInstaller folderInstaller = new FolderInstaller(engineEnvironmentSettings, factory); + + InstallRequest request = new InstallRequest(installPath); + + Assert.True(await folderInstaller.CanInstallAsync(request, CancellationToken.None)); + + InstallResult result = await folderInstaller.InstallAsync(request, provider, CancellationToken.None); + + Assert.True(result.Success); + Assert.Equal(request, result.InstallRequest); + Assert.Equal(InstallerErrorCode.Success, result.Error); + result.ErrorMessage.Should().BeNullOrEmpty(); + + var source = result.TemplatePackage as FolderManagedTemplatePackage; + Assert.NotNull(source); + source!.MountPointUri.Should().Be(installPath); + source.Version.Should().BeNullOrEmpty(); + source.DisplayName.Should().Be(installPath); + source.Identifier.Should().Be(installPath); + source.Installer.Should().Be(folderInstaller); + source.Provider.Should().Be(provider); + } + + [Fact] + public async Task CannotInstall_File() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + FolderInstaller folderInstaller = new FolderInstaller(engineEnvironmentSettings, factory); + + InstallRequest request = new InstallRequest(Path.GetTempFileName()); + Assert.False(await folderInstaller.CanInstallAsync(request, CancellationToken.None)); + + InstallResult result = await folderInstaller.InstallAsync(request, provider, CancellationToken.None); + + Assert.False(result.Success); + Assert.Equal(request, result.InstallRequest); + Assert.Equal(InstallerErrorCode.PackageNotFound, result.Error); + result.ErrorMessage.Should().NotBeNullOrEmpty(); + result.TemplatePackage.Should().BeNull(); + } + + [Fact] + public async Task CannotInstall_NotExist() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + FolderInstaller folderInstaller = new FolderInstaller(engineEnvironmentSettings, factory); + + InstallRequest request = new InstallRequest("not found"); + Assert.False(await folderInstaller.CanInstallAsync(request, CancellationToken.None)); + + InstallResult result = await folderInstaller.InstallAsync(request, provider, CancellationToken.None); + + Assert.False(result.Success); + Assert.Equal(request, result.InstallRequest); + Assert.Equal(InstallerErrorCode.PackageNotFound, result.Error); + result.ErrorMessage.Should().NotBeNullOrEmpty(); + result.TemplatePackage.Should().BeNull(); + } + + [Fact] + public async Task GetLatestVersion_Success() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + FolderInstaller folderInstaller = new FolderInstaller(engineEnvironmentSettings, factory); + + FolderManagedTemplatePackage source = new FolderManagedTemplatePackage(engineEnvironmentSettings, folderInstaller, provider, Path.GetRandomFileName(), DateTime.UtcNow); + IReadOnlyList results = await folderInstaller.GetLatestVersionAsync(new[] { source }, provider, CancellationToken.None); + + Assert.Single(results); + CheckUpdateResult result = results.Single(); + + Assert.True(result.Success); + Assert.Equal(source, result.TemplatePackage); + Assert.Equal(InstallerErrorCode.Success, result.Error); + result.ErrorMessage.Should().BeNullOrEmpty(); + result.LatestVersion.Should().BeNullOrEmpty(); + Assert.True(result.IsLatestVersion); + } + + [Fact] + public async Task GetLatestVersion_HandleExceptions() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + FolderInstaller folderInstaller = new FolderInstaller(engineEnvironmentSettings, factory); + + _ = await Assert.ThrowsAsync(() => folderInstaller.GetLatestVersionAsync(null!, provider, CancellationToken.None)); + } + + [Fact] + public async Task Update_Success() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + FolderInstaller folderInstaller = new FolderInstaller(engineEnvironmentSettings, factory); + + FolderManagedTemplatePackage source = new FolderManagedTemplatePackage(engineEnvironmentSettings, folderInstaller, provider, Path.GetRandomFileName(), DateTime.UtcNow); + //add a delay so update updates last changed time + await Task.Delay(100); + UpdateRequest updateRequest = new UpdateRequest(source, "1.0.0"); + UpdateResult result = await folderInstaller.UpdateAsync(updateRequest, provider, CancellationToken.None); + + Assert.True(result.Success); + Assert.Equal(updateRequest, result.UpdateRequest); + Assert.Equal(InstallerErrorCode.Success, result.Error); + Assert.Equal(source.MountPointUri, result.TemplatePackage?.MountPointUri); + Assert.NotEqual(source.LastChangeTime, result.TemplatePackage?.LastChangeTime); + } + + [Fact] + public async Task Update_HandleExceptions() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + FolderInstaller folderInstaller = new FolderInstaller(engineEnvironmentSettings, factory); + _ = await Assert.ThrowsAsync(() => folderInstaller.UpdateAsync(null!, provider, CancellationToken.None)); + } + + [Fact] + public async Task CanUninstall_Success() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + FolderInstaller folderInstaller = new FolderInstaller(engineEnvironmentSettings, factory); + + InstallRequest request = new InstallRequest(installPath); + + InstallResult result = await folderInstaller.InstallAsync(request, provider, CancellationToken.None); + + Assert.True(result.Success); + Assert.Equal(request, result.InstallRequest); + Assert.Equal(InstallerErrorCode.Success, result.Error); + result.ErrorMessage.Should().BeNullOrEmpty(); + + var source = result.TemplatePackage as FolderManagedTemplatePackage; + source.Should().NotBeNull(); + source!.MountPointUri.Should().Be(installPath); + Directory.Exists(installPath); + + UninstallResult uninstallResult = await folderInstaller.UninstallAsync(source, provider, CancellationToken.None); + + Assert.True(uninstallResult.Success); + Assert.Equal(source, uninstallResult.TemplatePackage); + Assert.Equal(InstallerErrorCode.Success, result.Error); + result.ErrorMessage.Should().BeNullOrEmpty(); + + //directory is not removed + Directory.Exists(installPath); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/GeneratorTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/GeneratorTests.cs new file mode 100644 index 000000000000..13e8dc73c11e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/GeneratorTests.cs @@ -0,0 +1,278 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.Edge.Template; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Microsoft.TemplateEngine.Utils; +using Xunit; +using ITemplateMatchInfo = Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo; +using WellKnownSearchFilters = Microsoft.TemplateEngine.Utils.WellKnownSearchFilters; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class GeneratorTests : TestBase + { + [Fact] + public async Task CanUseCustomGenerator() + { + var builtIns = new List<(Type, IIdentifiedComponent)>() + { + (typeof(IGenerator), new CustomGenerator()) + }; + builtIns.AddRange(BuiltInTemplatePackagesProviderFactory.GetComponents(TestTemplatesLocation)); + builtIns.AddRange(Orchestrator.RunnableProjects.Components.AllComponents); + builtIns.AddRange(Components.AllComponents); + + using ITemplateEngineHost testHost = TestHost.GetVirtualHost("generatorTest", additionalComponents: builtIns); + using IEngineEnvironmentSettings engineEnvironmentSettings = new EngineEnvironmentSettings(testHost); + TemplateCreator templateCreator = new(engineEnvironmentSettings); + TemplatePackageManager templatePackagesManager = new(engineEnvironmentSettings); + + IReadOnlyList foundTemplates = await templatePackagesManager.GetTemplatesAsync( + matchFilter: WellKnownSearchFilters.MatchesAllCriteria, + filters: new[] { WellKnownSearchFilters.NameFilter("test-template") }, + cancellationToken: default); + ITemplateMatchInfo template = Assert.Single(foundTemplates); + + string output = TestUtils.CreateTemporaryFolder(); + ITemplateCreationResult dryRunResult = await templateCreator.InstantiateAsync( + template.Info, + "MyProject", + fallbackName: null, + outputPath: output, + inputParameters: new Dictionary(), + forceCreation: true, + dryRun: true); + + Assert.Equal(CreationResultStatus.Success, dryRunResult.Status); + Assert.Equal((ICreationEffects)CustomGenerator.CreationEffects.Instance, dryRunResult.CreationEffects); + + ITemplateCreationResult runResult = await templateCreator.InstantiateAsync( + template.Info, + "MyProject", + fallbackName: null, + outputPath: output, + inputParameters: new Dictionary(), + forceCreation: true, + dryRun: false); + + Assert.Equal(CreationResultStatus.Success, runResult.Status); + Assert.Equal(CustomGenerator.SimpleCreationResult.Instance, runResult.CreationResult); + + string targetFile = Path.Combine(output, "success.txt"); + Assert.True(File.Exists(targetFile)); + } + + private class CustomGenerator : IGenerator + { + public Guid Id { get; } = new Guid("{AB083D9D-857A-419E-8394-113F97FFBD6B}"); + + public object ConvertParameterValueToType(IEngineEnvironmentSettings environmentSettings, ITemplateParameter parameter, string untypedValue, out bool valueResolutionError) => throw new NotImplementedException(); + + [Obsolete] + public Task CreateAsync(IEngineEnvironmentSettings environmentSettings, ITemplate template, IParameterSet parameters, string targetDirectory, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task CreateAsync(IEngineEnvironmentSettings environmentSettings, ITemplate template, IParameterSetData parameters, string targetDirectory, CancellationToken cancellationToken) + { + if (!environmentSettings.Host.FileSystem.DirectoryExists(targetDirectory)) + { + environmentSettings.Host.FileSystem.CreateDirectory(targetDirectory); + } + environmentSettings.Host.FileSystem.WriteAllText(Path.Combine(targetDirectory, "success.txt"), string.Empty); + + return Task.FromResult(SimpleCreationResult.Instance); + } + + [Obsolete] + public Task GetCreationEffectsAsync(IEngineEnvironmentSettings environmentSettings, ITemplate template, IParameterSet parameters, string targetDirectory, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task GetCreationEffectsAsync(IEngineEnvironmentSettings environmentSettings, ITemplate template, IParameterSetData parameters, string targetDirectory, CancellationToken cancellationToken) + { + return Task.FromResult((ICreationEffects)CreationEffects.Instance); + } + + [Obsolete] + public IParameterSet GetParametersForTemplate(IEngineEnvironmentSettings environmentSettings, ITemplate template) => throw new NotImplementedException(); + + public IList GetTemplatesAndLangpacksFromDir(IMountPoint source, out IList localizations) + { + localizations = new List(); + return new List() { new Template(this, source) }; + } + + public Task> GetTemplatesFromMountPointAsync(IMountPoint source, CancellationToken cancellationToken) + { + return Task.FromResult((IReadOnlyList)new List() { new Template(this, source) }); + } + + public Task LoadTemplateAsync(IEngineEnvironmentSettings settings, ITemplateLocator config, string? baselineName = null, CancellationToken cancellationToken = default) + { + if (!settings.TryGetMountPoint(config.MountPointUri, out IMountPoint? mountPoint)) + { + return Task.FromResult((ITemplate?)null); + } + if (mountPoint == null) + { + throw new InvalidOperationException($"{nameof(mountPoint)} is null after {nameof(EngineEnvironmentSettingsExtensions.TryGetMountPoint)} returned true."); + } + + return Task.FromResult((ITemplate?)new Template(this, mountPoint)); + } + + public bool TryEvaluateFromString(ILogger logger, string text, IDictionary variables, out bool result, out string evaluationError, HashSet? referencedVariablesKeys = null) => throw new NotImplementedException(); + + public bool TryGetTemplateFromConfigInfo(IFileSystemInfo config, out ITemplate? template, IFileSystemInfo? localeConfig, IFile? hostTemplateConfigFile, string? baselineName = null) + { + template = new Template(this, config.MountPoint); + return true; + } + + internal class Template : ITemplate, IScanTemplateInfo + { + private readonly IMountPoint _mountPoint; + + public Template(IGenerator generator, IMountPoint mountPoint) + { + Generator = generator; + _mountPoint = mountPoint; + } + + public IGenerator Generator { get; } + + public IFileSystemInfo Configuration => _mountPoint.Root; + + public IFileSystemInfo? LocaleConfiguration => null; + + public IDirectory TemplateSourceRoot => _mountPoint.Root; + + public bool IsNameAgreementWithFolderPreferred => false; + + public string Author => "Microsoft"; + + public string Description => "This is the description"; + + public IReadOnlyList Classifications => []; + + public string? DefaultName => null; + + public bool PreferDefaultName => false; + + public string Identity => "Static.Test.Template"; + + public Guid GeneratorId => Generator.Id; + + public string? GroupIdentity => null; + + public int Precedence => 0; + + public string Name => "Test template"; + + public string ShortName => "test-template"; + + [Obsolete] + public IReadOnlyDictionary Tags => throw new NotImplementedException(); + + public IReadOnlyDictionary TagsCollection { get; } = new Dictionary(); + + [Obsolete] + public IReadOnlyDictionary CacheParameters => throw new NotImplementedException(); + + public IParameterDefinitionSet ParameterDefinitions => ParameterDefinitionSet.Empty; + + public IReadOnlyList Parameters => ParameterDefinitions; + + public string MountPointUri => _mountPoint.MountPointUri; + + public string ConfigPlace => "."; + + public string? LocaleConfigPlace => null; + + public string? HostConfigPlace => null; + + public string? ThirdPartyNotices => null; + + public IReadOnlyDictionary BaselineInfo { get; } = new Dictionary(); + + public bool HasScriptRunningPostActions { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public IReadOnlyList ShortNameList => new[] { ShortName }; + + public IReadOnlyList PostActions => []; + + public IReadOnlyList Constraints => []; + + public IFileSystemInfo? HostSpecificConfiguration => null; + + public bool IsValid => true; + + public IReadOnlyList ValidationErrors => []; + + public IReadOnlyDictionary Localizations { get; } = new Dictionary(); + + public IReadOnlyDictionary HostConfigFiles { get; } = new Dictionary(); + + public ILocalizationLocator? Localization => null; + + public void Dispose() + { + _mountPoint.Dispose(); + } + } + + internal class CreationEffects : ICreationEffects2, ICreationEffects + { + private CreationEffects() { } + + public static ICreationEffects2 Instance { get; } = new CreationEffects(); + + public IReadOnlyList FileChanges => new[] { FileChange.Instance }; + + public ICreationResult CreationResult => SimpleCreationResult.Instance; + + IReadOnlyList ICreationEffects.FileChanges => new[] { FileChange.Instance }; + + private class FileChange : IFileChange2, IFileChange + { + private FileChange() { } + + public static FileChange Instance { get; } = new FileChange(); + + public string SourceRelativePath => "success.txt"; + + public string TargetRelativePath => "success.txt"; + + public ChangeKind ChangeKind => ChangeKind.Create; + + public byte[] Contents => throw new NotImplementedException(); + } + } + + internal class SimpleCreationResult : ICreationResult + { + private SimpleCreationResult() { } + + public static ICreationResult Instance { get; } = new SimpleCreationResult(); + + public IReadOnlyList PostActions { get; } = []; + + public IReadOnlyList PrimaryOutputs => new[] { CreationPath.Instance }; + + private class CreationPath : ICreationPath + { + private CreationPath() { } + + public string Path => "success.txt"; + + public static CreationPath Instance { get; } = new CreationPath(); + } + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/GlobalSettingsTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/GlobalSettingsTests.cs new file mode 100644 index 000000000000..0b31cda0d914 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/GlobalSettingsTests.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Edge.BuiltInManagedProvider; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class GlobalSettingsTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _helper; + + public GlobalSettingsTests(EnvironmentSettingsHelper helper) + { + _helper = helper; + } + + [Fact] + public async Task TestLocking() + { + var envSettings = _helper.CreateEnvironment(); + var settingsFile = Path.Combine(_helper.CreateTemporaryFolder(), "settings.json"); + using var globalSettings1 = new GlobalSettings(envSettings, settingsFile); + using var globalSettings2 = new GlobalSettings(envSettings, settingsFile); + var disposable = await globalSettings1.LockAsync(default); + bool exceptionThrown = false; + using var cts = new CancellationTokenSource(50); + try + { + await globalSettings2.LockAsync(cts.Token); + } + catch (TaskCanceledException) + { + exceptionThrown = true; + } + Assert.True(exceptionThrown, nameof(globalSettings2) + " was able to get lock on when it shouldn't"); + disposable.Dispose(); + //Check that we don't time out + using var cts2 = new CancellationTokenSource(1000); + using var settingsLock = await globalSettings2.LockAsync(cts2.Token); + } + + [Fact] + public async Task TestFileWatcher() + { + var envSettings = _helper.CreateEnvironment(); + var settingsFile = Path.Combine(_helper.CreateTemporaryFolder(), "settings.json"); + using var globalSettings1 = new GlobalSettings(envSettings, settingsFile); + using var globalSettings2 = new GlobalSettings(envSettings, settingsFile); + var taskSource = new TaskCompletionSource(); + globalSettings2.SettingsChanged += () => + { + try + { + var result = globalSettings2.GetInstalledTemplatePackagesAsync(default).GetAwaiter().GetResult(); + taskSource.TrySetResult(result.Single()); + } + catch (ObjectDisposedException) + { + // FileSystemWatcher callbacks race with test cleanup disposal. + // This handler may fire after globalSettings2 is disposed at end of test. + } + }; + var mutex = await globalSettings1.LockAsync(default); + var newData = new TemplatePackageData( + Guid.NewGuid(), + "Hi", + DateTime.UtcNow, + new Dictionary() { { "a", "b" } }); + await globalSettings1.SetInstalledTemplatePackagesAsync(new[] { newData }, default); + mutex.Dispose(); + var timeoutTask = Task.Delay(10000); + var firstFinishedTask = await Task.WhenAny(timeoutTask, taskSource.Task); + Assert.Equal(taskSource.Task, firstFinishedTask); + + var newData2 = await taskSource.Task; + Assert.Equal(newData.InstallerId, newData2.InstallerId); + Assert.Equal(newData.MountPointUri, newData2.MountPointUri); + Assert.Equal(newData.Details?["a"], newData2.Details?["a"]); + Assert.Equal(newData.LastChangeTime, newData2.LastChangeTime); + } + + [Fact] + public async Task TestReadWhileLocked() + { + var envSettings = _helper.CreateEnvironment(); + var settingsFile = Path.Combine(_helper.CreateTemporaryFolder(), "settings.json"); + using var globalSettings1 = new GlobalSettings(envSettings, settingsFile); + + #region Open1AndPopulateAndSave + + using (await globalSettings1.LockAsync(default)) + { + var newData = new TemplatePackageData( + Guid.NewGuid(), + "Hi", + DateTime.UtcNow, + new Dictionary() { { "a", "b" } }); + await globalSettings1.SetInstalledTemplatePackagesAsync(new[] { newData }, default); + } + + #endregion Open1AndPopulateAndSave + + #region Open2LoadAndLock + + using var globalSettings2 = new GlobalSettings(envSettings, settingsFile); + Assert.Equal((await globalSettings1.GetInstalledTemplatePackagesAsync(default))[0].InstallerId, (await globalSettings2.GetInstalledTemplatePackagesAsync(default))[0].InstallerId); + var mutex2 = await globalSettings2.LockAsync(default); + + #endregion Open2LoadAndLock + + #region Open3Load + + using var globalSettings3 = new GlobalSettings(envSettings, settingsFile); + Assert.Equal((await globalSettings1.GetInstalledTemplatePackagesAsync(default))[0].InstallerId, (await globalSettings3.GetInstalledTemplatePackagesAsync(default))[0].InstallerId); + + #endregion Open3Load + + mutex2.Dispose(); + } + + [Fact] + public void TestDisablingFilewatcher() + { + var envSettings = _helper.CreateEnvironment(environment: new MockEnvironment(new Dictionary { { "TEMPLATE_ENGINE_DISABLE_FILEWATCHER", "1" } })); + var settingsFile = Path.Combine(_helper.CreateTemporaryFolder(), "settings.json"); + using var globalSettings1 = new GlobalSettings(envSettings, settingsFile); + Assert.Empty(((MonitoredFileSystem)envSettings.Host.FileSystem).FilesWatched); + + envSettings = _helper.CreateEnvironment(); + settingsFile = Path.Combine(_helper.CreateTemporaryFolder(), "settings.json"); + using var globalSettings2 = new GlobalSettings(envSettings, settingsFile); + Assert.Single(((MonitoredFileSystem)envSettings.Host.FileSystem).FilesWatched); + Assert.Equal(settingsFile, ((MonitoredFileSystem)envSettings.Host.FileSystem).FilesWatched.Single()); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/HostConstraintTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/HostConstraintTests.cs new file mode 100644 index 000000000000..9aa38b3d0f53 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/HostConstraintTests.cs @@ -0,0 +1,296 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using FakeItEasy; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Edge.Constraints; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class HostConstraintTests + { + [Fact] + public async Task CanReadConfiguration_WithoutVersion() + { + var config = new + { + identity = "test", + constraints = new + { + host = new + { + type = "host", + args = new[] + { + new + { + hostName = "host1", + version = string.Empty + }, + new + { + hostName = "host2", + version = "1.0.0" + }, + new + { + hostName = "host3", + version = "[1.0.0-*]" + }, + + } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Host.HostIdentifier).Returns("host1"); + A.CallTo(() => settings.Host.Version).Returns("2.0.0"); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new HostConstraintFactory() }); + + var constraintManager = new TemplateConstraintManager(settings); + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + Assert.Equal(TemplateConstraintResult.Status.Allowed, evaluateResult.EvaluationStatus); + } + + [Fact] + public async Task CanReadConfiguration_ExactVersion() + { + var config = new + { + identity = "test", + constraints = new + { + host = new + { + type = "host", + args = new[] + { + new + { + hostName = "host1", + version = string.Empty + }, + new + { + hostName = "host2", + version = "1.0.0" + }, + new + { + hostName = "host3", + version = "[1.0.0-*]" + }, + + } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Host.HostIdentifier).Returns("host2"); + A.CallTo(() => settings.Host.Version).Returns("2.0.0"); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new HostConstraintFactory() }); + + var constraintManager = new TemplateConstraintManager(settings); + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + Assert.Equal(TemplateConstraintResult.Status.Restricted, evaluateResult.EvaluationStatus); + Assert.Equal("Running template on host2 (version: 2.0.0) is not supported, supported hosts is/are: host1, host2(1.0.0), host3([1.0.0-*]).", evaluateResult.LocalizedErrorMessage); + Assert.Null(evaluateResult.CallToAction); + + } + + [Fact] + public async Task CanReadConfiguration_VersionRange() + { + var config = new + { + identity = "test", + constraints = new + { + host = new + { + type = "host", + args = new[] + { + new + { + hostName = "host1", + version = string.Empty + }, + new + { + hostName = "host2", + version = "1.0.0" + }, + new + { + hostName = "host3", + version = "[1.0.0-*]" + }, + + } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Host.HostIdentifier).Returns("host3"); + A.CallTo(() => settings.Host.Version).Returns("2.0.0"); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new HostConstraintFactory() }); + + var constraintManager = new TemplateConstraintManager(settings); + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + Assert.Equal(TemplateConstraintResult.Status.Allowed, evaluateResult.EvaluationStatus); + } + + [Fact] + public async Task FailsOnWrongConfiguration() + { + var config = new + { + identity = "test", + constraints = new + { + host = new + { + type = "host", + args = + new + { + hostName = "host2", + version = "1.0.0" + } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Host.HostIdentifier).Returns("host3"); + A.CallTo(() => settings.Host.Version).Returns("2.0.0"); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new HostConstraintFactory() }); + + var constraintManager = new TemplateConstraintManager(settings); + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + Assert.Equal(TemplateConstraintResult.Status.NotEvaluated, evaluateResult.EvaluationStatus); + Assert.Equal("'{\"hostName\":\"host2\",\"version\":\"1.0.0\"}' is not a valid JSON array.", evaluateResult.LocalizedErrorMessage); + Assert.Equal("Check the constraint configuration in template.json.", evaluateResult.CallToAction); + } + + [Theory] + [InlineData("1.0.0", "1.0", true)] + [InlineData("(, 1.0.0]", "1.0", true)] + [InlineData("(, 1.0.0]", "2.0", false)] + [InlineData("[1.0.0]", "1.0.0", true)] + [InlineData("[1.0.0]", "2.0.0", false)] + [InlineData("[1.0, 2.0-preview3]", "2.0-preview1", true)] + [InlineData("[1.0, 2.0-preview3]", "2.0-preview4", false)] + [InlineData("[1.0, 2.0-preview3]", "2.0", false)] + [InlineData("[1.0, 2.0]", "1.2", true)] + [InlineData("[1.0, 2.0]", "2.2", false)] + //legacy format + [InlineData("[1.0-*]", "2.2", true)] + [InlineData("[*-2.0]", "2.2", false)] + public async Task CanProcessDifferentVersions(string configuredVersion, string hostVersion, bool expectedResult) + { + var config = new + { + identity = "test", + constraints = new + { + host = new + { + type = "host", + args = new[] + { + new + { + hostName = "host1", + version = configuredVersion + } + } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Host.HostIdentifier).Returns("host1"); + A.CallTo(() => settings.Host.Version).Returns(hostVersion); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new HostConstraintFactory() }); + + var constraintManager = new TemplateConstraintManager(settings); + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + if (expectedResult) + { + Assert.Equal(TemplateConstraintResult.Status.Allowed, evaluateResult.EvaluationStatus); + } + else + { + Assert.Equal(TemplateConstraintResult.Status.Restricted, evaluateResult.EvaluationStatus); + } + } + + [Theory] + [InlineData("host1", "fallback|other", "1.1", true)] + [InlineData("host1", "fallback|other", "2.1", false)] + [InlineData("host2", "fallback|other", "2.1", true)] + [InlineData("host2", "fallback|other", "3.1", false)] + public async Task CanProcessDifferentHostNames(string hostName, string fallbackHostNames, string hostVersion, bool expectedResult) + { + var config = new + { + identity = "test", + constraints = new + { + host = new + { + type = "host", + args = new[] + { + new + { + hostName = "host1", + version = "[1.0, 2.0]" + }, + new + { + hostName = "fallback", + version = "[2.0, 3.0]" + }, + } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Host.HostIdentifier).Returns(hostName); + A.CallTo(() => settings.Host.Version).Returns(hostVersion); + A.CallTo(() => settings.Host.FallbackHostTemplateConfigNames).Returns(fallbackHostNames.Split('|')); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new HostConstraintFactory() }); + + var constraintManager = new TemplateConstraintManager(settings); + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + if (expectedResult) + { + Assert.Equal(TemplateConstraintResult.Status.Allowed, evaluateResult.EvaluationStatus); + } + else + { + Assert.Equal(TemplateConstraintResult.Status.Restricted, evaluateResult.EvaluationStatus); + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/LocalizationTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/LocalizationTests.cs new file mode 100644 index 000000000000..228e09bb56c5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/LocalizationTests.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.Edge.Template; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class LocalizationTests : TestBase, IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public LocalizationTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Theory] + [InlineData(null, "name")] + [InlineData("de-DE", "name_de-DE:äÄßöÖüÜ")] + [InlineData("tr-TR", "name_tr-TR:çÇğĞıIİöÖşŞüÜ")] + public void TestLocalizedTemplateName(string? locale, string expectedName) + { + _ = LoadHostWithLocalizationTemplates(locale, out _, out ITemplateInfo localizationTemplate); + + Assert.Equal(expectedName, localizationTemplate.Name); + } + + [Theory] + [InlineData(null, "desc")] + [InlineData("de-DE", "desc_de-DE:äÄßöÖüÜ")] + [InlineData("tr-TR", "desc_tr-TR:çÇğĞıIİöÖşŞüÜ")] + public void TestLocalizedTemplateDescription(string? locale, string expectedDescription) + { + _ = LoadHostWithLocalizationTemplates(locale, out _, out ITemplateInfo localizationTemplate); + + Assert.Equal(expectedDescription, localizationTemplate.Description); + } + + [Theory] + [InlineData(null, "someSymbol", "sym0_displayName")] + [InlineData("de-DE", "someSymbol", "sym0_displayName_de-DE:äÄßöÖüÜ")] + [InlineData("tr-TR", "someSymbol", "sym0_displayName_tr-TR:çÇğĞıIİöÖşŞüÜ")] + public void TestLocalizedSymbolDisplayName(string? locale, string symbolName, string expectedDisplayName) + { + _ = LoadHostWithLocalizationTemplates(locale, out _, out ITemplateInfo localizationTemplate); + + ITemplateParameter? symbol = localizationTemplate.ParameterDefinitions?.FirstOrDefault(p => p.Name == symbolName); + Assert.NotNull(symbol); + Assert.Equal(expectedDisplayName, symbol?.DisplayName); + } + + [Theory] + [InlineData(null, "someChoice", "sym1_displayName")] + [InlineData("de-DE", "someChoice", "sym1_displayName")] + [InlineData("tr-TR", "someChoice", "sym1_displayName_tr-TR:çÇğĞıIİöÖşŞüÜ")] + public void TestLocalizedSymbolChoiceDisplayName(string? locale, string symbolName, string expectedDisplayName) + { + _ = LoadHostWithLocalizationTemplates(locale, out _, out ITemplateInfo localizationTemplate); + + ITemplateParameter? symbol = localizationTemplate.ParameterDefinitions?.FirstOrDefault(p => p.Name == symbolName); + Assert.NotNull(symbol); + Assert.Equal(expectedDisplayName, symbol?.DisplayName); + } + + [Theory] + [InlineData(null, "sym0_desc")] + [InlineData("de-DE", "sym0_desc_de-DE:äÄßöÖüÜ")] + [InlineData("tr-TR", "sym0_desc_tr-TR:çÇğĞıIİöÖşŞüÜ")] + public void TestLocalizedSymbolDescription(string? locale, string expectedDescription) + { + _ = LoadHostWithLocalizationTemplates(locale, out _, out ITemplateInfo localizationTemplate); + + ITemplateParameter? symbol = localizationTemplate.ParameterDefinitions?.FirstOrDefault(p => p.Name == "someSymbol"); + Assert.NotNull(symbol); + Assert.Equal(expectedDescription, symbol!.Description); + } + + [Theory] + [InlineData(null, "sym1_desc", "sym1_choice0", "sym1_choice1", "sym1_choice2")] + [InlineData("de-DE", "sym1_desc_de-DE:äÄßöÖüÜ", "sym1_choice0_de-DE:äÄßöÖüÜ", "sym1_choice1_de-DE:äÄßöÖüÜ", "sym1_choice2")] + [InlineData("tr-TR", "sym1_desc_tr-TR:çÇğĞıIİöÖşŞüÜ", "sym1_choice0_tr-TR:çÇğĞıIİöÖşŞüÜ", "sym1_choice1", "sym1_choice2_tr-TR:çÇğĞıIİöÖşŞüÜ")] + public void TestLocalizedSymbolChoices( + string? locale, + string symbolDesc, + string choice0Desc, + string choice1Desc, + string choice2Desc) + { + _ = LoadHostWithLocalizationTemplates(locale, out _, out ITemplateInfo localizationTemplate); + + ITemplateParameter? symbol = localizationTemplate.ParameterDefinitions?.FirstOrDefault(p => p.Name == "someChoice"); + Assert.NotNull(symbol); + Assert.Equal(symbolDesc, symbol!.Description); + + var choices = symbol.Choices; + Assert.NotNull(choices); + Assert.True(choices!.TryGetValue("choice0", out ParameterChoice? choice0), "Template symbol should contain a choice with name 'choice0'."); + Assert.Equal(choice0Desc, choice0?.Description); + Assert.True(choices.TryGetValue("choice1", out ParameterChoice? choice1), "Template symbol should contain a choice with name 'choice1'."); + Assert.Equal(choice1Desc, choice1?.Description); + Assert.True(choices.TryGetValue("choice2", out ParameterChoice? choice2), "Template symbol should contain a choice with name 'choice2'."); + Assert.Equal(choice2Desc, choice2?.Description); + } + + [Theory] + [InlineData(0, null, "pa0_desc", "pa0_manualInstructions")] + [InlineData(0, "de-DE", "pa0_desc_de-DE:äÄßöÖüÜ", "pa0_manualInstructions_de-DE:äÄßöÖüÜ")] + [InlineData(0, "tr-TR", "pa0_desc_tr-TR:çÇğĞıIİöÖşŞüÜ", "pa0_manualInstructions_tr-TR:çÇğĞıIİöÖşŞüÜ")] + [InlineData(1, null, "pa1_desc", "pa1_manualInstructions0")] + [InlineData(1, "de-DE", "pa1_desc_de-DE:äÄßöÖüÜ", "pa1_manualInstructions0")] + [InlineData(1, "tr-TR", "pa1_desc_tr-TR:çÇğĞıIİöÖşŞüÜ", "pa1_manualInstructions0_tr-TR:çÇğĞıIİöÖşŞüÜ")] + public async Task TestLocalizedPostActionFields( + int postActionIndex, + string? locale, + string expectedDescription, + string expectedManualInstructions) + { + var environmentSettings = LoadHostWithLocalizationTemplates(locale, out _, out ITemplateInfo localizationTemplate); + var templateCreator = new TemplateCreator(environmentSettings); + ITemplate? template = await templateCreator.LoadTemplateAsync(localizationTemplate, null, default); + Assert.NotNull(template); + Assert.NotNull(template!.Generator); + + ICreationEffects effects = await template.Generator.GetCreationEffectsAsync( + environmentSettings, + template, + new ParameterSetData(template), + Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()), + default); + + Assert.NotNull(effects); + Assert.NotNull(effects.CreationResult); + Assert.NotNull(effects.CreationResult.PostActions); + Assert.True(effects.CreationResult.PostActions.Count > postActionIndex, "Template does not contain enough post actions"); + Assert.Equal(expectedDescription, effects.CreationResult.PostActions[postActionIndex].Description); + Assert.Equal(expectedManualInstructions, effects.CreationResult.PostActions[postActionIndex].ManualInstructions); + } + + [Theory] + [InlineData("de", "name")] + [InlineData("de-de", "name_de-DE:äÄßöÖüÜ")] + [InlineData("de-AT", "name")] + [InlineData("de-at", "name")] + [InlineData("tr", "name_tr-TR:çÇğĞıIİöÖşŞüÜ")] + [InlineData("tr-TR", "name_tr-TR:çÇğĞıIİöÖşŞüÜ")] + public void TestLocaleCountryFallback(string locale, string expectedName) + { + _ = LoadHostWithLocalizationTemplates(locale, out _, out ITemplateInfo localizationTemplate); + + Assert.Equal(expectedName, localizationTemplate.Name); + } + + private IEngineEnvironmentSettings LoadHostWithLocalizationTemplates(string? locale, out TemplatePackageManager templatePackageManager, out ITemplateInfo localizationTemplate) + { + var builtins = BuiltInTemplatePackagesProviderFactory.GetComponents(GetTestTemplateLocation("TemplateWithLocalization")); + var env = _environmentSettingsHelper.CreateEnvironment(locale: locale, additionalComponents: builtins); + templatePackageManager = new TemplatePackageManager(env); + IReadOnlyList localizedTemplates = templatePackageManager.GetTemplatesAsync(default).Result; + + Assert.True(localizedTemplates.Count != 0, "Test template couldn't be loaded."); + var template = localizedTemplates.FirstOrDefault(t => t.Identity == "TestAssets.TemplateWithLocalization"); + Assert.NotNull(template); + localizationTemplate = template!; + + return env; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/Microsoft.TemplateEngine.Edge.UnitTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/Microsoft.TemplateEngine.Edge.UnitTests.csproj new file mode 100644 index 000000000000..7b7f81c75763 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/Microsoft.TemplateEngine.Edge.UnitTests.csproj @@ -0,0 +1,24 @@ + + + + $(NetCurrent);$(NetFrameworkCurrent) + + + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/Mocks/MockPackageManager.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/Mocks/MockPackageManager.cs new file mode 100644 index 000000000000..f379d97999d9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/Mocks/MockPackageManager.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FakeItEasy; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Edge.Installers.NuGet; +using Microsoft.TemplateEngine.TestHelper; +using NuGet.Protocol.Core.Types; +using static Microsoft.TemplateEngine.Edge.Installers.NuGet.NuGetApiPackageManager; + +namespace Microsoft.TemplateEngine.Edge.UnitTests.Mocks +{ + internal class MockPackageManager : IDownloader, IUpdateChecker + { + internal const string DefaultFeed = "test_feed"; + private readonly PackageManager? _packageManager; + private readonly string? _packageToPack; + + internal MockPackageManager() + { + } + + internal MockPackageManager(PackageManager packageManager, string packageToPack) + { + _packageManager = packageManager; + _packageToPack = packageToPack; + } + + public Task DownloadPackageAsync(string downloadPath, string identifier, string? version = null, IEnumerable? additionalSources = null, bool force = false, CancellationToken cancellationToken = default) + { + // names of exceptions throw them for test purposes + switch (identifier) + { + case nameof(InvalidNuGetSourceException): throw new InvalidNuGetSourceException("test message"); + case nameof(DownloadException): throw new DownloadException(identifier, version ?? string.Empty, new[] { DefaultFeed }); + case nameof(PackageNotFoundException): throw new PackageNotFoundException(identifier, new[] { DefaultFeed }); + case nameof(VulnerablePackageException) when version == "12.0.3": + throw new VulnerablePackageException("Test Message", identifier, version, GetMockVulnerabilities()); + case nameof(Exception): throw new Exception("Generic error"); + } + + if (_packageManager == null) + { + throw new InvalidOperationException($"{nameof(_packageManager)} was not initialized"); + } + if (_packageToPack == null) + { + throw new InvalidOperationException($"{nameof(_packageToPack)} was not initialized"); + } + + string testPackageLocation = _packageManager.PackNuGetPackage(_packageToPack); + string targetFileName = string.IsNullOrWhiteSpace(version) + ? Path.GetFileName(testPackageLocation) + : $"{Path.GetFileNameWithoutExtension(testPackageLocation)}.{version}.nupkg"; + File.Copy(testPackageLocation, Path.Combine(downloadPath, targetFileName)); + return Task.FromResult( + new NuGetPackageInfo( + "Microsoft", + "Microsoft", + true, + Path.Combine(downloadPath, targetFileName), + DefaultFeed, + identifier, + version ?? string.Empty, + [])); + } + + public Task<(string LatestVersion, bool IsLatestVersion, IReadOnlyList Vulnerabilities)> GetLatestVersionAsync(string identifier, string? version = null) + { + // names of exceptions throw them for test purposes + return identifier switch + { + nameof(InvalidNuGetSourceException) => throw new InvalidNuGetSourceException("test message"), + nameof(DownloadException) => throw new DownloadException(identifier, version ?? string.Empty, new[] { DefaultFeed }), + nameof(PackageNotFoundException) => throw new PackageNotFoundException(identifier, new[] { DefaultFeed }), + nameof(VulnerablePackageException) when version == "12.0.0" => throw new VulnerablePackageException("Test Message", identifier, version, GetMockVulnerabilities()), + nameof(Exception) => throw new Exception("Generic error"), + _ => Task.FromResult(("1.0.0", version != "1.0.0", (IReadOnlyList)new List())), + }; + } + + Task<(string LatestVersion, bool IsLatestVersion, NugetPackageMetadata PackageMetadata)> IUpdateChecker.GetLatestVersionAsync(string identifier, string? version, string? additionalNuGetSource, CancellationToken cancellationToken) + { + // names of exceptions throw them for test purposes + return identifier switch + { + nameof(InvalidNuGetSourceException) => throw new InvalidNuGetSourceException("test message"), + nameof(DownloadException) => throw new DownloadException(identifier, version ?? string.Empty, new[] { DefaultFeed }), + nameof(PackageNotFoundException) => throw new PackageNotFoundException(identifier, new[] { DefaultFeed }), + nameof(VulnerablePackageException) when version == "12.0.0" => throw new VulnerablePackageException("Test Message", identifier, version, GetMockVulnerabilities()), + nameof(Exception) => throw new Exception("Generic error"), + _ => Task.FromResult(("1.0.0", version != "1.0.0", new NugetPackageMetadata(A.Fake(), "owners", false))), + }; + } + + private IReadOnlyList GetMockVulnerabilities() => new List() + { + new VulnerabilityInfo(1, new List() { "https://testUrl1.com" }), + new VulnerabilityInfo(2, new List() { "https://testUrl2.com" }), + new VulnerabilityInfo(3, new List() { "https://testUrl3.com" }) + }; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGetApiPackageManagerTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGetApiPackageManagerTests.cs new file mode 100644 index 000000000000..5c71d02937cf --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGetApiPackageManagerTests.cs @@ -0,0 +1,283 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Edge.Installers.NuGet; +using Microsoft.TemplateEngine.TestHelper; +using NuGet.Configuration; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class NuGetApiPackageManagerTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + private readonly IList _additionalSources = new[] { "https://api.nuget.org/v3/index.json" }; + + public NuGetApiPackageManagerTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Fact] + public async Task DownloadPackage_Latest() + { + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + var result = await packageManager.DownloadPackageAsync( + installPath, + "Microsoft.DotNet.Common.ProjectTemplates.5.0", + // use a different source for checking specific nuget metadata + additionalSources: new[] { "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" }); + + result.Author.Should().Be("Microsoft"); + result.FullPath.Should().ContainAll(installPath, "Microsoft.DotNet.Common.ProjectTemplates.5.0"); + Assert.True(File.Exists(result.FullPath)); + result.PackageIdentifier.Should().Be("Microsoft.DotNet.Common.ProjectTemplates.5.0"); + result.Owners.Should().Be(string.Empty); + result.Reserved.Should().BeFalse(); + result.PackageVersion.Should().NotBeNullOrEmpty(); + result.NuGetSource.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task DownloadPackage_LatestFromNugetOrgFeed() + { + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + + var result = await packageManager.DownloadPackageAsync( + installPath, + "Microsoft.DotNet.Common.ProjectTemplates.5.0", + // add the source for getting ownership info + additionalSources: _additionalSources); + + result.Author.Should().Be("Microsoft"); + result.FullPath.Should().ContainAll(installPath, "Microsoft.DotNet.Common.ProjectTemplates.5.0"); + Assert.True(File.Exists(result.FullPath)); + result.PackageIdentifier.Should().Be("Microsoft.DotNet.Common.ProjectTemplates.5.0"); + result.Owners.Should().Be("dotnetframework, Microsoft"); + result.Reserved.Should().BeTrue(); + result.PackageVersion.Should().NotBeNullOrEmpty(); + result.NuGetSource.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task DownloadPackage_SpecificVersion() + { + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + var result = await packageManager.DownloadPackageAsync( + installPath, + "Microsoft.DotNet.Common.ProjectTemplates.5.0", + "5.0.0", + additionalSources: _additionalSources); + + result.Author.Should().Be("Microsoft"); + result.FullPath.Should().ContainAll(installPath, "Microsoft.DotNet.Common.ProjectTemplates.5.0", "5.0.0"); + Assert.True(File.Exists(result.FullPath)); + result.PackageIdentifier.Should().Be("Microsoft.DotNet.Common.ProjectTemplates.5.0"); + result.PackageVersion.Should().Be("5.0.0"); + result.NuGetSource.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task DownloadPackage_UnknownPackage() + { + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + var exception = await Assert.ThrowsAsync(() => packageManager.DownloadPackageAsync( + installPath, "Microsoft.DotNet.NotCommon.ProjectTemplates.5.0", "5.0.0", additionalSources: _additionalSources)); + + exception.PackageIdentifier.Should().Be("Microsoft.DotNet.NotCommon.ProjectTemplates.5.0"); + exception.PackageVersion.Should().NotBeNull(); + exception.PackageVersion!.ToString().Should().Be("5.0.0"); + exception.Message.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task DownloadPackage_InvalidPath() + { + string installPath = ":/?"; + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + var exception = await Assert.ThrowsAsync(() => packageManager.DownloadPackageAsync( + installPath, "Microsoft.DotNet.Common.ProjectTemplates.5.0", "5.0.0", additionalSources: _additionalSources)); + + exception.PackageIdentifier.Should().Be("Microsoft.DotNet.Common.ProjectTemplates.5.0"); + exception.PackageVersion.ToString().Should().Be("5.0.0"); + exception.Message.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task DownloadPackage_CannotOverwritePackage() + { + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + await packageManager.DownloadPackageAsync(installPath, "Microsoft.DotNet.Common.ProjectTemplates.5.0", "5.0.0", additionalSources: _additionalSources); + var exception = await Assert.ThrowsAsync(() => packageManager.DownloadPackageAsync( + installPath, "Microsoft.DotNet.Common.ProjectTemplates.5.0", "5.0.0", additionalSources: _additionalSources)); + + exception.PackageIdentifier.Should().Be("Microsoft.DotNet.Common.ProjectTemplates.5.0"); + exception.PackageVersion.ToString().Should().Be("5.0.0"); + exception.Message.Should().NotBeNullOrEmpty(); + } + + [Fact] + internal async Task DownloadPackage_HasVulnerabilities() + { + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + // Getting this version of the package as it has known vulnerabilities + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + + var exception = await Assert.ThrowsAsync(() => packageManager.DownloadPackageAsync( + installPath, + "System.Text.Json", + "8.0.4", + // add the source for getting vulnerability info + additionalSources: _additionalSources)); + + exception.PackageIdentifier.Should().Be("System.Text.Json"); + exception.PackageVersion.Should().Be("8.0.4"); + exception.Vulnerabilities.Should().NotBeNullOrEmpty(); + } + + [Fact] + internal async Task DownloadPackage_HasVulnerabilitiesForce() + { + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + // Getting this version of the package as it has known vulnerabilities + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + + var result = await packageManager.DownloadPackageAsync( + installPath, + "System.Text.Json", + "8.0.4", + // add the source for getting vulnerability info + additionalSources: _additionalSources, + force: true); + + result.PackageIdentifier.Should().Be("System.Text.Json"); + result.Author.Should().Be("Microsoft"); + result.PackageVersion.Should().Be("8.0.4"); + Assert.True(File.Exists(result.FullPath)); + result.PackageVulnerabilities.Should().NotBeNullOrEmpty(); + result.NuGetSource.Should().Be(_additionalSources[0]); + } + + [Fact] + public async Task GetLatestVersion_Success() + { + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + (string latestVersion, bool isLatestVersion, _) = await packageManager.GetLatestVersionAsync("Microsoft.DotNet.Common.ProjectTemplates.5.0", additionalSource: _additionalSources.FirstOrDefault()); + + latestVersion.Should().NotBeNullOrEmpty(); + isLatestVersion.Should().BeFalse(); + } + + [Fact] + public async Task GetLatestVersion_SpecificVersion() + { + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + (string latestVersion, bool isLatestVersion, _) = await packageManager.GetLatestVersionAsync( + "Microsoft.DotNet.Common.ProjectTemplates.5.0", "5.0.0", additionalSource: _additionalSources.First()); + + latestVersion.Should().NotBe("5.0.0"); + isLatestVersion.Should().BeFalse(); + } + + [Fact] + public async Task GetLatestVersion_UnknownPackage() + { + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + var exception = await Assert.ThrowsAsync(() => packageManager.GetLatestVersionAsync( + "Microsoft.DotNet.NotCommon.ProjectTemplates.5.0", "5.0.0", additionalSource: _additionalSources.FirstOrDefault())); + + exception.PackageIdentifier.Should().Be("Microsoft.DotNet.NotCommon.ProjectTemplates.5.0"); + exception.Message.Should().NotBeNullOrEmpty(); + } + + [Fact] + public void RemoveInsecurePackages_AllInsecure() + { + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + List allPackages = new List() + { + new PackageSource("http://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json"), + new PackageSource("http://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json"), + new PackageSource("http://pkgs.dev.azure.com/dnceng/public/_packaging/nuget-build/nuget/v3/index.json"), + new PackageSource("http://insecure-feed.org") + }; + var securePackages = packageManager.RemoveInsecurePackages(allPackages); + + securePackages.Should().BeEmpty(); + } + + [Fact] + public void RemoveInsecurePackages_AllSecure() + { + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + List allPackages = new List() + { + new PackageSource("https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json"), + new PackageSource("https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json"), + new PackageSource("https://pkgs.dev.azure.com/dnceng/public/_packaging/nuget-build/nuget/v3/index.json") + }; + var securePackages = packageManager.RemoveInsecurePackages(allPackages); + + securePackages.Should().NotBeEmpty(); + Assert.Equal(allPackages, securePackages); + } + + [Fact] + public void RemoveInsecurePackages_Mixed() + { + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + + NuGetApiPackageManager packageManager = new NuGetApiPackageManager(engineEnvironmentSettings); + List allPackages = new List() + { + new PackageSource("https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json"), + new PackageSource("https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json"), + new PackageSource("http://pkgs.dev.azure.com/dnceng/public/_packaging/nuget-build/nuget/v3/index.json"), + new PackageSource("http://insecure-feed.org") + }; + var securePackages = packageManager.RemoveInsecurePackages(allPackages); + + var expectedOutcome = new List() + { + new PackageSource("https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json"), + new PackageSource("https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json") + }; + + securePackages.Should().NotBeEmpty(); + securePackages.Should().Equal(expectedOutcome); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGetInstallerTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGetInstallerTests.cs new file mode 100644 index 000000000000..b51855d2b77c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGetInstallerTests.cs @@ -0,0 +1,568 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Edge.Installers.NuGet; +using Microsoft.TemplateEngine.Edge.UnitTests.Mocks; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class NuGetInstallerTests : TestBase, IClassFixture, IClassFixture + { + private readonly PackageManager _packageManager; + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public NuGetInstallerTests(PackageManager packageManager, EnvironmentSettingsHelper environmentSettingsHelper) + { + _packageManager = packageManager; + _environmentSettingsHelper = environmentSettingsHelper; + } + + public static IEnumerable SerializationData() + { + //can read details + yield return new object[] + { + new TemplatePackageData( + default, + "MountPointUri", + default, + new Dictionary + { + { "Author", "TestAuthor" }, + { "NuGetSource", "https://api.nuget.org/v3/index.json" }, + { "PackageId", "TestPackage" }, + { "Version", "4.7.0.395" }, + { "Owners", "test, test2" }, + { "Reserved", "true" } + }), + "TestPackage", "4.7.0.395", "TestAuthor", "https://api.nuget.org/v3/index.json", false, "true", "test, test2" + }; + //skips irrelevant details + yield return new object?[] + { + new TemplatePackageData( + default, + "MountPointUri", + default, + new Dictionary + { + { "Irrelevant", "not needed" }, + { "NuGetSource", "https://api.nuget.org/v3/index.json" }, + { "PackageId", "TestPackage" }, + { "Version", "4.7.0.395" }, + { "Owners", "test, test2" }, + { "Reserved", "false" } + }), + "TestPackage", "4.7.0.395", null, "https://api.nuget.org/v3/index.json", false, "false", "test, test2" + }; + } + + [Fact] + public async Task CanInstall_LocalPackage() + { + MockInstallerFactory factory = new MockInstallerFactory(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + string package = PackTestTemplatesNuGetPackage(_packageManager); + + InstallRequest request = new InstallRequest(package); + Assert.True(await installer.CanInstallAsync(request, CancellationToken.None)); + } + + [Fact] + public async Task CanInstall_CannotInstallDirectory() + { + MockInstallerFactory factory = new MockInstallerFactory(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + string package = Directory.GetCurrentDirectory(); + + InstallRequest request = new InstallRequest(package); + Assert.False(await installer.CanInstallAsync(request, CancellationToken.None)); + } + + [Fact] + public async Task CanInstall_CannotInstallFile() + { + MockInstallerFactory factory = new MockInstallerFactory(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + string package = typeof(NuGetInstallerTests).Assembly.Location; + + InstallRequest request = new InstallRequest(package); + Assert.False(await installer.CanInstallAsync(request, CancellationToken.None)); + } + + [Theory] + [InlineData("ValidId", "", true)] + [InlineData("Invalid&%", "", false)] + [InlineData("ValidId", "1.0.0", true)] + [InlineData("ValidId", "InvalidVersion", false)] + [InlineData("Invalid&%", "InvalidVersion", false)] + public async Task CanInstall_RemotePackage(string identifier, string version, bool result) + { + MockInstallerFactory factory = new MockInstallerFactory(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + InstallRequest request = new InstallRequest(identifier, version); + + Assert.Equal(result, await installer.CanInstallAsync(request, CancellationToken.None)); + } + + [Fact] + public async Task Install_LocalPackage() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + string package = PackTestTemplatesNuGetPackage(_packageManager); + + InstallRequest request = new InstallRequest(package); + + InstallResult installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + + Assert.True(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + Assert.Equal(InstallerErrorCode.Success, installResult.Error); + installResult.ErrorMessage.Should().BeNullOrEmpty(); + + var source = installResult.TemplatePackage as NuGetManagedTemplatePackage; + Assert.NotNull(source); + source!.MountPointUri.Should().ContainAll(new[] { installPath, "Microsoft.TemplateEngine.TestTemplates" }); + source.Author.Should().Be("Microsoft"); + source.Owners.Should().BeNull(); + source.Reserved.Should().Be("False"); + source.Version.Should().NotBeNullOrEmpty(); + source.DisplayName.Should().StartWith("Microsoft.TemplateEngine.TestTemplates@"); + source.Identifier.Should().Be("Microsoft.TemplateEngine.TestTemplates"); + source.Installer.Should().Be(installer); + source.IsLocalPackage.Should().Be(true); + source.Provider.Should().Be(provider); + } + + [Fact] + public async Task Install_LocalPackage_CannotInstallUnsupportedRequest() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + string package = typeof(NuGetInstallerTests).Assembly.Location; + + InstallRequest request = new InstallRequest(package); + InstallResult installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + + Assert.False(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + Assert.Equal(InstallerErrorCode.UnsupportedRequest, installResult.Error); + installResult.ErrorMessage.Should().NotBeNullOrEmpty(); + installResult.TemplatePackage.Should().BeNull(); + } + + [Fact] + public async Task Install_LocalPackage_CannotInstallSamePackageTwice() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + string package = PackTestTemplatesNuGetPackage(_packageManager); + + InstallRequest request = new InstallRequest(package); + + InstallResult installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + + Assert.True(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + + installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + + Assert.False(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + Assert.Equal(InstallerErrorCode.DownloadFailed, installResult.Error); + installResult.ErrorMessage.Should().NotBeNullOrEmpty(); + installResult.TemplatePackage.Should().BeNull(); + } + + [Fact] + public async Task Install_RemotePackage() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(_packageManager, TestPackageProjectPath); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + InstallRequest request = new InstallRequest("Microsoft.TemplateEngine.TestTemplates", "1.0.0"); + + InstallResult installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + + Assert.True(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + Assert.Equal(InstallerErrorCode.Success, installResult.Error); + installResult.ErrorMessage.Should().BeNullOrEmpty(); + + var source = installResult.TemplatePackage as NuGetManagedTemplatePackage; + Assert.NotNull(source); + source!.MountPointUri.Should().ContainAll(new[] { installPath, "Microsoft.TemplateEngine.TestTemplates" }); + source.Author.Should().Be("Microsoft"); + source.Owners.Should().Be("Microsoft"); + source.Reserved.Should().Be("True"); + source.Version.Should().Be("1.0.0"); + source.DisplayName.Should().Be("Microsoft.TemplateEngine.TestTemplates@1.0.0"); + source.Identifier.Should().Be("Microsoft.TemplateEngine.TestTemplates"); + source.Installer.Should().Be(installer); + source.IsLocalPackage.Should().Be(false); + source.Provider.Should().Be(provider); + source.NuGetSource.Should().Be(MockPackageManager.DefaultFeed); + } + + [Theory] + [InlineData(nameof(DownloadException), InstallerErrorCode.DownloadFailed)] + [InlineData(nameof(PackageNotFoundException), InstallerErrorCode.PackageNotFound)] + [InlineData(nameof(InvalidNuGetSourceException), InstallerErrorCode.InvalidSource)] + [InlineData(nameof(Exception), InstallerErrorCode.GenericError)] + public async Task Install_RemotePackage_HandleExceptions(string exception, InstallerErrorCode expectedErrorCode) + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + InstallRequest request = new InstallRequest(exception); + + InstallResult installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + + Assert.False(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + Assert.Equal(expectedErrorCode, installResult.Error); + installResult.ErrorMessage.Should().NotBeNullOrEmpty(); + installResult.TemplatePackage.Should().BeNull(); + } + + [Fact] + public async Task Install_RemotePackage_HandleVulnerablePackage() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + InstallRequest request = new InstallRequest(nameof(VulnerablePackageException), "12.0.3"); + + InstallResult installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + Assert.False(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + Assert.Equal(InstallerErrorCode.VulnerablePackage, installResult.Error); + installResult.ErrorMessage.Should().NotBeNullOrEmpty(); + installResult.TemplatePackage.Should().BeNull(); + installResult.Vulnerabilities.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task GetLatestVersion_RemotePackage() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(_packageManager, TestPackageProjectPath); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + InstallRequest request = new InstallRequest("Microsoft.TemplateEngine.TestTemplates", "1.0.0"); + + InstallResult installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + + Assert.True(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + + NuGetManagedTemplatePackage? source = installResult.TemplatePackage as NuGetManagedTemplatePackage; + Assert.NotNull(source); + IReadOnlyList checkUpdateResults = await installer.GetLatestVersionAsync(new[] { source! }, provider, CancellationToken.None); + + Assert.Single(checkUpdateResults); + CheckUpdateResult result = checkUpdateResults.Single(); + + Assert.True(result.Success); + Assert.Equal(source, result.TemplatePackage); + Assert.Equal(InstallerErrorCode.Success, result.Error); + result.ErrorMessage.Should().BeNullOrEmpty(); + Assert.Equal("1.0.0", result.LatestVersion); + Assert.False(result.IsLatestVersion); + } + + [Fact] + public async Task GetLatestVersion_RemotePackageWithVulnerabilities() + { + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(_packageManager, TestPackageProjectPath); + + NuGetInstaller installer = new NuGetInstaller(new MockInstallerFactory(), engineEnvironmentSettings, _environmentSettingsHelper.CreateTemporaryFolder(), mockPackageManager, mockPackageManager); + InstallRequest request = new InstallRequest(nameof(VulnerablePackageException), "1.0.0"); + InstallResult installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + + Assert.True(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + + NuGetManagedTemplatePackage? source = installResult.TemplatePackage as NuGetManagedTemplatePackage; + Assert.NotNull(source); + source.Version = "12.0.0"; + IReadOnlyList checkUpdateResults = await installer.GetLatestVersionAsync(new[] { source! }, provider, CancellationToken.None); + + Assert.Single(checkUpdateResults); + CheckUpdateResult result = checkUpdateResults.Single(); + + Assert.False(result.Success); + result.ErrorMessage.Should().NotBeNullOrEmpty(); + } + + [Theory] + [InlineData(nameof(PackageNotFoundException), InstallerErrorCode.PackageNotFound)] + [InlineData(nameof(InvalidNuGetSourceException), InstallerErrorCode.InvalidSource)] + [InlineData(nameof(VulnerablePackageException), InstallerErrorCode.VulnerablePackage, "12.0.0")] + [InlineData(nameof(Exception), InstallerErrorCode.GenericError)] + public async Task GetLatestVersion_RemotePackage_HandleExceptions(string exception, InstallerErrorCode expectedErrorCode, string version = "1.0.0") + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + NuGetManagedTemplatePackage source = new NuGetManagedTemplatePackage(engineEnvironmentSettings, installer, provider, installPath, exception) + { + Version = version + }; + IReadOnlyList checkUpdateResults = await installer.GetLatestVersionAsync(new[] { source }, provider, CancellationToken.None); + + Assert.Single(checkUpdateResults); + CheckUpdateResult result = checkUpdateResults.Single(); + + Assert.False(result.Success); + Assert.Equal(source, result.TemplatePackage); + Assert.Equal(expectedErrorCode, result.Error); + result.ErrorMessage.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task Uninstall_RemotePackage() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(_packageManager, TestPackageProjectPath); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + InstallRequest request = new InstallRequest("Microsoft.TemplateEngine.TestTemplates", "1.0.0"); + + InstallResult installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + + Assert.True(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + + var source = installResult.TemplatePackage as NuGetManagedTemplatePackage; + Assert.NotNull(source); + string mountPoint = source!.MountPointUri; + Assert.True(File.Exists(mountPoint)); + + UninstallResult result = await installer.UninstallAsync(source, provider, CancellationToken.None); + + Assert.True(result.Success); + Assert.Equal(source, result.TemplatePackage); + Assert.Equal(InstallerErrorCode.Success, result.Error); + result.ErrorMessage.Should().BeNullOrEmpty(); + Assert.False(File.Exists(mountPoint)); + } + + [Fact] + public async Task Update_RemotePackage() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(_packageManager, TestPackageProjectPath); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + InstallRequest request = new InstallRequest("Microsoft.TemplateEngine.TestTemplates", "1.0.0"); + + InstallResult installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + + Assert.True(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + Assert.Equal(InstallerErrorCode.Success, installResult.Error); + installResult.ErrorMessage.Should().BeNullOrEmpty(); + + var source = installResult.TemplatePackage as NuGetManagedTemplatePackage; + Assert.NotNull(source); + string oldMountPoint = source!.MountPointUri; + Assert.True(File.Exists(oldMountPoint)); + UpdateRequest updateRequest = new UpdateRequest(source, "1.0.1"); + + UpdateResult updateResult = await installer.UpdateAsync(updateRequest, provider, CancellationToken.None); + Assert.True(updateResult.Success); + Assert.Equal(updateRequest, updateResult.UpdateRequest); + Assert.Equal(InstallerErrorCode.Success, updateResult.Error); + updateResult.ErrorMessage.Should().BeNullOrEmpty(); + + var updatedSource = updateResult.TemplatePackage as NuGetManagedTemplatePackage; + Assert.NotNull(updatedSource); + updatedSource!.MountPointUri.Should().ContainAll(new[] { installPath, "Microsoft.TemplateEngine.TestTemplates" }); + updatedSource.Author.Should().Be("Microsoft"); + updatedSource.Version.Should().Be("1.0.1"); + updatedSource.DisplayName.Should().Be("Microsoft.TemplateEngine.TestTemplates@1.0.1"); + updatedSource.Identifier.Should().Be("Microsoft.TemplateEngine.TestTemplates"); + updatedSource.Installer.Should().Be(installer); + updatedSource.IsLocalPackage.Should().Be(false); + updatedSource.Provider.Should().Be(provider); + updatedSource.NuGetSource.Should().Be(MockPackageManager.DefaultFeed); + Assert.False(File.Exists(oldMountPoint)); + Assert.True(File.Exists(updatedSource.MountPointUri)); + } + + [Fact] + internal async Task Update_CannotUpdateVulnerabilities() + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(_packageManager, TestPackageProjectPath); + + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + InstallRequest request = new InstallRequest(nameof(VulnerablePackageException), "2.0.10"); + + InstallResult installResult = await installer.InstallAsync(request, provider, CancellationToken.None); + + Assert.True(installResult.Success); + Assert.Equal(request, installResult.InstallRequest); + Assert.Equal(InstallerErrorCode.Success, installResult.Error); + installResult.ErrorMessage.Should().BeNullOrEmpty(); + + var source = installResult.TemplatePackage as NuGetManagedTemplatePackage; + Assert.NotNull(source); + string oldMountPoint = source!.MountPointUri; + Assert.True(File.Exists(oldMountPoint)); + UpdateRequest updateRequest = new UpdateRequest(source, "12.0.3"); + + UpdateResult updateResult = await installer.UpdateAsync(updateRequest, provider, CancellationToken.None); + Assert.False(updateResult.Success); + Assert.Equal(InstallerErrorCode.VulnerablePackage, updateResult.Error); + updateResult.ErrorMessage.Should().NotBeNullOrEmpty(); + updateResult.Vulnerabilities.Should().NotBeNullOrEmpty(); + } + + [Theory] + [MemberData(nameof(SerializationData))] + public void Deserialize( + TemplatePackageData data, + string identifier, + string version, + string? author, + string nugetFeed, + bool local, + string reserved, + string owners) + { + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + + NuGetManagedTemplatePackage source = (NuGetManagedTemplatePackage)installer.Deserialize(provider, data); + source.MountPointUri.Should().Be(data.MountPointUri); + source.Author.Should().Be(author); + source.Reserved.Should().Be(reserved); + source.Owners.Should().Be(owners); + source.Version.Should().Be(version); + source.DisplayName.Should().Be($"{identifier}@{version}"); + source.Identifier.Should().Be(identifier); + source.Installer.Should().Be(installer); + source.IsLocalPackage.Should().Be(local); + source.Provider.Should().Be(provider); + source.NuGetSource.Should().Be(nugetFeed); + } + + [Fact] + public void Deserialize_ThrowsWhenDetailsDoNotHavePackageID() + { + var templateData = new TemplatePackageData( + default, + "MountPointUri", + default, + new Dictionary + { + { "Irrelevant", "not needed" }, + { "Version", "4.7.0.395" } + }); + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + + Assert.Throws(() => installer.Deserialize(provider, templateData)); + } + + [Fact] + public void Deserialize_ThrowsWhenInstallerIdDoesNotMatch() + { + var templateData = new TemplatePackageData( + Guid.NewGuid(), + "MountPointUri", + default, + new Dictionary + { + { "Irrelevant", "not needed" }, + { "Version", "4.7.0.395" } + }); + MockInstallerFactory factory = new MockInstallerFactory(); + MockManagedTemplatePackageProvider provider = new MockManagedTemplatePackageProvider(); + string installPath = _environmentSettingsHelper.CreateTemporaryFolder(); + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + MockPackageManager mockPackageManager = new MockPackageManager(); + NuGetInstaller installer = new NuGetInstaller(factory, engineEnvironmentSettings, installPath, mockPackageManager, mockPackageManager); + Assert.Throws(() => installer.Deserialize(provider, templateData)); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGetVersionHelperTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGetVersionHelperTests.cs new file mode 100644 index 000000000000..96a6ed01dd3a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGetVersionHelperTests.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Edge.Installers.NuGet; +using NuGet.Versioning; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class NuGetVersionHelperTests + { + [Theory] + [InlineData(null, true)] + [InlineData("", true)] + [InlineData("*", true)] + [InlineData("1.*", true)] + [InlineData("55.66.77.*", true)] + [InlineData("55.66.77*", true)] + [InlineData("123.456.789.012", true)] + [InlineData("1.2", true)] + [InlineData("1.*.1", false)] + [InlineData("1.*.*", false)] + [InlineData("*.1", false)] + [InlineData("a.b", false)] + [InlineData("a.b.*", false)] + public void IsSupportedVersionStringTest(string? versionString, bool isSupported) + { + Assert.Equal(isSupported, NuGetVersionHelper.IsSupportedVersionString(versionString)); + } + + [Theory] + [InlineData(null, true)] + [InlineData("", true)] + [InlineData("*", true)] + [InlineData("1.*", true)] + [InlineData("55.66.77.*", true)] + [InlineData("55.66.77*", true)] + [InlineData("123.456.789.012", false)] + [InlineData("1.2", false)] + public void TryParseFloatRangeReturnsExpectedBoolFlag(string? versionString, bool isFloatingVersion) + { + Assert.Equal(isFloatingVersion, NuGetVersionHelper.TryParseFloatRangeEx(versionString, out _)); + } + + [Theory] + [InlineData("1.2.3.4", null, true)] + [InlineData("1.2.3.4", "", true)] + [InlineData("1.2.3.4", "1.2.*", true)] + [InlineData("1.2.3.4", "2.2*", false)] + [InlineData("1.2.3.4", "1.2.*-*", true)] + public void TryParseFloatRangeMatchingTest(string versionString, string? pattern, bool isMatch) + { + NuGetVersion version = new NuGetVersion(versionString); + Assert.True(NuGetVersionHelper.TryParseFloatRangeEx(pattern, out FloatRange floatRange)); + Assert.Equal(isMatch, floatRange.Satisfies(version)); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGet_testFeed.config b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGet_testFeed.config new file mode 100644 index 000000000000..a3f3aea61c8f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/NuGet_testFeed.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/OSConstraintTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/OSConstraintTests.cs new file mode 100644 index 000000000000..543bc83fe051 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/OSConstraintTests.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class OSConstraintTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _sharedSettings; + + public OSConstraintTests(EnvironmentSettingsHelper helper) + { + _sharedSettings = helper.CreateEnvironment(); + } + + [Fact] + public async Task CanReadStringConfiguration() + { + var config = new + { + identity = "test", + constraints = new + { + winOnly = new + { + type = "os", + args = "Windows" + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + var constraintManager = new TemplateConstraintManager(_sharedSettings); + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + + Assert.Equal(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), evaluateResult.EvaluationStatus == TemplateConstraintResult.Status.Allowed); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Equal($"Running template on {RuntimeInformation.OSDescription} is not supported, supported OS is/are: {OSPlatform.Windows}.", evaluateResult.LocalizedErrorMessage); + } + else + { + Assert.Null(evaluateResult.LocalizedErrorMessage); + } + Assert.Null(evaluateResult.CallToAction); + } + + [Fact] + public async Task CanReadArrayConfiguration() + { + var config = new + { + identity = "test", + constraints = new + { + winOnly = new + { + type = "os", + args = new[] { "Windows", "Linux" } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + var constraintManager = new TemplateConstraintManager(_sharedSettings); + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + + var pass = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + Assert.Equal(pass, evaluateResult.EvaluationStatus == TemplateConstraintResult.Status.Allowed); + + if (!pass) + { + Assert.Equal($"Running template on {RuntimeInformation.OSDescription} is not supported, supported OS is/are: {OSPlatform.Windows}, {OSPlatform.Linux}.", evaluateResult.LocalizedErrorMessage); + } + else + { + Assert.Null(evaluateResult.LocalizedErrorMessage); + } + Assert.Null(evaluateResult.CallToAction); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/PathInfoTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/PathInfoTests.cs new file mode 100644 index 000000000000..9072b3a94ca5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/PathInfoTests.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using FakeItEasy; +using Microsoft.TemplateEngine.Abstractions; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class PathInfoTests + { + [Fact] + public void DefaultLocationTest() + { + var environment = A.Fake(); + A.CallTo(() => environment.GetEnvironmentVariable("HOME")).Returns("/home/path"); + A.CallTo(() => environment.GetEnvironmentVariable("USERPROFILE")).Returns("C:\\users\\user"); + + var host = A.Fake(); + A.CallTo(() => host.HostIdentifier).Returns("hostID"); + A.CallTo(() => host.Version).Returns("1.0.0"); + + var pathInfo = new DefaultPathInfo(environment, host); + + var homeFolder = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "C:\\users\\user" : "/home/path"; + + Assert.Equal(homeFolder, pathInfo.UserProfileDir); + Assert.Equal(Path.Combine(homeFolder, ".templateengine"), pathInfo.GlobalSettingsDir); + Assert.Equal(Path.Combine(homeFolder, ".templateengine", "hostID"), pathInfo.HostSettingsDir); + Assert.Equal(Path.Combine(homeFolder, ".templateengine", "hostID", "1.0.0"), pathInfo.HostVersionSettingsDir); + } + + [Fact] + public void DefaultLocationTest_ExpectedExceptions() + { + var environment = A.Fake(); + A.CallTo(() => environment.GetEnvironmentVariable("HOME")).Returns("/home/path"); + A.CallTo(() => environment.GetEnvironmentVariable("USERPROFILE")).Returns("C:\\users\\user"); + + var host = A.Fake(); + A.CallTo(() => host.HostIdentifier).Returns("hostID"); + A.CallTo(() => host.Version).Returns(string.Empty); + Assert.Throws(() => new DefaultPathInfo(environment, host)); + + A.CallTo(() => host.HostIdentifier).Returns(string.Empty); + A.CallTo(() => host.Version).Returns("ver"); + Assert.Throws(() => new DefaultPathInfo(environment, host)); + } + + [Theory] + [InlineData("global", "host", "version")] + [InlineData(null, "host", "version")] + [InlineData("global", null, "version")] + [InlineData("global", "host", null)] + public void CustomLocationTest(string? global, string? hostDir, string? hostVersion) + { + var environment = A.Fake(); + A.CallTo(() => environment.GetEnvironmentVariable("HOME")).Returns("/home/path"); + A.CallTo(() => environment.GetEnvironmentVariable("USERPROFILE")).Returns("C:\\users\\user"); + + var host = A.Fake(); + A.CallTo(() => host.HostIdentifier).Returns("hostID"); + A.CallTo(() => host.Version).Returns("1.0.0"); + + var pathInfo = new DefaultPathInfo(environment, host, global, hostDir, hostVersion); + + var homeFolder = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "C:\\users\\user" : "/home/path"; + var defaultGlobal = Path.Combine(homeFolder, ".templateengine"); + var defaultHost = Path.Combine(homeFolder, ".templateengine", "hostID"); + var defaultHostVesrion = Path.Combine(homeFolder, ".templateengine", "hostID", "1.0.0"); + + Assert.Equal(homeFolder, pathInfo.UserProfileDir); + Assert.Equal(string.IsNullOrWhiteSpace(global) ? defaultGlobal : global, pathInfo.GlobalSettingsDir); + Assert.Equal(string.IsNullOrWhiteSpace(hostDir) ? defaultHost : hostDir, pathInfo.HostSettingsDir); + Assert.Equal(string.IsNullOrWhiteSpace(hostVersion) ? defaultHostVesrion : hostVersion, pathInfo.HostVersionSettingsDir); + } + + [Theory] + [InlineData("custom")] + [InlineData(null)] + public void CustomHiveLocationTest(string? hiveLocation) + { + var environment = A.Fake(); + A.CallTo(() => environment.GetEnvironmentVariable("HOME")).Returns("/home/path"); + A.CallTo(() => environment.GetEnvironmentVariable("USERPROFILE")).Returns("C:\\users\\user"); + + var host = A.Fake(); + A.CallTo(() => host.HostIdentifier).Returns("hostID"); + A.CallTo(() => host.Version).Returns("1.0.0"); + + var envSettings = A.Fake(); + A.CallTo(() => envSettings.Host).Returns(host); + A.CallTo(() => envSettings.Environment).Returns(environment); + + var pathInfo = new DefaultPathInfo(envSettings, hiveLocation); + + var homeFolder = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "C:\\users\\user" : "/home/path"; + var expectedGlobal = string.IsNullOrWhiteSpace(hiveLocation) + ? Path.Combine(homeFolder, ".templateengine") + : Path.Combine(hiveLocation); + var expectedHost = string.IsNullOrWhiteSpace(hiveLocation) + ? Path.Combine(homeFolder, ".templateengine", "hostID") + : Path.Combine(hiveLocation, "hostID"); + var expectedHostVesrion = string.IsNullOrWhiteSpace(hiveLocation) + ? Path.Combine(homeFolder, ".templateengine", "hostID", "1.0.0") + : Path.Combine(hiveLocation, "hostID", "1.0.0"); + + Assert.Equal(homeFolder, pathInfo.UserProfileDir); + Assert.Equal(expectedGlobal, pathInfo.GlobalSettingsDir); + Assert.Equal(expectedHost, pathInfo.HostSettingsDir); + Assert.Equal(expectedHostVesrion, pathInfo.HostVersionSettingsDir); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/ScannerTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/ScannerTests.cs new file mode 100644 index 000000000000..e891fef137bb --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/ScannerTests.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class ScannerTests : TestBase, IClassFixture + { + private readonly EnvironmentSettingsHelper _settingsHelper; + + public ScannerTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _settingsHelper = environmentSettingsHelper; + } + + [Fact] + public async Task CanLogValidationMessagesOnInstall_MissingIdentity() + { + string templatesLocation = Path.Combine(TestTemplatesLocation, "Invalid", "MissingIdentity"); + + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + IEngineEnvironmentSettings settings = _settingsHelper.CreateEnvironment(virtualize: true, addLoggerProviders: new[] { loggerProvider }); + + Scanner scanner = new(settings); + + ScanResult result = await scanner.ScanAsync(templatesLocation, default); + + Assert.Empty(result.Templates); +#pragma warning disable CS0618 // Type or member is obsolete + Assert.Empty(result.Localizations); +#pragma warning restore CS0618 // Type or member is obsolete + + string errorMessage = Assert.Single(loggedMessages, l => l.Level is LogLevel.Error).Message; + Assert.Equal($"Failed to load template from {Path.GetFullPath(templatesLocation) + Path.DirectorySeparatorChar}.template.config/template.json.{Environment.NewLine}Details: 'identity' is missing or is an empty string.", errorMessage); + } + + [Fact] + public async Task CanLogValidationMessagesOnInstall_ErrorsInTemplateConfig() + { + string templatesLocation = Path.Combine(TestTemplatesLocation, "Invalid", "MissingMandatoryConfig"); + + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + IEngineEnvironmentSettings settings = _settingsHelper.CreateEnvironment(virtualize: true, addLoggerProviders: new[] { loggerProvider }); + + Scanner scanner = new(settings); + + ScanResult result = await scanner.ScanAsync(templatesLocation, default); + + Assert.Empty(result.Templates); +#pragma warning disable CS0618 // Type or member is obsolete + Assert.Empty(result.Localizations); +#pragma warning restore CS0618 // Type or member is obsolete + + List errorMessages = loggedMessages.Where(lm => lm.Level == LogLevel.Error).Select(e => e.Message).ToList(); + Assert.Equal(2, errorMessages.Count); + + List warningMessages = loggedMessages.Where(lm => lm.Level == LogLevel.Warning).Select(e => e.Message).ToList(); + Assert.Empty(warningMessages); + + List debugMessages = loggedMessages.Where(lm => lm.Level == LogLevel.Debug).Select(e => e.Message).ToList(); + Assert.Equal(4, debugMessages.Count); + + Assert.Equal( + """ + The template '' (MissingConfigTest) has the following validation errors: + [Error][MV002] Missing 'name'. + [Error][MV003] Missing 'shortName'. + + """, + errorMessages[0]); + Assert.Equal("Failed to load the template '' (MissingConfigTest): the template is not valid.", errorMessages[1]); + + Assert.Contains( + """ + The template '' (MissingConfigTest) has the following validation messages: + [Info][MV005] Missing 'sourceName'. + [Info][MV006] Missing 'author'. + [Info][MV007] Missing 'groupIdentity'. + [Info][MV008] Missing 'generatorVersions'. + [Info][MV009] Missing 'precedence'. + [Info][MV010] Missing 'classifications'. + + """, + debugMessages); + } + + [Fact] + public async Task CanLogValidationMessagesOnInstall_Localization() + { + string templatesLocation = Path.Combine(TestTemplatesLocation, "Invalid", "Localization", "ValidationFailure"); + + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + IEngineEnvironmentSettings settings = _settingsHelper.CreateEnvironment(virtualize: true, addLoggerProviders: new[] { loggerProvider }); + + Scanner scanner = new(settings); + + ScanResult result = await scanner.ScanAsync(templatesLocation, default); + + Assert.Single(result.Templates); +#pragma warning disable CS0618 // Type or member is obsolete + Assert.Empty(result.Localizations); +#pragma warning restore CS0618 // Type or member is obsolete + + List errorMessages = loggedMessages.Where(lm => lm.Level == LogLevel.Error).Select(e => e.Message).OrderBy(em => em).ToList(); + + Assert.Equal(2, errorMessages.Count); + + List warningMessages = loggedMessages.Where(lm => lm.Level == LogLevel.Warning).Select(e => e.Message).OrderBy(em => em).ToList(); + + Assert.Equal(3, warningMessages.Count); + + Assert.Equal( + """ + The template 'name' (TestAssets.Invalid.Localization.ValidationFailure) has the following validation errors in 'de-DE' localization: + [Error][LOC001] In localization file under the post action with id 'pa1', there are localized strings for manual instruction(s) with ids 'do-not-exist'. These manual instructions do not exist in the template.json file and should be removed from localization file. + [Error][LOC002] Post action(s) with id(s) 'pa0' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + + """, + errorMessages[0]); + Assert.Equal( + """ + The template 'name' (TestAssets.Invalid.Localization.ValidationFailure) has the following validation errors in 'tr' localization: + [Error][LOC002] Post action(s) with id(s) 'pa6' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + + """, + errorMessages[1]); + + Assert.Equal("Failed to load the 'de-DE' localization the template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the localization file is not valid. The localization will be skipped.", warningMessages[0]); + Assert.Equal("Failed to load the 'tr' localization the template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the localization file is not valid. The localization will be skipped.", warningMessages[1]); + + Assert.Equal( + """ + The template 'name' (TestAssets.Invalid.Localization.ValidationFailure) has the following validation warnings: + [Warning][CONFIG0201] Id of the post action 'pa2' at index '3' is not unique. Only the first post action that uses this id will be localized. + + """, + warningMessages[2]); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/SdkVersionConstraintTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/SdkVersionConstraintTests.cs new file mode 100644 index 000000000000..4434f79d8ee6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/SdkVersionConstraintTests.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using FakeItEasy; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Components; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Edge.Constraints; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class SdkVersionConstraintTests + { + [Theory] + [InlineData("1.2.3", true)] + [InlineData("1.2.3-dev", true)] + [InlineData("1.2.4", false)] + [InlineData("4.5.3-dev", false)] + [InlineData("4.5.3", true)] + [InlineData("4.5.0", true)] + [InlineData("4.6.0", false)] + public async Task Evaluate_ArrayOfVersions(string sdkVersion, bool allowed) + { + var config = new + { + identity = "test-constraint-01", + constraints = new + { + specVersions = new + { + type = "sdk-version", + args = new[] { "1.2.3-*", "4.5.*" } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + ISdkInfoProvider sdkInfoProvider = new SdkInfoProviderMock(sdkVersion); //A.Fake(); + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { sdkInfoProvider }); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new SdkVersionConstraintFactory() }); + + var constraintManager = new TemplateConstraintManager(settings); + + //Workaround needed + //A.CallTo(() => sdkInfoProvider.GetVersionAsync(A._)).Returns(Task.Run(() => sdkVersion)); + + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + Assert.Equal(allowed ? TemplateConstraintResult.Status.Allowed : TemplateConstraintResult.Status.Restricted, evaluateResult.EvaluationStatus); + } + + [Theory] + [InlineData("1.2.2", false)] + [InlineData("1.2.3", true)] + [InlineData("1.2.3-dev", true)] + [InlineData("1.2.4", true)] + [InlineData("4.5.3-dev", false)] + [InlineData("4.5.3", false)] + [InlineData("4.5.0", true)] + [InlineData("4.4.0-dev", true)] + public async Task Evaluate_SingleVersionRange(string sdkVersion, bool allowed) + { + var config = new + { + identity = "test-constraint-01", + constraints = new + { + specVersions = new + { + type = "sdk-version", + args = "(1.2.3-*, 4.5]" + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + ISdkInfoProvider sdkInfoProvider = new SdkInfoProviderMock(sdkVersion); //A.Fake(); + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { sdkInfoProvider }); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new SdkVersionConstraintFactory() }); + + var constraintManager = new TemplateConstraintManager(settings); + + //Workaround needed + //A.CallTo(() => sdkInfoProvider.GetVersionAsync(A._)).Returns(t); + + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + Assert.Equal(allowed ? TemplateConstraintResult.Status.Allowed : TemplateConstraintResult.Status.Restricted, evaluateResult.EvaluationStatus); + } + + [Theory] + [InlineData("1.1.1", new[] { "0.1.2", "1.2.3", "3.4.5" }, true)] + [InlineData("1.1.1", new[] { "0.1.2", "1.2.2", "3.4.5" }, false)] + [InlineData("1.1.1", new[] { "0.1.2", "1.2.3", "4.5.6" }, true)] + public async Task Evaluate_AlternativeInstalledVersions(string sdkVersion, IReadOnlyList installedVersions, bool hasAlternativeInstalled) + { + var config = new + { + identity = "test-constraint-01", + constraints = new + { + specVersions = new + { + type = "sdk-version", + args = new[] { "1.2.3-*", "4.5.*" } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + ISdkInfoProvider sdkInfoProvider = new SdkInfoProviderMock(sdkVersion, installedVersions); //A.Fake(); + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { sdkInfoProvider }); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new SdkVersionConstraintFactory() }); + + var constraintManager = new TemplateConstraintManager(settings); + + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + Assert.Equal(TemplateConstraintResult.Status.Restricted, evaluateResult.EvaluationStatus); + Assert.StartsWith( + hasAlternativeInstalled + ? "Sample CTA with alternatives" + : "Sample CTA without alternatives", + evaluateResult.CallToAction); + } + + // This is a workaround in a weird bug with FakeItEasy, when using: + // ISdkInfoProvider sdkInfoProvider = A.Fake(); + // A.CallTo(() => sdkInfoProvider.GetVersionAsync(A._)).Returns(Task.FromResult(sdkVersion)); + // The task then randomly returns empty string in the target code + private class SdkInfoProviderMock : ISdkInfoProvider + { + private readonly string _res; + private readonly IEnumerable _installed; + + public SdkInfoProviderMock(string res, IEnumerable? installed = null) + { + _res = res; + _installed = installed ?? new[] { _res }; + } + + public Guid Id { get; } + + public Task GetCurrentVersionAsync(CancellationToken cancellationToken) => Task.FromResult(_res); + + public Task> GetInstalledVersionsAsync(CancellationToken cancellationToken) => Task.FromResult(_installed); + + public string ProvideConstraintRemedySuggestion( + IReadOnlyList supportedVersions, + IReadOnlyList viableInstalledVersions) => viableInstalledVersions.Any() + ? "Sample CTA with alternatives" + : "Sample CTA without alternatives"; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplateCacheTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplateCacheTests.cs new file mode 100644 index 000000000000..5701de9005bd --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplateCacheTests.cs @@ -0,0 +1,407 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using FakeItEasy; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class TemplateCacheTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public TemplateCacheTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Theory] + [InlineData("en-US", "en")] + [InlineData("zh-CN", "zh-Hans")] + [InlineData("zh-SG", "zh-Hans")] + [InlineData("zh-TW", "zh-Hant")] + [InlineData("zh-HK", "zh-Hant")] + [InlineData("zh-MO", "zh-Hant")] + [InlineData("pt-BR", "pt-BR")] + [InlineData("pt", null)] + [InlineData("uk-UA", null)] + [InlineData("invariant", null)] + public void PicksCorrectLocator(string currentCulture, string? expectedLocator) + { + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + CultureInfo persistedCulture = CultureInfo.CurrentUICulture; + try + { + if (currentCulture != "invariant") + { + CultureInfo.CurrentUICulture = new CultureInfo(currentCulture); + } + else + { + currentCulture = string.Empty; + CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture; + } + string[] availableLocales = new[] { "cs", "de", "en", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-Hans", "zh-Hant" }; + + IScanTemplateInfo template = A.Fake(); + A.CallTo(() => template.Identity).Returns("testIdentity"); + List locators = new List(); + foreach (string locale in availableLocales) + { + ILocalizationLocator locator = A.Fake(); +#pragma warning disable CS0618 // Type or member is obsolete + A.CallTo(() => locator.Identity).Returns("testIdentity"); +#pragma warning restore CS0618 // Type or member is obsolete + A.CallTo(() => locator.Locale).Returns(locale); + A.CallTo(() => locator.Name).Returns(locale + " name"); + locators.Add(locator); + } + A.CallTo(() => template.Localizations).Returns(locators.ToDictionary(l => l.Locale, l => l)); + IMountPoint mountPoint = A.Fake(); + A.CallTo(() => mountPoint.MountPointUri).Returns("testMount"); + + ScanResult result = new ScanResult(mountPoint, new[] { template }, locators, []); + + TemplateCache templateCache = new TemplateCache([], new[] { result }, new Dictionary(), environmentSettings); + + Assert.Equal(currentCulture, templateCache.Locale); + Assert.Equal("testIdentity", templateCache.TemplateInfo.Single().Identity); + Assert.Equal(string.IsNullOrEmpty(expectedLocator) ? string.Empty : expectedLocator + " name", templateCache.TemplateInfo.Single().Name); + } + finally + { + CultureInfo.CurrentUICulture = persistedCulture; + } + } + + [Fact] + public void CanHandlePostActions() + { + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + SettingsFilePaths paths = new SettingsFilePaths(environmentSettings); + + Guid postAction1 = Guid.NewGuid(); + Guid postAction2 = Guid.NewGuid(); + + var template = GetFakedTemplate("testIdentity", "testMount", "testName"); + A.CallTo(() => template.PostActions).Returns(new[] { postAction1, postAction2 }); + IMountPoint mountPoint = A.Fake(); + A.CallTo(() => mountPoint.MountPointUri).Returns("testMount"); + + ScanResult result = new ScanResult(mountPoint, new[] { template }, [], []); + TemplateCache templateCache = new TemplateCache([], new[] { result }, new Dictionary(), environmentSettings); + + WriteObject(environmentSettings.Host.FileSystem, paths.TemplateCacheFile, templateCache); + var readCache = new TemplateCache(ReadObject(environmentSettings.Host.FileSystem, paths.TemplateCacheFile)); + + Assert.Single(readCache.TemplateInfo); + var readTemplate = readCache.TemplateInfo[0]; + Assert.Equal(new[] { postAction1, postAction2 }, readTemplate.PostActions); + } + + [Fact] + public void CanHandleConstraints() + { + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + SettingsFilePaths paths = new SettingsFilePaths(environmentSettings); + + TemplateConstraintInfo constraintInfo1 = new TemplateConstraintInfo("t1", null); + TemplateConstraintInfo constraintInfo2 = new TemplateConstraintInfo("t1", "{[ \"one\", \"two\"]}"); + + var template = GetFakedTemplate("testIdentity", "testMount", "testName"); + A.CallTo(() => template.Constraints).Returns(new[] { constraintInfo1, constraintInfo2 }); + IMountPoint mountPoint = A.Fake(); + A.CallTo(() => mountPoint.MountPointUri).Returns("testMount"); + + ScanResult result = new ScanResult(mountPoint, new[] { template }, [], []); + TemplateCache templateCache = new TemplateCache([], new[] { result }, new Dictionary(), environmentSettings); + + WriteObject(environmentSettings.Host.FileSystem, paths.TemplateCacheFile, templateCache); + var readCache = new TemplateCache(ReadObject(environmentSettings.Host.FileSystem, paths.TemplateCacheFile)); + + Assert.Single(readCache.TemplateInfo); + var readTemplate = readCache.TemplateInfo[0]; + Assert.Equal(2, readTemplate.Constraints.Count); + Assert.Equal("t1", readTemplate.Constraints[0].Type); + Assert.Equal("t1", readTemplate.Constraints[1].Type); + Assert.Null(readTemplate.Constraints[0].Args); + Assert.Equal(constraintInfo2.Args, readTemplate.Constraints[1].Args); + } + + [Fact] + public void CanHandleParameters() + { + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + SettingsFilePaths paths = new SettingsFilePaths(environmentSettings); + + ITemplateParameter param1 = new TemplateParameter("param1", "parameter", "string"); + ITemplateParameter param2 = new TemplateParameter("param2", "parameter", "string", new TemplateParameterPrecedence(PrecedenceDefinition.ConditionalyRequired, isRequiredCondition: "param1 == \"foo\"")); + ITemplateParameter param3 = new TemplateParameter( + "param3", + "parameter", + "choice", + new TemplateParameterPrecedence(PrecedenceDefinition.Required), + defaultValue: "def", + defaultIfOptionWithoutValue: "def-no-value", + description: "desc", + displayName: "displ", + allowMultipleValues: true, + choices: new Dictionary() + { + { "ch1", new ParameterChoice("ch1-displ", "ch1-desc") }, + { "ch2", new ParameterChoice("ch2-displ", "ch2-desc") }, + }); + + var template = GetFakedTemplate("testIdentity", "testMount", "testName"); + A.CallTo(() => template.ParameterDefinitions).Returns(new ParameterDefinitionSet(new[] { param1, param2, param3 })); + IMountPoint mountPoint = A.Fake(); + A.CallTo(() => mountPoint.MountPointUri).Returns("testMount"); + + ScanResult result = new ScanResult(mountPoint, new[] { template }, [], []); + TemplateCache templateCache = new TemplateCache([], new[] { result }, new Dictionary(), environmentSettings); + + WriteObject(environmentSettings.Host.FileSystem, paths.TemplateCacheFile, templateCache); + var readCache = new TemplateCache(ReadObject(environmentSettings.Host.FileSystem, paths.TemplateCacheFile)); + + Assert.Single(readCache.TemplateInfo); + var readTemplate = readCache.TemplateInfo[0]; + Assert.Equal(3, readTemplate.ParameterDefinitions.Count); + Assert.True(readTemplate.ParameterDefinitions.ContainsKey("param1")); + Assert.Equal(PrecedenceDefinition.Optional, readTemplate.ParameterDefinitions["param1"].Precedence.PrecedenceDefinition); + Assert.True(readTemplate.ParameterDefinitions.ContainsKey("param2")); + Assert.Equal("string", readTemplate.ParameterDefinitions["param2"].DataType); + Assert.Equal("param1 == \"foo\"", readTemplate.ParameterDefinitions["param2"].Precedence.IsRequiredCondition); + Assert.True(readTemplate.ParameterDefinitions.ContainsKey("param3")); + Assert.Equal("choice", readTemplate.ParameterDefinitions["param3"].DataType); + Assert.Equal(PrecedenceDefinition.Required, readTemplate.ParameterDefinitions["param3"].Precedence.PrecedenceDefinition); + Assert.Equal("def", readTemplate.ParameterDefinitions["param3"].DefaultValue); + Assert.Equal("def-no-value", readTemplate.ParameterDefinitions["param3"].DefaultIfOptionWithoutValue); + Assert.Equal("desc", readTemplate.ParameterDefinitions["param3"].Description); + Assert.Equal("displ", readTemplate.ParameterDefinitions["param3"].DisplayName); + Assert.True(readTemplate.ParameterDefinitions["param3"].AllowMultipleValues); + Assert.Equal(2, readTemplate.ParameterDefinitions["param3"].Choices!.Count); + } + + [Theory] + [InlineData(true, "defaultName")] + [InlineData(true, null)] + [InlineData(false, "anotherDefault")] + public void CanHandleDefaultName(bool preferDefaultName, string? defaultName) + { + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + SettingsFilePaths paths = new SettingsFilePaths(environmentSettings); + + var template = GetFakedTemplate("testIdentity", "testMount", "testName"); + A.CallTo(() => template.PreferDefaultName).Returns(preferDefaultName); + A.CallTo(() => template.DefaultName).Returns(defaultName); + IMountPoint mountPoint = A.Fake(); + A.CallTo(() => mountPoint.MountPointUri).Returns("testMount"); + + ScanResult result = new ScanResult(mountPoint, new[] { template }, [], []); + TemplateCache templateCache = new TemplateCache([], new[] { result }, new Dictionary(), environmentSettings); + + WriteObject(environmentSettings.Host.FileSystem, paths.TemplateCacheFile, templateCache); + var readCache = new TemplateCache(ReadObject(environmentSettings.Host.FileSystem, paths.TemplateCacheFile)); + + Assert.Single(readCache.TemplateInfo); + var readTemplate = readCache.TemplateInfo[0]; + Assert.Equal(preferDefaultName, readTemplate.PreferDefaultName); + Assert.Equal(defaultName, readTemplate.DefaultName); + } + + [Fact] + public void ProducesCorrectWarningOnOverlappingIdentity_ManagedCandidatesOnly() + { + List<(LogLevel, string)> loggedMessages = new List<(LogLevel, string)>(); + InMemoryLoggerProvider loggerProvider = new InMemoryLoggerProvider(loggedMessages); + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true, addLoggerProviders: new[] { loggerProvider }); + var overlappingIdentity = "testIdentity"; + + var templateA = GetFakedTemplate(overlappingIdentity, "testMountA", "TemplateA"); + var managedTPA = GetFakedManagedTemplatePackage("testMountA", "PackageA"); + + var templateB = GetFakedTemplate(overlappingIdentity, "testMountB", "TemplateB"); + var managedTPB = GetFakedManagedTemplatePackage("testMountB", "PackageB"); + + var templateC = GetFakedTemplate(overlappingIdentity, "testMountC", "TemplateC"); + var managedTPC = GetFakedManagedTemplatePackage("testMountC", "PackageC"); + + var expectedOutput = "The following templates use the same identity 'testIdentity':" + + $"{Environment.NewLine} • 'TemplateA' from 'PackageA'" + + $"{Environment.NewLine} • 'TemplateB' from 'PackageB'" + + $"{Environment.NewLine} • 'TemplateC' from 'PackageC'" + + $"{Environment.NewLine}The template from 'TemplateC' will be used. To resolve this conflict, uninstall the conflicting template packages."; + + ScanResult result = new ScanResult(A.Fake(), new[] { templateA, templateB, templateC }, [], []); + _ = new TemplateCache(new[] { managedTPA, managedTPB, managedTPC }, new[] { result }, new Dictionary(), environmentSettings); + + var warningMessages = loggedMessages.Where(log => log.Item1 == LogLevel.Warning); + Assert.Single(warningMessages); + Assert.Contains(expectedOutput, warningMessages.Single().Item2); + } + + [Fact] + public void ProducesCorrectOutputOnOverlappingIdentity_ManagedAndUnmanagedCandidates() + { + List<(LogLevel, string)> loggedMessages = new List<(LogLevel, string)>(); + InMemoryLoggerProvider loggerProvider = new InMemoryLoggerProvider(loggedMessages); + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true, addLoggerProviders: new[] { loggerProvider }); + var overlappingIdentity = "testIdentity"; + + var templateA = GetFakedTemplate(overlappingIdentity, "testMountA", "TemplateA"); + var managedTPA = GetFakedManagedTemplatePackage("testMountA", "PackageA"); + + var templateB = GetFakedTemplate(overlappingIdentity, "testMountB", "TemplateB"); + var managedTPB = GetFakedTemplatePackage("testMountB"); + + var templateC = GetFakedTemplate(overlappingIdentity, "testMountC", "TemplateC"); + var managedTPC = GetFakedManagedTemplatePackage("testMountC", "PackageC"); + + var expectedOutput = "The following templates use the same identity 'testIdentity':" + + $"{Environment.NewLine} • 'TemplateA' from 'PackageA'" + + $"{Environment.NewLine} • 'TemplateC' from 'PackageC'" + + $"{Environment.NewLine}The template from 'TemplateC' will be used. To resolve this conflict, uninstall the conflicting template packages."; + + ScanResult result = new ScanResult(A.Fake(), new[] { templateA, templateB, templateC }, [], []); + _ = new TemplateCache(new[] { managedTPA, managedTPB, managedTPC }, new[] { result }, new Dictionary(), environmentSettings); + + var warningMessages = loggedMessages.Where(log => log.Item1 == LogLevel.Warning); + Assert.Single(warningMessages); + Assert.Contains(expectedOutput, warningMessages.Single().Item2); + } + + [Fact] + public void NoOutputOnOverlappingIdentity_UnmanagedCandidateWins() + { + List<(LogLevel, string)> loggedMessages = new List<(LogLevel, string)>(); + InMemoryLoggerProvider loggerProvider = new InMemoryLoggerProvider(loggedMessages); + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true, addLoggerProviders: new[] { loggerProvider }); + var overlappingIdentity = "testIdentity"; + + var templateA = GetFakedTemplate(overlappingIdentity, "testMountA", "TemplateA"); + var managedTPA = GetFakedManagedTemplatePackage("testMountA", "PackageA"); + + var templateB = GetFakedTemplate(overlappingIdentity, "testMountB", "TemplateB"); + var managedTPB = GetFakedManagedTemplatePackage("testMountB", "PackageB"); + + var templateC = GetFakedTemplate(overlappingIdentity, "testMountC", "TemplateC"); + var managedTPC = GetFakedTemplatePackage("testMountC"); + + ScanResult result = new ScanResult(A.Fake(), new[] { templateA, templateB, templateC }, [], []); + _ = new TemplateCache(new[] { managedTPA, managedTPB, managedTPC }, new[] { result }, new Dictionary(), environmentSettings); + + var warningMessages = loggedMessages.Where(log => log.Item1 == LogLevel.Warning); + Assert.Empty(warningMessages); + } + + [Fact] + public void CanHandleHostData() + { + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + SettingsFilePaths paths = new SettingsFilePaths(environmentSettings); + + string hostfile = + /*lang=json,strict*/ + """ + { + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "useArtifacts": { + "longName": "use-artifacts", + "shortName": "" + }, + "inherit": { + "shortName": "" + } + } + } + """; + + string hostFileFormatted = JsonNode.Parse(hostfile)!.ToJsonString(); + const string hostFileLocation = ".template.config/dotnetcli.host.json"; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { hostFileLocation, hostfile } + }; + string sourceBasePath = environmentSettings.GetTempVirtualizedPath(); + TestFileSystemUtils.WriteTemplateSource(environmentSettings, sourceBasePath, templateSourceFiles); + + var template = GetFakedTemplate("testIdentity", sourceBasePath, "testName"); + A.CallTo(() => template.HostConfigFiles).Returns(new Dictionary() { { "dotnetcli", hostFileLocation } }); + using IMountPoint sourceMountPoint = environmentSettings.MountPath(sourceBasePath); + A.CallTo(() => template.GeneratorId).Returns(new("0C434DF7-E2CB-4DEE-B216-D7C58C8EB4B3")); // runnable projects generator ID + + ScanResult result = new ScanResult(sourceMountPoint, new[] { template }, [], []); + TemplateCache templateCache = new TemplateCache([], new[] { result }, new Dictionary(), environmentSettings); + + Assert.Equal(hostFileLocation, templateCache.TemplateInfo[0].HostConfigPlace); + Assert.Equal(hostFileFormatted, templateCache.TemplateInfo[0].HostData); + + WriteObject(environmentSettings.Host.FileSystem, paths.TemplateCacheFile, templateCache); + var readCache = new TemplateCache(ReadObject(environmentSettings.Host.FileSystem, paths.TemplateCacheFile)); + + Assert.Single(readCache.TemplateInfo); + var readTemplate = readCache.TemplateInfo[0]; + Assert.Equal(hostFileLocation, readTemplate.HostConfigPlace); + Assert.Equal(hostFileFormatted, readTemplate.HostData); + } + + private IScanTemplateInfo GetFakedTemplate(string identity, string mountPointUri, string name) + { + IScanTemplateInfo template = A.Fake(); + A.CallTo(() => template.Identity).Returns(identity); + A.CallTo(() => template.MountPointUri).Returns(mountPointUri); + A.CallTo(() => template.Name).Returns(name); + A.CallTo(() => template.ConfigPlace).Returns(".template.config/template.json"); + A.CallTo(() => template.ShortNameList).Returns(new[] { "testShort" }); + + return template; + } + + private IManagedTemplatePackage GetFakedManagedTemplatePackage(string mountPointUri, string displayName) + { + var managedTemplatePackage = A.Fake(); + A.CallTo(() => managedTemplatePackage.MountPointUri).Returns(mountPointUri); + A.CallTo(() => managedTemplatePackage.DisplayName).Returns(displayName); + + return managedTemplatePackage; + } + + private ITemplatePackage GetFakedTemplatePackage(string mountPointUri) + { + var managedTemplatePackage = A.Fake(); + A.CallTo(() => managedTemplatePackage.MountPointUri).Returns(mountPointUri); + + return managedTemplatePackage; + } + + private static JsonObject ReadObject(IPhysicalFileSystem fileSystem, string path) + { + using var fileStream = fileSystem.OpenRead(path); + using var textReader = new StreamReader(fileStream, System.Text.Encoding.UTF8, true); + string content = textReader.ReadToEnd(); + return JsonNode.Parse(content)!.AsObject(); + } + + private static void WriteObject(IPhysicalFileSystem fileSystem, string path, object obj) + { + using var fileStream = fileSystem.CreateFile(path); + JsonSerializer.Serialize(fileStream, obj); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplateConstraintManagerTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplateConstraintManagerTests.cs new file mode 100644 index 000000000000..3ba82f612d28 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplateConstraintManagerTests.cs @@ -0,0 +1,236 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FakeItEasy; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class TemplateConstraintManagerTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public TemplateConstraintManagerTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Fact] + public async Task CanEvaluateConstraint() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-1")); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-2")); + + var constraintManager = new TemplateConstraintManager(engineEnvironmentSettings); + + var success1 = await constraintManager.EvaluateConstraintAsync("test-1", "yes", default); + var failure1 = await constraintManager.EvaluateConstraintAsync("test-1", "no", default); + var notEvaluated1 = await constraintManager.EvaluateConstraintAsync("test-1", "not-valid", default); + var success2 = await constraintManager.EvaluateConstraintAsync("test-2", "yes", default); + + Assert.Equal(TemplateConstraintResult.Status.Allowed, success1.EvaluationStatus); + Assert.Null(success1.LocalizedErrorMessage); + Assert.Null(success1.CallToAction); + + Assert.Equal(TemplateConstraintResult.Status.Restricted, failure1.EvaluationStatus); + Assert.Equal("cannot run", failure1.LocalizedErrorMessage); + Assert.Equal("do smth", failure1.CallToAction); + + Assert.Equal(TemplateConstraintResult.Status.NotEvaluated, notEvaluated1.EvaluationStatus); + Assert.Equal("bad params", notEvaluated1.LocalizedErrorMessage); + Assert.Null(notEvaluated1.CallToAction); + + Assert.Equal(TemplateConstraintResult.Status.Allowed, success2.EvaluationStatus); + Assert.Null(success2.LocalizedErrorMessage); + Assert.Null(success2.CallToAction); + } + + [Fact] + public async Task CanGetConstraints() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-1")); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-2")); + + var constraintManager = new TemplateConstraintManager(engineEnvironmentSettings); + + var constraints = await constraintManager.GetConstraintsAsync(); + + Assert.Equal(4, constraints.Count); + Assert.Equal(new[] { "host", "os", "test-1", "test-2" }, constraints.Select(c => c.Type).OrderBy(t => t)); + } + + [Fact] + public async Task CanGetConstraints_Filter() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-1")); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-2")); + + var constraintManager = new TemplateConstraintManager(engineEnvironmentSettings); + + var templateInfo = A.Fake(); + A.CallTo(() => templateInfo.Constraints).Returns(new[] + { + new TemplateConstraintInfo("test-1", "yes") + }); + + var constraints = await constraintManager.GetConstraintsAsync(new[] { templateInfo }, default); + + Assert.Single(constraints); + Assert.Equal("test-1", constraints.Single().Type); + } + + [Fact] + public async Task CanGetConstraints_WhenCreationFailed() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new FailingTestConstraintFactory("test-1")); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-2")); + + var constraintManager = new TemplateConstraintManager(engineEnvironmentSettings); + var constraints = await constraintManager.GetConstraintsAsync(); + + Assert.Equal(3, constraints.Count); + Assert.Equal(new[] { "host", "os", "test-2" }, constraints.Select(c => c.Type).OrderBy(t => t)); + } + + [Fact] + public async Task CannotEvaluateConstraint_WhenCreationFailed() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new FailingTestConstraintFactory("test-1")); + + var constraintManager = new TemplateConstraintManager(engineEnvironmentSettings); + + var result = await constraintManager.EvaluateConstraintAsync("test-1", "yes", default); + Assert.Equal(TemplateConstraintResult.Status.NotEvaluated, result.EvaluationStatus); + Assert.Equal("The constraint 'test-1' failed to initialize: creation failed", result.LocalizedErrorMessage); + Assert.Null(result.CallToAction); + } + + [Fact] + public async Task CanEvaluateConstraint_WhenOtherCreationFailed() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new FailingTestConstraintFactory("test-1")); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-2")); + + var constraintManager = new TemplateConstraintManager(engineEnvironmentSettings); + + var result1 = await constraintManager.EvaluateConstraintAsync("test-1", "yes", default); + Assert.Equal(TemplateConstraintResult.Status.NotEvaluated, result1.EvaluationStatus); + Assert.Equal("The constraint 'test-1' failed to initialize: creation failed", result1.LocalizedErrorMessage); + Assert.Null(result1.CallToAction); + + var success2 = await constraintManager.EvaluateConstraintAsync("test-2", "yes", default); + + Assert.Equal(TemplateConstraintResult.Status.Allowed, success2.EvaluationStatus); + Assert.Null(success2.LocalizedErrorMessage); + Assert.Null(success2.CallToAction); + } + + [Fact] + public async Task CanEvaluateConstraint_WhenOtherCreationStillRuns() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new LongRunningTestConstraintFactory("test-1")); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-2")); + + var constraintManager = new TemplateConstraintManager(engineEnvironmentSettings); + + var success2 = await constraintManager.EvaluateConstraintAsync("test-2", "yes", default); + + Assert.Equal(TemplateConstraintResult.Status.Allowed, success2.EvaluationStatus); + Assert.Null(success2.LocalizedErrorMessage); + Assert.Null(success2.CallToAction); + } + + [Fact] + public async Task CanGetConstraints_DoesNotWaitForNotNeededConstraints() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new LongRunningTestConstraintFactory("test-1")); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-2")); + + var templateInfo = A.Fake(); + A.CallTo(() => templateInfo.Constraints).Returns(new[] + { + new TemplateConstraintInfo("test-2", "yes") + }); + + IReadOnlyList? constraints = null; + var constraintsTask = Task.Run(async () => + { + var constraintManager = new TemplateConstraintManager(engineEnvironmentSettings); + constraints = await constraintManager.GetConstraintsAsync(new[] { templateInfo }, default); + }); + var completedTask = await Task.WhenAny(constraintsTask, Task.Delay(10000)); + + Assert.Equal(completedTask, constraintsTask); + Assert.Equal(1, constraints?.Count); + Assert.Equal("test-2", constraints?.Single().Type); + } + + [Fact] + public async Task CanEvaluateConstraints_Success() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-1")); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateConstraintFactory), new TestConstraintFactory("test-2")); + + var constraintManager = new TemplateConstraintManager(engineEnvironmentSettings); + ITemplateInfo template = A.Fake(); + A.CallTo(() => template.Constraints).Returns(new[] { new TemplateConstraintInfo("test-1", "yes"), new TemplateConstraintInfo("test-2", "no") }); + + var result = await constraintManager.EvaluateConstraintsAsync(new[] { template }, default); + + Assert.Equal(2, result.Single().Result.Count); + Assert.Equal(template, result.Single().Template); + Assert.Equal(TemplateConstraintResult.Status.Allowed, result.Single().Result.Single(r => r.ConstraintType == "test-1").EvaluationStatus); + Assert.Equal(TemplateConstraintResult.Status.Restricted, result.Single().Result.Single(r => r.ConstraintType == "test-2").EvaluationStatus); + } + + private class FailingTestConstraintFactory : ITemplateConstraintFactory + { + public FailingTestConstraintFactory(string type) + { + Type = type; + Id = Guid.NewGuid(); + } + + public string Type { get; } + + public Guid Id { get; } + + public Task CreateTemplateConstraintAsync(IEngineEnvironmentSettings environmentSettings, CancellationToken cancellationToken) + { + throw new Exception("creation failed"); + } + } + + private class LongRunningTestConstraintFactory : ITemplateConstraintFactory + { + public LongRunningTestConstraintFactory(string type) + { + Type = type; + Id = Guid.NewGuid(); + } + + public string Type { get; } + + public Guid Id { get; } + + public async Task CreateTemplateConstraintAsync(IEngineEnvironmentSettings environmentSettings, CancellationToken cancellationToken) + { + await Task.Delay(30000); + throw new Exception("creation failed"); + } + } + + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplateCreatorTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplateCreatorTests.cs new file mode 100644 index 000000000000..c3ca50c69dca --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplateCreatorTests.cs @@ -0,0 +1,1024 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Edge.Template; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class TemplateCreatorTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public TemplateCreatorTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + _environmentSettingsHelper = environmentSettingsHelper; + } + + private const string TemplateConfigBooleanParam = /*lang=json,strict*/ """ + { + "identity": "test.template", + "name": "tst", + "shortName": "tst", + "symbols": { + "paramA": { + "type": "parameter", + "datatype": "bool" + } + } + } + """; + + private const string? XmlConditionWithinMsBuildConditionSource = """ + + + + + + + + + + + + """; + + private const string XmlConditionWithinMsBuildConditionOutputOnFalse = """ + + + """; + + private const string XmlConditionWithinMsBuildConditionOutputOnTrue = """ + + + + + + + """; + + private const string? MsBuildConditionWithinXmlConditionSource = """ + + + + + + + + + + + + """; + + private const string MsBuildConditionWithinXmlConditionOutputOnFalse = """ + + + + """; + + private const string MsBuildConditionWithinXmlConditionOutputOnTrue = """ + + + + + + + """; + + [Theory] + [InlineData(false, XmlConditionWithinMsBuildConditionSource, XmlConditionWithinMsBuildConditionOutputOnFalse)] + [InlineData(true, XmlConditionWithinMsBuildConditionSource, XmlConditionWithinMsBuildConditionOutputOnTrue)] + [InlineData(false, MsBuildConditionWithinXmlConditionSource, MsBuildConditionWithinXmlConditionOutputOnFalse)] + [InlineData(true, MsBuildConditionWithinXmlConditionSource, MsBuildConditionWithinXmlConditionOutputOnTrue)] + public async Task InstantiateAsync_XmlConditionsAndComments(bool paramA, string sourceSnippet, string expectedOutput) + { + IReadOnlyDictionary parameters = new Dictionary() + { + { "paramA", paramA.ToString() } + }; + + await InstantiateAsyncHelper( + TemplateConfigBooleanParam, + sourceSnippet, + expectedOutput, + string.Empty, + false, + sourceExtension: ".csproj", + expectedOutputName: "./sourceFile.csproj", + parameters1: parameters); + } + + private const string TemplateConfigQuotelessLiteralsEnabled = /*lang=json,strict*/ """ + { + "identity": "test.template", + "name": "tst", + "shortName": "tst", + "symbols": { + "ChoiceParam": { + "type": "parameter", + "description": "sample switch", + "datatype": "choice", + "enableQuotelessLiterals": true, + "choices": [ + { + "choice": "FirstChoice", + "description": "First Sample Choice" + }, + { + "choice": "SecondChoice", + "description": "Second Sample Choice" + }, + { + "choice": "ThirdChoice", + "description": "Third Sample Choice" + } + ], + "defaultValue": "ThirdChoice", + "defaultIfOptionWithoutValue": "SecondChoice" + } + } + } + """; + + [Theory] + // basic choice + [InlineData("FirstChoice", "FIRST", false)] + // nonexistent choice + [InlineData("Invalid", "UNKNOWN", true)] + // value not set - default used + [InlineData(null, "SECOND", false)] + // explicit unset + [InlineData("", "UNKNOWN", false)] + public async Task InstantiateAsync_ParamsProperlyHonored(string? parameterValue, string expectedOutput, bool instantiateShouldFail) + { + string sourceSnippet = """ + #if( ChoiceParam == FirstChoice ) + FIRST + #elseif (ChoiceParam == SecondChoice ) + SECOND + #elseif (ChoiceParam == ThirdChoice ) + THIRD + #else + UNKNOWN + #endif + """; + IReadOnlyDictionary parameters = new Dictionary() + { + { "ChoiceParam", parameterValue } + }; + + await InstantiateAsyncHelper( + TemplateConfigQuotelessLiteralsEnabled, + sourceSnippet, + expectedOutput, + "ChoiceParam", + instantiateShouldFail, + parameters1: parameters); + } + + private const string TemplateConfigCyclicParamsDependency = /*lang=json*/ """ + { + "identity": "test.template", + "name": "tst", + "shortName": "tst", + "symbols": { + "A": { + "type": "parameter", + "datatype": "bool", + "isEnabled": "!C && B != false", + }, + "B": { + "type": "parameter", + "datatype": "bool", + "isEnabled": "A != true || C", + }, + "C": { + "type": "parameter", + "datatype": "bool" + }, + } + } + """; + + [Theory] + [InlineData(false, true, false, "B,", false)] + [InlineData(true, false, false, "A,", true)] + // Theoretically the result is deterministic, but we'd need to understand the expression tree as well (and purge it) + [InlineData(true, false, true, "B,C", true)] + public async Task InstantiateAsync_ConditionalParametersCycleEvaluation(bool a_val, bool b_val, bool c_val, string expectedOutput, bool instantiateShouldFail) + { + // + // Template content preparation + // + + string sourceSnippet = """ + #if( A ) + A, + #endif + + #if( B ) + B, + #endif + + #if( C ) + C + #endif + """; + IReadOnlyDictionary parameters = new Dictionary() + { + { "A", a_val.ToString() }, + { "B", b_val.ToString() }, + { "C", c_val.ToString() } + }; + + await InstantiateAsyncHelper( + TemplateConfigCyclicParamsDependency, + sourceSnippet, + expectedOutput, + @"Failed to create template. +Details: Parameter conditions contain cyclic dependency: [A, B, A] that is preventing deterministic evaluation.", + instantiateShouldFail, + parameters1: parameters); + } + + private const string TemplateConfigIsRequiredCondition = /*lang=json*/ """ + { + "identity": "test.template", + "name": "tst", + "shortName": "tst", + "symbols": { + "A": { + "type": "parameter", + "datatype": "bool", + "isRequired": "!C && B != false", + "defaultValue": "false", + }, + "B": { + "type": "parameter", + "datatype": "bool", + "isRequired": "A != true || C", + "defaultValue": "true", + }, + "C": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + }, + } + } + """; + + [Theory] + [InlineData(false, true, false, "B,", false, null)] + [InlineData(true, false, false, "A,", false, null)] + [InlineData(null, null, true, "C", true, "B")] + [InlineData(null, true, false, "C", true, "A")] + [InlineData(null, null, false, "C", true, "A, B")] + [InlineData(null, null, null, "B,", true, "A, B")] + [InlineData(null, true, null, "B,", true, "A")] + [InlineData(null, false, null, "", false, null)] + [InlineData(false, false, false, "", false, null)] + [InlineData(true, false, true, "A,C", false, null)] + public async Task InstantiateAsync_ConditionalParametersIsRequiredEvaluation(bool? a_val, bool? b_val, bool? c_val, string expectedOutput, bool instantiateShouldFail, string? expectedErrorMessage) + { + // + // Template content preparation + // + + string sourceSnippet = """ + #if( A ) + A, + #endif + + #if( B ) + B, + #endif + + #if( C ) + C + #endif + """; + IReadOnlyDictionary parameters = new Dictionary() + { + { "A", a_val?.ToString() }, + { "B", b_val?.ToString() }, + { "C", c_val?.ToString() } + } + .Where(p => p.Value != null) + .ToDictionary(p => p.Key, p => p.Value); + + await InstantiateAsyncHelper( + TemplateConfigIsRequiredCondition, + sourceSnippet, + // To make the test data more compact we have left out the newlines - let's add them back here + expectedOutput.Length <= 2 ? expectedOutput : expectedOutput.Replace(",", $",{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}"), + expectedErrorMessage, + instantiateShouldFail, + parameters1: parameters); + } + + private const string TemplateConfigEnabledAndRequiredConditionsTogether = /*lang=json*/ """ + { + "identity": "test.template", + "name": "tst", + "shortName": "tst", + "symbols": { + "A": { + "type": "parameter", + "datatype": "string", + "isEnabled": "A_enable", + "isRequired": "true", + }, + "B": { + "type": "parameter", + "datatype": "string", + "isEnabled": "B_enable", + "isRequired": true + }, + "A_enable": { + "type": "parameter", + "datatype": "bool" + }, + "B_enable": { + "type": "parameter", + "datatype": "bool" + }, + } + } + """; + + [Theory] + [InlineData(false, false, null, null, "", false, null)] + [InlineData(true, false, null, null, "", true, "A")] + [InlineData(true, false, null, "x", "", true, "A")] + [InlineData(true, true, "false", "false", "", false, null)] + [InlineData(true, false, "true", null, "A,", false, null)] + [InlineData(false, true, null, null, "", true, "B")] + [InlineData(true, true, null, null, "", true, "A, B")] + [InlineData(null, null, null, null, "", false, null)] + [InlineData(null, true, null, "true", "B,", false, null)] + [InlineData(true, null, "true", "false", "A,", false, null)] + [InlineData(true, null, "true", null, "A,", false, null)] + [InlineData(null, true, null, null, "", true, "B")] + public async Task InstantiateAsync_ConditionalParametersRequiredOverwrittenByDisabled( + bool? a_enable_val, + bool? b_enable_val, + string? a, + string? b, + string expectedOutput, + bool instantiateShouldFail, + string? expectedErrorMessage) + { + // + // Template content preparation + // + + string sourceSnippet = """ + #if( A ) + A, + #endif + + #if( B ) + B, + #endif + + #if( C ) + C + #endif + """; + + IReadOnlyDictionary parameters = new Dictionary() + { + { "A_enable", a_enable_val?.ToString() }, + { "B_enable", b_enable_val?.ToString() }, + { "A", a }, + { "B", b } + } + .Where(p => p.Value != null) + .ToDictionary(p => p.Key, p => p.Value); + + await InstantiateAsyncHelper( + TemplateConfigEnabledAndRequiredConditionsTogether, + sourceSnippet, + expectedOutput, + expectedErrorMessage, + instantiateShouldFail, + parameters1: parameters); + } + + private const string TemplateConfigEnabledConditionInversed = /*lang=json*/ """ + { + "identity": "test.template", + "name": "tst", + "shortName": "tst", + "symbols": { + "A": { + "type": "parameter", + "datatype": "bool", + "isEnabled": "!A_disable", + "isRequired": false + }, + "A_disable": { + "type": "parameter", + "datatype": "bool" + }, + } + } + """; + + [Theory] + [InlineData(false, false, "", false, null)] + [InlineData(false, true, "A,", false, null)] + [InlineData(false, null, "", false, null)] + [InlineData(true, false, "", false, null)] + [InlineData(true, true, "", false, null)] + [InlineData(null, false, "", false, null)] + [InlineData(null, true, "A,", false, null)] + public async Task InstantiateAsync_ConditionalParametersInversedEnablingCondition( + bool? a_disable_val, + bool? a, + string expectedOutput, + bool instantiateShouldFail, + string? expectedErrorMessage) + { + // + // Template content preparation + // + + string sourceSnippet = """ + #if( A ) + A, + #endif + + #if( B ) + B, + #endif + """; + + IReadOnlyDictionary parameters = new Dictionary() + { + { "A_disable", a_disable_val?.ToString() }, + { "A", a?.ToString() }, + } + .Where(p => p.Value != null) + .ToDictionary(p => p.Key, p => p.Value); + + await InstantiateAsyncHelper( + TemplateConfigEnabledConditionInversed, + sourceSnippet, + expectedOutput, + expectedErrorMessage, + instantiateShouldFail, + parameters1: parameters); + } + + private const string TemplateConfigEnabledConditionEvaluationBehavior = """ + { + "identity": "test.template", + "name": "tst", + "shortName": "tst", + "symbols": { + "A": { + "type": "parameter", + "datatype": "bool", + "isRequired": false, + "isEnabled": "!IsDisabled", + "defaultValue": true + }, + ##Enable_Param + } + } + """; + + private const string ParamSnippetStringNoDefault = """ + "IsDisabled": { + "type": "parameter", + "datatype": "string" + } + """; + + private const string ParamSnippetStringDefault = """ + "IsDisabled": { + "type": "parameter", + "datatype": "string", + "defaultValue": "false" + } + """; + + private const string ParamSnippetBooleanNoDefault = """ + "IsDisabled": { + "type": "parameter", + "datatype": "bool" + } + """; + + private const string ParamSnippetBooleanDefaultFalse = """ + "IsDisabled": { + "type": "parameter", + "datatype": "bool", + "defaultValue": false + } + """; + + private const string ParamSnippetBooleanDefaultTrue = """ + "IsDisabled": { + "type": "parameter", + "datatype": "bool", + "defaultValue": true + } + """; + + [Theory] + [InlineData(ParamSnippetStringNoDefault, "", true, "Failed to evaluate condition IsEnabled on parameter A (condition text: !IsDisabled, evaluation error: Unable to logical not System.String)")] + [InlineData(ParamSnippetStringDefault, "A,", false, null)] + [InlineData(ParamSnippetBooleanNoDefault, "A,", false, null)] + [InlineData(ParamSnippetBooleanDefaultFalse, "A,", false, null)] + [InlineData(ParamSnippetBooleanDefaultTrue, "notA,", false, null)] + //[InlineData(false, true, "A,", false, null)] + public async Task InstantiateAsync_ConditionalParametersEvaluationBehavior( + string paramSnippet, + string expectedOutput, + bool instantiateShouldFail, + string? expectedErrorMessage) + { + // + // Template content preparation + // + + string sourceSnippet = """ + #if (A) + A, + #endif + + #if (!A) + notA, + #endif + + #if( B ) + B, + #endif + """; + + //IReadOnlyDictionary parameters = new Dictionary() + //{ + // { "A", a_disable_val?.ToString() }, + // { "Ap", a?.ToString() }, + //} + // .Where(p => p.Value != null) + // .ToDictionary(p => p.Key, p => p.Value); + + await InstantiateAsyncHelper( + TemplateConfigEnabledConditionEvaluationBehavior.Replace("##Enable_Param", paramSnippet), + sourceSnippet, + expectedOutput, + expectedErrorMessage, + instantiateShouldFail, + parameters1: new Dictionary()); + } + + private const string TemplateConfigForExternalConditionsEvaluation = /*lang=json*/ """ + { + "identity": "test.template", + "name": "tst", + "shortName": "tst", + "symbols": { + "parA": { + "type": "parameter", + "datatype": "string", + "isEnabled": "A_enable", + "isRequired": "true || true", + }, + "parB": { + "type": "parameter", + "datatype": "string", + "isEnabled": "B_enable", + "isRequired": true + }, + "C": { + "type": "parameter", + "datatype": "bool" + }, + "A_enable": { + "type": "parameter", + "datatype": "bool" + }, + "B_enable": { + "type": "parameter", + "datatype": "bool" + }, + } + } + """; + + [Theory] + [InlineData(null, true, false, null, false, null, /*c_val*/ true, "C", false, "")] + [InlineData(true, true, true, null, false, null, /*c_val*/ false, "parA,", false, "")] + [InlineData(null, true, false, null, true, null, /*c_val*/ true, "", true, "parB")] + [InlineData(null, true, true, null, false, null, /*c_val*/ true, "", true, "parA")] + [InlineData(null, true, false, null, false, false, /*c_val*/ false, "", true, @"Attempt to pass result of external evaluation of parameters conditions for parameter(s) that do not have appropriate condition set in template (IsEnabled or IsRequired attributes not populated with condition): B (parameter)")] + public async Task InstantiateAsync_ConditionalParametersWithExternalEvaluation( + bool? a_val, + bool? a_enabled, + bool? a_required, + bool? b_val, + bool? b_enabled, + bool? b_required, + bool c_val, + string expectedOutput, + bool instantiateShouldFail, + string expectedErrorMessage) + { + // + // Template content preparation + // + + string sourceSnippet = """ + #if( parA ) + parA, + #endif + + #if( parB ) + parB, + #endif + + #if( C ) + C + #endif + """; + + List parameters = new( + new[] + { + new InputDataBag("parA", a_val, a_enabled, a_required), + new InputDataBag("parB", b_val, b_enabled, b_required), + new InputDataBag("C", c_val), + }); + + await InstantiateAsyncHelper( + TemplateConfigForExternalConditionsEvaluation, + sourceSnippet, + expectedOutput, + expectedErrorMessage, + instantiateShouldFail, + parameters2: parameters); + } + + private const string TemplateConfigPreferDefaultNameWithDefaultName = /*lang=json*/ """ + { + "identity": "test.template", + "name": "tst", + "shortName": "tst", + "preferDefaultName": true, + "defaultName": "defaultName", + "sourceName": "sourceFile" + } + """; + + private const string TemplateConfigPreferDefaultNameWithoutDefaultName = /*lang=json*/ """ + { + "identity": "test.template", + "name": "tst", + "shortName": "tst", + "preferDefaultName": true, + "sourceName": "sourceFile" + } + """; + + private const string TemplateConfigNoPreferDefaultNameWithDefaultName = /*lang=json*/ """ + { + "identity": "test.template", + "name": "tst", + "shortName": "tst", + "preferDefaultName": false, + "sourceName": "sourceFile" + } + """; + + [Theory] + [InlineData(TemplateConfigPreferDefaultNameWithDefaultName, "thisIsAName", "./thisIsAName.cs", false, "")] + [InlineData(TemplateConfigPreferDefaultNameWithDefaultName, null, "./defaultName.cs", false, "")] + [InlineData(TemplateConfigNoPreferDefaultNameWithDefaultName, null, "./tst2.cs", false, "")] + [InlineData(TemplateConfigPreferDefaultNameWithoutDefaultName, null, "./tst2.cs", true, "Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration.")] + public async Task InstantiateAsync_PreferDefaultName(string templateConfig, string? name, string expectedOutputName, bool instanceFailure, string errorMessage) + { + string sourceSnippet = """ + using System; + + Console.log("Hello there, this is a test!"); + """; + + await InstantiateAsyncHelper( + templateConfig, + sourceSnippet, + sourceSnippet, + errorMessage, + instanceFailure, + name: name, + expectedOutputName: expectedOutputName); + } + + [Fact] + public async Task InstantiateAsync_InvalidTemplate() + { + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new InMemoryLoggerProvider(loggedMessages); + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true, addLoggerProviders: new[] { loggerProvider }); + + const string templateConfig = /*lang=json*/ """ + { + "identity": "test.template", + "name": "test" + } + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, templateConfig } + }; + + string sourceBasePath = environmentSettings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(environmentSettings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = environmentSettings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new(); + + IFile? templateConfigFile = sourceMountPoint.FileInfo(TestFileSystemUtils.DefaultConfigRelativePath); + Assert.NotNull(templateConfigFile); + + using var runnableConfig = new RunnableProjectConfig(environmentSettings, rpg, templateConfigFile); + + TemplateCreator creator = new TemplateCreator(environmentSettings); + + string targetDir = environmentSettings.GetTempVirtualizedPath(); + + ITemplateCreationResult instantiateResult = await creator.InstantiateAsync( + templateInfo: runnableConfig, + name: "test", + fallbackName: "test", + inputParameters: new Dictionary(), + outputPath: targetDir); + + Assert.Equal(CreationResultStatus.TemplateIssueDetected, instantiateResult.Status); + Assert.Equal("The template is invalid and cannot be instantiated.", instantiateResult.ErrorMessage); + + string[] errors = loggedMessages.Where(m => m.Level == LogLevel.Error).Select(m => m.Message).ToArray(); + string debugMessage = loggedMessages.Where(m => m.Level == LogLevel.Debug).Select(m => m.Message).Last(); + + Assert.Equal(2, errors.Length); + + Assert.Equal( + """ + The template 'test' (test.template) has the following validation errors: + [Error][MV003] Missing 'shortName'. + + """, + errors[0]); + Assert.Equal("Failed to load the template 'test' (test.template): the template is not valid.", errors[1]); + Assert.Equal( + """ + The template 'test' (test.template) has the following validation messages: + [Info][MV005] Missing 'sourceName'. + [Info][MV006] Missing 'author'. + [Info][MV007] Missing 'groupIdentity'. + [Info][MV008] Missing 'generatorVersions'. + [Info][MV009] Missing 'precedence'. + [Info][MV010] Missing 'classifications'. + + """, + debugMessage); + } + + [Fact] + public async Task InstantiateAsync_InvalidLocalization() + { + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new InMemoryLoggerProvider(loggedMessages); + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true, addLoggerProviders: new[] { loggerProvider }); + + const string templateConfig = /*lang=json*/ """ + { + "identity": "test.template", + "name": "test", + "shortName": "test" + } + """; + + //this localization has post action, which doesn't exist in main config + const string templateLoc = /*lang=json*/ """ + { + "name": "name_de-DE", + "description": "desc_de-DE", + "postActions/pa0/description": "pa0_desc_de-DE", + "postActions/pa0/manualInstructions/first_instruction/text": "pa0_manualInstructions_de-DE" + } + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, templateConfig }, + { ".template.config/localize/templatestrings.de-DE.json", templateLoc } + }; + + string sourceBasePath = environmentSettings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(environmentSettings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = environmentSettings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new(); + + IFile? templateConfigFile = sourceMountPoint.FileInfo(TestFileSystemUtils.DefaultConfigRelativePath); + Assert.NotNull(templateConfigFile); + IFile? templateLocFile = sourceMountPoint.FileInfo(".template.config/localize/templatestrings.de-DE.json"); + Assert.NotNull(templateLoc); + + using var runnableConfig = new RunnableProjectConfig(environmentSettings, rpg, templateConfigFile, localeConfigFile: templateLocFile); + + TemplateCreator creator = new TemplateCreator(environmentSettings); + + string targetDir = environmentSettings.GetTempVirtualizedPath(); + + ITemplateCreationResult instantiateResult = await creator.InstantiateAsync( + templateInfo: runnableConfig, + name: "test", + fallbackName: "test", + inputParameters: new Dictionary(), + outputPath: targetDir); + + Assert.Equal(CreationResultStatus.Success, instantiateResult.Status); + Assert.Null(instantiateResult.ErrorMessage); + + string error = loggedMessages.Where(m => m.Level == LogLevel.Error).Select(m => m.Message).Single(); + string warning = loggedMessages.Where(m => m.Level == LogLevel.Warning).Select(m => m.Message).Single(); + + Assert.Equal( + """ + The template 'test' (test.template) has the following validation errors in 'de-DE' localization: + [Error][LOC002] Post action(s) with id(s) 'pa0' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + + """, + error); + Assert.Equal("Failed to load the 'de-DE' localization the template 'test' (test.template): the localization file is not valid. The localization will be skipped.", warning); + } + + private async Task InstantiateAsyncHelper( + string templateSnippet, + string sourceSnippet, + string expectedOutput, + string? expectedErrorMessage, + bool instantiateShouldFail, + string? name = "sourceFile", + string expectedOutputName = "./sourceFile.cs", + string sourceExtension = ".cs", + IReadOnlyDictionary? parameters1 = null, + IReadOnlyList? parameters2 = null) + { + // + // Template content preparation + // + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, templateSnippet } + }; + + string sourceFileName = name is null ? "sourceFile" + sourceExtension : name + sourceExtension; + + //content + templateSourceFiles.Add(sourceFileName, sourceSnippet); + + // + // Dependencies preparation and mounting + // + + string sourceBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(_engineEnvironmentSettings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = _engineEnvironmentSettings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new(); + + IFile? templateConfigFile = sourceMountPoint.FileInfo(TestFileSystemUtils.DefaultConfigRelativePath); + Assert.NotNull(templateConfigFile); + + using var runnableConfig = new RunnableProjectConfig(_engineEnvironmentSettings, rpg, templateConfigFile); + + TemplateCreator creator = new TemplateCreator(_engineEnvironmentSettings); + + string targetDir = _engineEnvironmentSettings.GetTempVirtualizedPath(); + + ITemplateCreationResult res; + if (parameters1 != null) + { + res = await creator.InstantiateAsync( + templateInfo: runnableConfig, + name: name, + fallbackName: "tst2", + inputParameters: parameters1!, + outputPath: targetDir); + } + else if (parameters2 != null) + { + IParameterDefinitionSet parameters = runnableConfig.ParameterDefinitions; + + InputDataSet data; + try + { + data = new InputDataSet( + parameters, + parameters2!.Select(p => new EvaluatedInputParameterData( + parameters[p.Name], + p.Value, + DataSource.User, + p.IsEnabledConditionResult, + p.IsRequiredConditionResult, + p.IsNull ? InputDataState.Unset : InputDataState.Set)).ToList()) + { + ContinueOnMismatchedConditionsEvaluation = true + }; + + res = await creator.InstantiateAsync( + templateInfo: runnableConfig, + name: name, + fallbackName: "tst2", + inputParameters: data, + outputPath: targetDir); + } + catch (Exception e) + { + Assert.True(instantiateShouldFail); + Assert.True(instantiateShouldFail); + e.Message.Should().BeEquivalentTo(e.Message); + return; + } + } + else + { + InputDataSet parameters = new InputDataSet(runnableConfig); + + res = await creator.InstantiateAsync( + templateInfo: runnableConfig, + name: name, + fallbackName: "tst2", + inputParameters: parameters, + outputPath: targetDir); + } + + if (instantiateShouldFail) + { + res.ErrorMessage.Should().NotBeNullOrEmpty(); + res.ErrorMessage.Should().Contain(expectedErrorMessage); + res.OutputBaseDirectory.Should().Match(s => + string.IsNullOrEmpty(s) || !_engineEnvironmentSettings.Host.FileSystem.FileExists(s)); + } + else + { + res.ErrorMessage.Should().BeNull(); + res.OutputBaseDirectory.Should().NotBeNullOrEmpty(); + + res.CreationEffects.Should().NotBeNull(); + res.CreationEffects!.FileChanges.Should().NotBeNullOrEmpty().And.HaveCount(1); + res.CreationEffects.FileChanges[0].TargetRelativePath.Should().Be(expectedOutputName); + + string resultContent = File.Exists(Path.Combine(res.OutputBaseDirectory!, sourceFileName)) + ? _engineEnvironmentSettings.Host.FileSystem + .ReadAllText(Path.Combine(res.OutputBaseDirectory!, sourceFileName)).Trim() + : _engineEnvironmentSettings.Host.FileSystem + .ReadAllText(Path.Combine(res.OutputBaseDirectory!, expectedOutputName)).Trim(); + resultContent.Should().BeEquivalentTo(expectedOutput.Trim()); + } + } + + private class InputDataBag + { + public InputDataBag(string name, bool? value, bool? isEnabledConditionResult = null, bool? isRequiredConditionResult = null) + { + Name = name; + Value = value?.ToString(); + IsEnabledConditionResult = isEnabledConditionResult; + IsRequiredConditionResult = isRequiredConditionResult; + IsNull = value == null; + } + + public string Name { get; } + + public string? Value { get; } + + public bool? IsEnabledConditionResult { get; } + + public bool? IsRequiredConditionResult { get; } + + public bool IsNull { get; } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplatePackageManagerTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplatePackageManagerTests.cs new file mode 100644 index 000000000000..1b47408e79f3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/TemplatePackageManagerTests.cs @@ -0,0 +1,463 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FakeItEasy; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.Edge.UnitTests.Fakes; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Microsoft.TemplateEngine.Utils; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class TemplatePackageManagerTests : TestBase, IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public TemplatePackageManagerTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Fact] + public async Task OrderOfScanningIsCorrect() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(); + var workingDir = TestUtils.CreateTemporaryFolder("workingDir"); + var folders = new List(); + + for (int i = 0; i < 100; i++) + { + var folderPath = Path.Combine(workingDir, $"Folder{i}"); + folders.Add(folderPath); + engineEnvironmentSettings.Host.FileSystem.CreateDirectory(Path.Combine(folderPath, ".template.config")); + engineEnvironmentSettings.Host.FileSystem.WriteAllText( + Path.Combine(folderPath, ".template.config", "template.json"), + $"{{ \"identity\": \"AllHaveSameIdentity\", \"shortName\": \"sample{i}\", \"name\": \"sample name {i}\"}}"); + } + + FakeFactory.SetNuPkgsAndFolders(folders: folders); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplatePackageProviderFactory), new FakeFactory()); + var templates = await new TemplatePackageManager(engineEnvironmentSettings).GetTemplatesAsync(default); + + Assert.Single(templates); + Assert.Equal("sample99", templates.Single().ShortNameList[0]); + } + + [Fact] + public async Task OrderOfScanningIsCorrectWithPriority() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(); + var workingDir = TestUtils.CreateTemporaryFolder("workingDir"); + var folders = new List(); + + for (int i = 0; i < 100; i++) + { + var folderPath = Path.Combine(workingDir, $"Folder{i}"); + folders.Add(folderPath); + engineEnvironmentSettings.Host.FileSystem.CreateDirectory(Path.Combine(folderPath, ".template.config")); + engineEnvironmentSettings.Host.FileSystem.WriteAllText( + Path.Combine(folderPath, ".template.config", "template.json"), + $"{{ \"identity\": \"AllHaveSameIdentity\", \"shortName\": \"sample{i}\", \"name\": \"sample name {i}\"}}"); + } + + FakeFactoryWithPriority.StaticPriority = 100; + FakeFactoryWithPriority.SetNuPkgsAndFolders(folders: folders.Take(50)); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplatePackageProviderFactory), new FakeFactoryWithPriority()); + + FakeFactory.SetNuPkgsAndFolders(folders: folders.Skip(50).Take(50)); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplatePackageProviderFactory), new FakeFactory()); + + var templates = await new TemplatePackageManager(engineEnvironmentSettings).GetTemplatesAsync(default); + + Assert.Single(templates); + Assert.Equal("sample49", templates.Single().ShortNameList[0]); + } + + [Fact] + public async Task RebuildCacheIfNotCurrentScansAll() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(); + + var nupkgFolder = TestTemplatePackagesLocation; + var nupkgsWildcard = new[] { Path.Combine(nupkgFolder, "*.nupkg") }; + + FakeFactory.SetNuPkgsAndFolders(nupkgsWildcard); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplatePackageProviderFactory), new FakeFactory()); + await new TemplatePackageManager(engineEnvironmentSettings).GetTemplatesAsync(default); + + var allNupkgs = Directory.GetFiles(nupkgFolder); + // All mount points should have been scanned + AssertMountPointsWereOpened(allNupkgs, engineEnvironmentSettings); + } + + [Theory] + [InlineData("ManagedPackage", null)] + [InlineData("ManagedPackage", "1.0.0")] + public async Task GetsManagedPackageWithTemplates(string packageIdentifier, string? version) + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment( + additionalComponents: new List<(Type, IIdentifiedComponent)> + { + (typeof(IGenerator), GetGeneratorMock()), + (typeof(IInstallerFactory), GetInstallerFactoryMock()), + (typeof(IMountPointFactory), new MountPointFactoryMock()), + (typeof(ITemplatePackageProviderFactory), new FakeManagedPackageProviderFactory()) + }); + + var result = await new TemplatePackageManager(engineEnvironmentSettings) + .GetManagedTemplatePackageAsync(packageIdentifier, version, default); + var (package, templates) = result; + + Assert.NotNull(templates); + Assert.Single(templates); + Assert.NotNull(package); + Assert.Equal("ManagedMount", package.MountPointUri); + } + + [Theory] + [InlineData("ManagedPackage", "1.0.1")] + public async Task CantGetManagedPackageWithTemplatesDueToVersionMismatchAsync(string packageIdentifier, string? version) + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment( + additionalComponents: new List<(Type, IIdentifiedComponent)> + { + (typeof(IGenerator), GetGeneratorMock()), + (typeof(IInstallerFactory), GetInstallerFactoryMock()), + (typeof(IMountPointFactory), new MountPointFactoryMock()), + (typeof(ITemplatePackageProviderFactory), new FakeManagedPackageProviderFactory()) + }); + + var packageManager = new TemplatePackageManager(engineEnvironmentSettings); + await Assert.ThrowsAsync(async () => await packageManager.GetManagedTemplatePackageAsync(packageIdentifier, version, default)); + } + + [Fact] + public async Task EnsureCacheIsLoadedOnlyOnce() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(); + var monitoredFileSystem = (MonitoredFileSystem)engineEnvironmentSettings.Host.FileSystem; + + var nupkgFolder = TestTemplatePackagesLocation; + var nupkgsWildcard = new[] { Path.Combine(nupkgFolder, "*.nupkg") }; + + FakeFactory.SetNuPkgsAndFolders(nupkgsWildcard); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplatePackageProviderFactory), new FakeFactory()); + + // Execute 1st time to create file on disk + monitoredFileSystem.Reset(); + var templatePackageManager1 = new TemplatePackageManager(engineEnvironmentSettings); + await templatePackageManager1.GetTemplatesAsync(default); + Assert.Contains(new SettingsFilePaths(engineEnvironmentSettings).TemplateCacheFile, monitoredFileSystem.FilesOpened); + // All mount points should have been scanned + AssertMountPointsWereOpened(Directory.GetFiles(nupkgFolder), engineEnvironmentSettings); + + // Execute 2st time with different templatePackageManager to load existing cached created in 1st step + monitoredFileSystem.Reset(); + var templatePackageManager2 = new TemplatePackageManager(engineEnvironmentSettings); + await templatePackageManager2.GetTemplatesAsync(default); + Assert.Contains(new SettingsFilePaths(engineEnvironmentSettings).TemplateCacheFile, monitoredFileSystem.FilesOpened); + // No mount points should have been scanned + AssertMountPointsWereOpened([], engineEnvironmentSettings); + + // Execute 3rd time with same templatePackageManager to test that TemplateCacheFile is not parsed. + monitoredFileSystem.Reset(); + await templatePackageManager2.GetTemplatesAsync(default); + Assert.DoesNotContain(new SettingsFilePaths(engineEnvironmentSettings).TemplateCacheFile, monitoredFileSystem.FilesOpened); + // No mount points should have been scanned + AssertMountPointsWereOpened([], engineEnvironmentSettings); + } + + [Fact] + public async Task RebuildCacheSkipsNonAccessibleMounts() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(); + var nupkgFolder = TestTemplatePackagesLocation; + var validAndInvalidNuPkg = new[] { Directory.GetFiles(nupkgFolder, "*.nupkg")[0], Path.Combine(nupkgFolder, $"{default(Guid)}.nupkg") }; + + FakeFactory.SetNuPkgsAndFolders(validAndInvalidNuPkg); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplatePackageProviderFactory), new FakeFactory()); + await new TemplatePackageManager(engineEnvironmentSettings).GetTemplatesAsync(default); + + var allNupkgs = validAndInvalidNuPkg.Take(1); + // All mount points should have been scanned + AssertMountPointsWereOpened(allNupkgs, engineEnvironmentSettings); + } + + [Fact] + public async Task RebuildCacheIfForceRebuildScansAll() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(); + + var nupkgFolder = TestTemplatePackagesLocation; + var nupkgsWildcard = new[] { Path.Combine(nupkgFolder, "*.nupkg") }; + + FakeFactory.SetNuPkgsAndFolders(nupkgsWildcard); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplatePackageProviderFactory), new FakeFactory()); + TemplatePackageManager templatePackageManager = new TemplatePackageManager(engineEnvironmentSettings); + await templatePackageManager.GetTemplatesAsync(default); + + var allNupkgs = Directory.GetFiles(nupkgFolder); + // All mount points should have been scanned + AssertMountPointsWereOpened(allNupkgs, engineEnvironmentSettings); + + var monitoredFileSystem = (MonitoredFileSystem)engineEnvironmentSettings.Host.FileSystem; + + monitoredFileSystem.Reset(); + await templatePackageManager.GetTemplatesAsync(default); + // Make sure that we don't rescan with force=false + AssertMountPointsWereOpened([], engineEnvironmentSettings); + + monitoredFileSystem.Reset(); + await templatePackageManager.RebuildTemplateCacheAsync(default); + // Make sure that we rescan with force=false + AssertMountPointsWereOpened(allNupkgs, engineEnvironmentSettings); + + monitoredFileSystem.Reset(); + await templatePackageManager.GetTemplatesAsync(default); + // Make sure that we don't rescan with force=false + AssertMountPointsWereOpened([], engineEnvironmentSettings); + } + + [Fact] + public async Task EnsureCacheRoundtripPreservesTemplateWithLocaleTimestamp() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment("en-GB"); + + var nupkgFolder = TestTemplatePackagesLocation; + var nupkgsWildcard = new[] { Path.Combine(nupkgFolder, "*.nupkg") }; + + FakeFactory.SetNuPkgsAndFolders(nupkgsWildcard); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplatePackageProviderFactory), new FakeFactory()); + TemplatePackageManager templatePackageManager = new TemplatePackageManager(engineEnvironmentSettings); + await templatePackageManager.GetTemplatesAsync(default); + + var allNupkgs = Directory.GetFiles(nupkgFolder); + // All mount points should have been scanned + AssertMountPointsWereOpened(allNupkgs, engineEnvironmentSettings); + + var monitoredFileSystem = (MonitoredFileSystem)engineEnvironmentSettings.Host.FileSystem; + + FakeFactory.TriggerChanged(); + + monitoredFileSystem.Reset(); + await templatePackageManager.GetTemplatesAsync(default); + // Make sure that we don't rescan with force=false + AssertMountPointsWereOpened([], engineEnvironmentSettings); + } + + [Fact] + public async Task RemoveMountpointRemovesTemplates() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(); + + var nupkgFolder = TestTemplatePackagesLocation; + var allNupkgs = Directory.GetFiles(nupkgFolder).Select(Path.GetFullPath).ToList(); + + FakeFactory.SetNuPkgsAndFolders(allNupkgs); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplatePackageProviderFactory), new FakeFactory()); + TemplatePackageManager templatePackageManager = new TemplatePackageManager(engineEnvironmentSettings); + var templatesAll = await templatePackageManager.GetTemplatesAsync(default); + + // All mount points should have been scanned + AssertMountPointsWereOpened(allNupkgs, engineEnvironmentSettings); + + // All mountpoints/sources should have at least 1 template + Assert.Equal(allNupkgs.OrderBy(m => m), templatesAll.Select(t => t.MountPointUri).Distinct().OrderBy(m => m)); + + var monitoredFileSystem = (MonitoredFileSystem)engineEnvironmentSettings.Host.FileSystem; + + monitoredFileSystem.Reset(); + + //Remove all but 1 + allNupkgs.RemoveRange(1, allNupkgs.Count - 1); + FakeFactory.TriggerChanged(); + + var templatesOnly1 = await templatePackageManager.GetTemplatesAsync(default); + + // Make sure that templates return only have MountPointUri of our remaining nupkg + Assert.Equal(allNupkgs, templatesOnly1.Select(t => t.MountPointUri).Distinct().OrderBy(m => m)); + } + + [Fact] + public async Task CanSkipFaultedProvider() + { + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(); + FakeFactory.SetNuPkgsAndFolders(folders: new[] { TestTemplatesLocation }); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplatePackageProviderFactory), new FakeFactory()); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplatePackageProviderFactory), new FaultFakeFactory()); + + TemplatePackageManager templatePackageManager = new TemplatePackageManager(engineEnvironmentSettings); + var templates = await templatePackageManager.GetTemplatesAsync(default); + Assert.NotEmpty(templates); + } + + private void AssertMountPointsWereOpened(IEnumerable mountPoints, IEngineEnvironmentSettings environmentSettings) + { + string[] expectedScannedDirectories = mountPoints + .Select(Path.GetFullPath) + .OrderBy(x => x) + .ToArray(); + string[] actualScannedDirectories = ((MonitoredFileSystem)environmentSettings.Host.FileSystem).FilesOpened + .Where((f) => Path.GetExtension(f) == ".nupkg") // Filter to only check for .nupkgs, and ignore templatecache.json and others... + .Select(Path.GetFullPath) + .OrderBy(x => x) + .ToArray(); + + Assert.Equal(expectedScannedDirectories, actualScannedDirectories); + } + + internal IGenerator GetGeneratorMock() + { + var generatorMock = A.Fake(); + A.CallTo(() => generatorMock.GetTemplatesFromMountPointAsync(A._, A._)) + .ReturnsLazily(call => + { + var template = A.Fake(); + A.CallTo(() => template.MountPointUri).Returns("ManagedMount"); + A.CallTo(() => template.IsValid).Returns(true); + IReadOnlyList templates = new List() { template }; + + return Task.FromResult(templates); + }); + + return generatorMock; + } + + public IInstallerFactory GetInstallerFactoryMock() + { + var installerFactoryMock = A.Fake(); + A.CallTo(() => installerFactoryMock.Name).Returns("InstallerMock"); + A.CallTo(() => installerFactoryMock.CreateInstaller(A._, A._)) + .ReturnsLazily((IEngineEnvironmentSettings settings, string path) => GetInstallerMock(installerFactoryMock)); + + return installerFactoryMock; + } + + internal class MountPointFactoryMock : IMountPointFactory + { + public Guid Id => Guid.Empty; + + public bool TryMount(IEngineEnvironmentSettings environmentSettings, IMountPoint? parent, string mountPointUri, out IMountPoint? mountPoint) + { + mountPoint = new MockMountPoint(environmentSettings); + return true; + } + } + + public interface IManagedInstallerMock : IInstaller, ISerializableInstaller; + + internal IManagedInstallerMock GetInstallerMock(IInstallerFactory factory) + { + var installerMock = A.Fake(); + A.CallTo(() => installerMock.Factory).Returns(factory); + A.CallTo(() => installerMock.Serialize(A._)) + .Returns(new TemplatePackageData(Guid.Empty, "ManagedMount", DateTime.Now, new Dictionary())); + + return installerMock; + } + + private class FakeFactory : ITemplatePackageProviderFactory + { + private static readonly List> AllCreatedProviders = new List>(); + + public string DisplayName => nameof(FakeFactory); + + public Guid Id { get; } = new Guid("{61CFA828-97B6-44EB-A44D-0AE673D6DF52}"); + + private static IEnumerable? Folders { get; set; } + + private static IEnumerable? NuPkgs { get; set; } + + public static void SetNuPkgsAndFolders(IEnumerable? nupkgs = null, IEnumerable? folders = null) + { + NuPkgs = nupkgs; + Folders = folders; + } + + public static void TriggerChanged() + { + foreach (var provider in AllCreatedProviders) + { + if (provider.TryGetTarget(out var actualProvider)) + { + actualProvider.UpdatePackages(NuPkgs, Folders); + } + } + } + + public ITemplatePackageProvider CreateProvider(IEngineEnvironmentSettings settings) + { + var defaultTemplatePackageProvider = new DefaultTemplatePackageProvider(this, settings, NuPkgs, Folders); + AllCreatedProviders.Add(new WeakReference(defaultTemplatePackageProvider)); + return defaultTemplatePackageProvider; + } + } + + private class FaultFakeFactory : ITemplatePackageProviderFactory + { + public string DisplayName => nameof(FaultFakeFactory); + + public Guid Id { get; } = new Guid("{61CFA828-97B6-44EB-A44D-0AE673D6DF53}"); + + public ITemplatePackageProvider CreateProvider(IEngineEnvironmentSettings settings) + { + return new FaultProvider(this); + } + + private class FaultProvider : ITemplatePackageProvider + { + public FaultProvider(ITemplatePackageProviderFactory factory) + { + Factory = factory; + } + + public ITemplatePackageProviderFactory Factory { get; } + + public event Action? TemplatePackagesChanged + { + add { } + remove { } + } + + public Task> GetAllTemplatePackagesAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + } + } + + private class FakeFactoryWithPriority : ITemplatePackageProviderFactory, IPrioritizedComponent + { + private static readonly List> AllCreatedProviders = new(); + + public string DisplayName => nameof(FakeFactory); + + public Guid Id { get; } = new Guid("{D98CAC97-2474-48B2-AE8D-B665D9E79C66}"); + + private static IEnumerable? Folders { get; set; } + + private static IEnumerable? NuPkgs { get; set; } + + public static void SetNuPkgsAndFolders(IEnumerable? nupkgs = null, IEnumerable? folders = null) + { + NuPkgs = nupkgs; + Folders = folders; + } + + public static int StaticPriority { get; set; } + + public ITemplatePackageProvider CreateProvider(IEngineEnvironmentSettings settings) + { + var defaultTemplatePackageProvider = new DefaultTemplatePackageProvider(this, settings, NuPkgs, Folders); + AllCreatedProviders.Add(new WeakReference(defaultTemplatePackageProvider)); + return defaultTemplatePackageProvider; + } + + public int Priority => StaticPriority; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/WorkloadConstraintTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/WorkloadConstraintTests.cs new file mode 100644 index 000000000000..d525e67f38ed --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Edge.UnitTests/WorkloadConstraintTests.cs @@ -0,0 +1,154 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using FakeItEasy; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Components; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Edge.Constraints; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Edge.UnitTests +{ + public class WorkloadConstraintTests + { + [Theory] + [InlineData(new[] { "workload", "workloadAA" }, false)] + [InlineData(new[] { "workload", "workloadA" }, true)] + [InlineData(new[] { "workloadB", "workload" }, true)] + [InlineData(new string[0], false)] + public async Task Evaluate_ArrayOfVersions(IReadOnlyList workloads, bool allowed) + { + var config = new + { + identity = "test-constraint-01", + constraints = new + { + specVersions = new + { + type = "workload", + args = new[] { "workloadA", "workloadB" } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + IWorkloadsInfoProvider workloadInfoProvider = new WorkloadsInfoProviderMock(workloads); //A.Fake(); + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { workloadInfoProvider }); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new WorkloadConstraintFactory() }); + + var constraintManager = new TemplateConstraintManager(settings); + + //A.CallTo(() => workloadInfoProvider + // .GetInstalledWorkloadsAsync(A._)) + // .Returns(Task.FromResult(workloads.Select(s => new WorkloadInfo(s, $"D:{s}")))); + + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + Assert.Equal(allowed ? TemplateConstraintResult.Status.Allowed : TemplateConstraintResult.Status.Restricted, evaluateResult.EvaluationStatus); + } + + [Fact] + public async Task Evaluate_MultipleConflictingProviders() + { + var config = new + { + identity = "test-constraint-01", + constraints = new + { + specVersions = new + { + type = "workload", + args = new[] { "workloadA", "workloadB" } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + IWorkloadsInfoProvider workloadInfoProviderA = A.Fake(); + A.CallTo(() => workloadInfoProviderA + .GetInstalledWorkloadsAsync(A._)) + .Returns(Task.FromResult(new[] { "workload", "workloadA" }.Select(s => new WorkloadInfo(s, $"D:{s}")))); + + IWorkloadsInfoProvider workloadInfoProviderB = A.Fake(); + A.CallTo(() => workloadInfoProviderB + .GetInstalledWorkloadsAsync(A._)) + .Returns(Task.FromResult(new[] { "workload", "workloadA", "workloadB" }.Select(s => new WorkloadInfo(s, $"D:{s}")))); + + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { workloadInfoProviderA, workloadInfoProviderB }); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new WorkloadConstraintFactory() }); + List<(LogLevel, string)> messagesCollection = new(); + ILogger logger = new InMemoryLoggerProvider(messagesCollection).CreateLogger("x"); + A.CallTo(() => settings.Host.Logger).Returns(logger); + + var constraintManager = new TemplateConstraintManager(settings); + + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + Assert.Equal(TemplateConstraintResult.Status.NotEvaluated, evaluateResult.EvaluationStatus); + Assert.Equal(0, messagesCollection.Count(t => t.Item1 >= LogLevel.Warning)); + Assert.StartsWith("The constraint 'workload' failed to initialize", evaluateResult.LocalizedErrorMessage); + } + + [Fact] + public async Task Evaluate_MultipleDuplicateProviders() + { + var config = new + { + identity = "test-constraint-01", + constraints = new + { + specVersions = new + { + type = "workload", + args = new[] { "workloadA", "workloadB" } + } + } + }; + + var configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(config))!.AsObject()); + IWorkloadsInfoProvider workloadInfoProviderA = A.Fake(); + A.CallTo(() => workloadInfoProviderA + .GetInstalledWorkloadsAsync(A._)) + .Returns(Task.FromResult(new[] { "workload", "workloadA" }.Select(s => new WorkloadInfo(s, $"D:{s}")))); + + IWorkloadsInfoProvider workloadInfoProviderB = A.Fake(); + A.CallTo(() => workloadInfoProviderB + .GetInstalledWorkloadsAsync(A._)) + .Returns(Task.FromResult(new[] { "workloadA", "workload" }.Select(s => new WorkloadInfo(s, $"D:{s}")))); + + IEngineEnvironmentSettings settings = A.Fake(); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { workloadInfoProviderA, workloadInfoProviderB }); + A.CallTo(() => settings.Components.OfType()).Returns(new[] { new WorkloadConstraintFactory() }); + List<(LogLevel, string)> messagesCollection = new(); + ILogger logger = new InMemoryLoggerProvider(messagesCollection).CreateLogger("x"); + A.CallTo(() => settings.Host.Logger).Returns(logger); + + var constraintManager = new TemplateConstraintManager(settings); + + var evaluateResult = await constraintManager.EvaluateConstraintAsync(configModel.Constraints.Single().Type, configModel.Constraints.Single().Args, default); + Assert.Equal(TemplateConstraintResult.Status.NotEvaluated, evaluateResult.EvaluationStatus); + Assert.Equal(0, messagesCollection.Count(t => t.Item1 >= LogLevel.Warning)); + Assert.StartsWith("The constraint 'workload' failed to initialize", evaluateResult.LocalizedErrorMessage); + } + + // This is a workaround in a weird bug with FakeItEasy - more details in SdkVersionConstraintTests.cs + private class WorkloadsInfoProviderMock : IWorkloadsInfoProvider + { + private readonly IEnumerable _res; + + public WorkloadsInfoProviderMock(IEnumerable res) => _res = res.Select(s => new WorkloadInfo(s, $"D:{s}")); + + public Guid Id { get; } + + public Task> GetInstalledWorkloadsAsync(CancellationToken token) => Task.FromResult(_res); + + public string ProvideConstraintRemedySuggestion(IReadOnlyList supportedWorkloads) => "Sample CTA"; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BasicTests.SourceNameForms_BasicTest.verified.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BasicTests.SourceNameForms_BasicTest.verified.txt new file mode 100644 index 000000000000..ba1ffaa3298c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BasicTests.SourceNameForms_BasicTest.verified.txt @@ -0,0 +1,6 @@ +Identity: MyApp.1 +Namespace: MyApp._1 +Class name: MyApp__1 +Namespace (lc): myapp._1 +Class name (lc): myapp__1 +Lowercase: myapp.1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs new file mode 100644 index 000000000000..2244b7fceb83 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs @@ -0,0 +1,24 @@ +//CPP2 processing +//Values: +var A = False; +var B = False; +var C = True; +var D = True; + +//computed - 1: "value": "A" +var A1 = "false"; +var B1 = "false"; +var C1 = "true"; +var D1 = "true"; +//computed - 2: "value": "A == true" +var A2 = "false"; +var B2 = "false"; +var C2 = "true"; +var D2 = "true"; +//computed - 3: "A == \"true\"" +var A3 = "false"; +var B3 = "false"; +var C3 = "true"; +var D3 = "true"; + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar.cs new file mode 100644 index 000000000000..d67218d16166 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar.cs @@ -0,0 +1,24 @@ +//CPP processing +//Values: +var A = False; +var B = False; +var C = True; +var D = True; + +// if (A) syntax +var A = "false"; +var B = "false"; +var C = "true"; +var D = "true"; + +// if (A == true) syntax +var A = "false"; +var B = "false"; +var C = "true"; +var D = "true"; + +// if (A == "true") syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar.props b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar.props new file mode 100644 index 000000000000..e1fbbb3f537b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar.props @@ -0,0 +1,35 @@ + + +False +False +True +True + + +C-true +D-true + +C-true +D-true + +C-true +D-true + + + A-false +B-false +C-true +D-true + + +A-false +B-false +C-true +D-true + + + +A-false +B-false +C-false +D-false diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar.vb b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar.vb new file mode 100644 index 000000000000..7b24fba4daeb --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.no options.verified/TestAssets.TemplateWithBooleanParameters/bar.vb @@ -0,0 +1,25 @@ +'VB processing +'Values: +var A = False; +var B = False; +var C = True; +var D = True; + + +'( A ) syntax +var A = "false"; +var B = "false"; +var C = "true"; +var D = "true"; + +'( A == true) syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; + +'( A == "true") syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs new file mode 100644 index 000000000000..d019f708a208 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs @@ -0,0 +1,24 @@ +//CPP2 processing +//Values: +var A = False; +var B = False; +var C = False; +var D = False; + +//computed - 1: "value": "A" +var A1 = "false"; +var B1 = "false"; +var C1 = "false"; +var D1 = "false"; +//computed - 2: "value": "A == true" +var A2 = "false"; +var B2 = "false"; +var C2 = "false"; +var D2 = "false"; +//computed - 3: "A == \"true\"" +var A3 = "false"; +var B3 = "false"; +var C3 = "false"; +var D3 = "false"; + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar.cs new file mode 100644 index 000000000000..57fe7f2f1d5b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar.cs @@ -0,0 +1,24 @@ +//CPP processing +//Values: +var A = False; +var B = False; +var C = False; +var D = False; + +// if (A) syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; + +// if (A == true) syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; + +// if (A == "true") syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar.props b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar.props new file mode 100644 index 000000000000..79c46fafe974 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar.props @@ -0,0 +1,29 @@ + + +False +False +False +False + + + + + + + A-false +B-false +C-false +D-false + + +A-false +B-false +C-false +D-false + + + +A-false +B-false +C-false +D-false diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar.vb b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar.vb new file mode 100644 index 000000000000..fcb1c04985a3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to false.verified/TestAssets.TemplateWithBooleanParameters/bar.vb @@ -0,0 +1,25 @@ +'VB processing +'Values: +var A = False; +var B = False; +var C = False; +var D = False; + + +'( A ) syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; + +'( A == true) syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; + +'( A == "true") syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs new file mode 100644 index 000000000000..afdf8e1ec5fd --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs @@ -0,0 +1,24 @@ +//CPP2 processing +//Values: +var A = True; +var B = True; +var C = True; +var D = True; + +//computed - 1: "value": "A" +var A1 = "true"; +var B1 = "true"; +var C1 = "true"; +var D1 = "true"; +//computed - 2: "value": "A == true" +var A2 = "true"; +var B2 = "true"; +var C2 = "true"; +var D2 = "true"; +//computed - 3: "A == \"true\"" +var A3 = "true"; +var B3 = "true"; +var C3 = "true"; +var D3 = "true"; + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar.cs new file mode 100644 index 000000000000..f545668a110e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar.cs @@ -0,0 +1,24 @@ +//CPP processing +//Values: +var A = True; +var B = True; +var C = True; +var D = True; + +// if (A) syntax +var A = "true"; +var B = "true"; +var C = "true"; +var D = "true"; + +// if (A == true) syntax +var A = "true"; +var B = "true"; +var C = "true"; +var D = "true"; + +// if (A == "true") syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar.props b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar.props new file mode 100644 index 000000000000..8b7223bb09ff --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar.props @@ -0,0 +1,41 @@ + + +True +True +True +True + + +A-true +B-true +C-true +D-true + +A-true +B-true +C-true +D-true + +A-true +B-true +C-true +D-true + + + A-true +B-true +C-true +D-true + + +A-true +B-true +C-true +D-true + + + +A-false +B-false +C-false +D-false diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar.vb b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar.vb new file mode 100644 index 000000000000..4983464c5a75 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.option equals to true.verified/TestAssets.TemplateWithBooleanParameters/bar.vb @@ -0,0 +1,25 @@ +'VB processing +'Values: +var A = True; +var B = True; +var C = True; +var D = True; + + +'( A ) syntax +var A = "true"; +var B = "true"; +var C = "true"; +var D = "true"; + +'( A == true) syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; + +'( A == "true") syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs new file mode 100644 index 000000000000..30e0515a4713 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar-computed.cs @@ -0,0 +1,24 @@ +//CPP2 processing +//Values: +var A = True; +var B = True; +var C = True; +var D = False; + +//computed - 1: "value": "A" +var A1 = "true"; +var B1 = "true"; +var C1 = "true"; +var D1 = "false"; +//computed - 2: "value": "A == true" +var A2 = "true"; +var B2 = "true"; +var C2 = "true"; +var D2 = "false"; +//computed - 3: "A == \"true\"" +var A3 = "true"; +var B3 = "true"; +var C3 = "true"; +var D3 = "false"; + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar.cs new file mode 100644 index 000000000000..33fe2b2b7216 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar.cs @@ -0,0 +1,24 @@ +//CPP processing +//Values: +var A = True; +var B = True; +var C = True; +var D = False; + +// if (A) syntax +var A = "true"; +var B = "true"; +var C = "true"; +var D = "false"; + +// if (A == true) syntax +var A = "true"; +var B = "true"; +var C = "true"; +var D = "false"; + +// if (A == "true") syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar.props b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar.props new file mode 100644 index 000000000000..e67d61a8ed03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar.props @@ -0,0 +1,38 @@ + + +True +True +True +False + + +A-true +B-true +C-true + +A-true +B-true +C-true + +A-true +B-true +C-true + + + A-true +B-true +C-true +D-false + + +A-true +B-true +C-true +D-false + + + +A-false +B-false +C-false +D-false diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar.vb b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar.vb new file mode 100644 index 000000000000..0add466d9328 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/BooleanConditionsTest.options without value.verified/TestAssets.TemplateWithBooleanParameters/bar.vb @@ -0,0 +1,25 @@ +'VB processing +'Values: +var A = True; +var B = True; +var C = True; +var D = False; + + +'( A ) syntax +var A = "true"; +var B = "true"; +var C = "true"; +var D = "false"; + +'( A == true) syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; + +'( A == "true") syntax +var A = "false"; +var B = "false"; +var C = "false"; +var D = "false"; diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/End2EndTests.SourceNameFormsTest.verified.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/End2EndTests.SourceNameFormsTest.verified.txt new file mode 100644 index 000000000000..ba1ffaa3298c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/End2EndTests.SourceNameFormsTest.verified.txt @@ -0,0 +1,6 @@ +Identity: MyApp.1 +Namespace: MyApp._1 +Class name: MyApp__1 +Namespace (lc): myapp._1 +Class name (lc): myapp__1 +Lowercase: myapp.1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/End2EndTests.ValueForms_DerivedSymbolFromGeneratedSymbolTest.verified.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/End2EndTests.ValueForms_DerivedSymbolFromGeneratedSymbolTest.verified.txt new file mode 100644 index 000000000000..dbf7e3cf0f3e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/End2EndTests.ValueForms_DerivedSymbolFromGeneratedSymbolTest.verified.txt @@ -0,0 +1,3 @@ +Real.Web.App +REAL.WEB.APP +REAL_WEB_APP diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/End2EndTests.ValueForms_DerivedSymbolTest.verified.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/End2EndTests.ValueForms_DerivedSymbolTest.verified.txt new file mode 100644 index 000000000000..dbf7e3cf0f3e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/End2EndTests.ValueForms_DerivedSymbolTest.verified.txt @@ -0,0 +1,3 @@ +Real.Web.App +REAL.WEB.APP +REAL_WEB_APP diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForChoiceParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/ChoiceOtherFile.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForChoiceParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/ChoiceOtherFile.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForChoiceParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/ChoiceOtherFile.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForChoiceParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForChoiceParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs new file mode 100644 index 000000000000..02c394a3a5cf --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForChoiceParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.DefaultIfOptionWithoutValue +{ + public class Program + { + // User specified comment: RegularDefaultString + public void Main() + { + Console.WriteLine("Template created with MyChoice = NoValueDefaultChoice"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForStringParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForStringParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs new file mode 100644 index 000000000000..7da7a0a9e0a2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForStringParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.DefaultIfOptionWithoutValue +{ + public class Program + { + // User specified comment: NoValueDefaultString + public void Main() + { + Console.WriteLine("Template created with MyChoice = RegularDefaultChoice"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForStringParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/StringOtherFile.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForStringParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/StringOtherFile.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultForStringParamIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/StringOtherFile.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefault_UserProvidedValuesAreIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefault_UserProvidedValuesAreIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs new file mode 100644 index 000000000000..38badc27ee9b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefault_UserProvidedValuesAreIsUsed.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.DefaultIfOptionWithoutValue +{ + public class Program + { + // User specified comment: UserString + public void Main() + { + Console.WriteLine("Template created with MyChoice = OtherChoice"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultsNotUsedIfSwitchesNotSpecified.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultsNotUsedIfSwitchesNotSpecified.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs new file mode 100644 index 000000000000..6fbb0d33c959 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.DefaultIfOptionWithoutValue_NoValueDefaultsNotUsedIfSwitchesNotSpecified.verified/TestAssets.DefaultIfOptionWithoutValue/Program.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.DefaultIfOptionWithoutValue +{ + public class Program + { + // User specified comment: RegularDefaultString + public void Main() + { + Console.WriteLine("Template created with MyChoice = RegularDefaultChoice"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Company.ClassLibrary1.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Company.ClassLibrary1.csproj new file mode 100644 index 000000000000..505cdaabb665 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Company.ClassLibrary1.csproj @@ -0,0 +1,25 @@ + + + + + + + netstandard1.4 + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/RenameBattery/B.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/RenameBattery/B.txt new file mode 100644 index 000000000000..e02abfc9b0e1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/RenameBattery/B.txt @@ -0,0 +1 @@ + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/RenameBattery/D.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/RenameBattery/D.txt new file mode 100644 index 000000000000..e02abfc9b0e1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/RenameBattery/D.txt @@ -0,0 +1 @@ + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Test.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Test.cs new file mode 100644 index 000000000000..4bbde1bb7ed7 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Test.cs @@ -0,0 +1,19 @@ +using MyApp.Test; + +//Stuff +namespace MyApp +{ + public class DefaultTrueIncluded { } + + public class DefaultFalseIncluded { } + +#if DEBUG1 + public class InsideUnknownDirectiveNoEmit { } +#endif + +//-:cnd +#if DEBUG2 + public class InsideUnknownDirectiveEmit { } +#endif +//+:cnd +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Test.css b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Test.css new file mode 100644 index 000000000000..bceb48f31bf1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Test.css @@ -0,0 +1,13 @@ +/*Stuff*/ +a { + DefaultTrueIncluded: 0; + DefaultFalseIncluded: 0; + /*#if (DEBUG1) */ + InsideUnknownDirectiveNoEmit: 0; + /*#endif*/ +/*-:cnd*/ + /*#if (DEBUG2) */ + InsideUnknownDirectiveEmit: 0; + /*#endif*/ +/*+:cnd*/ +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Test.json b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Test.json new file mode 100644 index 000000000000..7296e8bf7a25 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.KitchenSink_ConfigurationKitchenSink.verified/TestAssets.ConfigurationKitchenSink/Test.json @@ -0,0 +1,13 @@ +//Stuff +{ + "DefaultTrueIncluded": 0, + "DefaultFalseIncluded": 0, + //#if DEBUG1 + "InsideUnknownDirectiveNoEmit": 0, + //#endif +//-:cnd + //#if DEBUG2 + "InsideUnknownDirectiveEmit": 0 + //#endif +//+:cnd +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.PortsAndCoalesceRenames.verified/TestAssets.TemplateWithPortsAndCoalesce/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.PortsAndCoalesceRenames.verified/TestAssets.TemplateWithPortsAndCoalesce/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.PortsAndCoalesceRenames.verified/TestAssets.TemplateWithPortsAndCoalesce/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroNegative.verified/TestAssets.TemplateWithRegexMatchMacro/bar.2.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroNegative.verified/TestAssets.TemplateWithRegexMatchMacro/bar.2.cs new file mode 100644 index 000000000000..ee85d0e6c178 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroNegative.verified/TestAssets.TemplateWithRegexMatchMacro/bar.2.cs @@ -0,0 +1 @@ +False diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroNegative.verified/TestAssets.TemplateWithRegexMatchMacro/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroNegative.verified/TestAssets.TemplateWithRegexMatchMacro/bar.cs new file mode 100644 index 000000000000..ee85d0e6c178 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroNegative.verified/TestAssets.TemplateWithRegexMatchMacro/bar.cs @@ -0,0 +1 @@ +False diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroPositive.verified/TestAssets.TemplateWithRegexMatchMacro/bar.2.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroPositive.verified/TestAssets.TemplateWithRegexMatchMacro/bar.2.cs new file mode 100644 index 000000000000..8bc17f2153c4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroPositive.verified/TestAssets.TemplateWithRegexMatchMacro/bar.2.cs @@ -0,0 +1 @@ +True diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroPositive.verified/TestAssets.TemplateWithRegexMatchMacro/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroPositive.verified/TestAssets.TemplateWithRegexMatchMacro/bar.cs new file mode 100644 index 000000000000..8bc17f2153c4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.RegexMatch_RegexMatchMacroPositive.verified/TestAssets.TemplateWithRegexMatchMacro/bar.cs @@ -0,0 +1 @@ +True diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileNorenamepart.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileNorenamepart.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileNorenamepart.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileYesNewName.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileYesNewName.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileYesNewName.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/YesNewName/FileNorenamepart.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/YesNewName/FileNorenamepart.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/YesNewName/FileNorenamepart.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/YesNewName/FileYesNewName.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/YesNewName/FileYesNewName.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CaseSensitiveNameBasedRenames.verified/TestAssets.TemplateWithCaseSensitiveNameBasedRenames/YesNewName/FileYesNewName.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourceAndTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourceAndTargetPaths/Target/Output/bar.name.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourceAndTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourceAndTargetPaths/Target/Output/bar.name.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourceAndTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourceAndTargetPaths/Target/Output/bar.name.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourceAndTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourceAndTargetPaths/Target/Output/bar/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourceAndTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourceAndTargetPaths/Target/Output/bar/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourceAndTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourceAndTargetPaths/Target/Output/bar/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourcePathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourcePath/bar.name.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourcePathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourcePath/bar.name.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourcePathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourcePath/bar.name.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourcePathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourcePath/bar/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourcePathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourcePath/bar/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomSourcePathRename.verified/TestAssets.TemplateWithSourceNameAndCustomSourcePath/bar/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomTargetPath/Custom/Path/bar.name.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomTargetPath/Custom/Path/bar.name.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomTargetPath/Custom/Path/bar.name.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomTargetPath/Custom/Path/bar/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomTargetPath/Custom/Path/bar/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_CustomTargetPathRename.verified/TestAssets.TemplateWithSourceNameAndCustomTargetPath/Custom/Path/bar/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_DerivedSymbolFileRename.verified/TestAssets.TemplateWithDerivedSymbolFileRename/Rename.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_DerivedSymbolFileRename.verified/TestAssets.TemplateWithDerivedSymbolFileRename/Rename.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_DerivedSymbolFileRename.verified/TestAssets.TemplateWithDerivedSymbolFileRename/Rename.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/TESTPROJECT3.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/TESTPROJECT3.cs new file mode 100644 index 000000000000..612586fd6399 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/TESTPROJECT3.cs @@ -0,0 +1,9 @@ +using System; +using System.Text; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.TemplateWithRenames +{ + class MYPROJECT3 + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/TestProject1.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/TestProject1.cs new file mode 100644 index 000000000000..121a3e69517c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/TestProject1.cs @@ -0,0 +1,9 @@ +using System; +using System.Text; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.TemplateWithRenames +{ + class MyProject1 + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/baz.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/baz.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/baz.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/baz/baz.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/baz/baz.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/baz/baz.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/testproject2.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/testproject2.cs new file mode 100644 index 000000000000..24525ffbe444 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/testproject2.cs @@ -0,0 +1,9 @@ +using System; +using System.Text; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.TemplateWithRenames +{ + class myproject2 + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/uc/BAZ.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/uc/BAZ.cs new file mode 100644 index 000000000000..cc57727a2c86 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_FileRenames.verified/TestAssets.TemplateWithRenames/uc/BAZ.cs @@ -0,0 +1,9 @@ +using System; +using System.Text; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.TemplateWithRenames +{ + class bar_uc + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_JoinAndFolderRename.verified/TestAssets.TemplateWithJoinAndFolderRename/Source/Api/Microsoft/Office/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_JoinAndFolderRename.verified/TestAssets.TemplateWithJoinAndFolderRename/Source/Api/Microsoft/Office/bar.cs new file mode 100644 index 000000000000..b3b9332a37ef --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_JoinAndFolderRename.verified/TestAssets.TemplateWithJoinAndFolderRename/Source/Api/Microsoft/Office/bar.cs @@ -0,0 +1 @@ +Content is not relevant \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFile.verified/TestAssets.TemplateWithMultipleRenamesOnSameFile/ballandbase.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFile.verified/TestAssets.TemplateWithMultipleRenamesOnSameFile/ballandbase.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFile.verified/TestAssets.TemplateWithMultipleRenamesOnSameFile/ballandbase.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFile.verified/TestAssets.TemplateWithMultipleRenamesOnSameFile/baseball.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFile.verified/TestAssets.TemplateWithMultipleRenamesOnSameFile/baseball.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFile.verified/TestAssets.TemplateWithMultipleRenamesOnSameFile/baseball.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFileHandlesInducedOverlap.verified/TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap/bar.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFileHandlesInducedOverlap.verified/TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap/bar.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFileHandlesInducedOverlap.verified/TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap/bar.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFileHandlesOverlap.verified/TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesOverlap/pinb.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFileHandlesOverlap.verified/TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesOverlap/pinb.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_MultipleRenamesOnSameFileHandlesOverlap.verified/TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesOverlap/pinb.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_NegativeFileRenames.verified/TestAssets.TemplateWithUnspecifiedSourceName/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_NegativeFileRenames.verified/TestAssets.TemplateWithUnspecifiedSourceName/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_NegativeFileRenames.verified/TestAssets.TemplateWithUnspecifiedSourceName/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_NegativeFileRenames.verified/TestAssets.TemplateWithUnspecifiedSourceName/bar/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_NegativeFileRenames.verified/TestAssets.TemplateWithUnspecifiedSourceName/bar/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_NegativeFileRenames.verified/TestAssets.TemplateWithUnspecifiedSourceName/bar/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourceNameFileRenames.verified/TestAssets.TemplateWithSourceName/baz.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourceNameFileRenames.verified/TestAssets.TemplateWithSourceName/baz.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourceNameFileRenames.verified/TestAssets.TemplateWithSourceName/baz.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourceNameFileRenames.verified/TestAssets.TemplateWithSourceName/baz/baz.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourceNameFileRenames.verified/TestAssets.TemplateWithSourceName/baz/baz.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourceNameFileRenames.verified/TestAssets.TemplateWithSourceName/baz/baz.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourceNameInTargetPathGetsRenamed.verified/TestAssets.TemplateWithSourceNameInTargetPathGetsRenamed/bar/baz/baz.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourceNameInTargetPathGetsRenamed.verified/TestAssets.TemplateWithSourceNameInTargetPathGetsRenamed/bar/baz/baz.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourceNameInTargetPathGetsRenamed.verified/TestAssets.TemplateWithSourceNameInTargetPathGetsRenamed/bar/baz/baz.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourcePathOutsideConfigRoot.verified/TestAssets.TemplateWithSourcePathOutsideConfigRoot/blah/MountPointRoot/baz/bar/bar.baz.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourcePathOutsideConfigRoot.verified/TestAssets.TemplateWithSourcePathOutsideConfigRoot/blah/MountPointRoot/baz/bar/bar.baz.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourcePathOutsideConfigRoot.verified/TestAssets.TemplateWithSourcePathOutsideConfigRoot/blah/MountPointRoot/baz/bar/bar.baz.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourcePathOutsideConfigRoot.verified/TestAssets.TemplateWithSourcePathOutsideConfigRoot/blah/MountPointRoot/baz/baz.baz.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourcePathOutsideConfigRoot.verified/TestAssets.TemplateWithSourcePathOutsideConfigRoot/blah/MountPointRoot/baz/baz.baz.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourcePathOutsideConfigRoot.verified/TestAssets.TemplateWithSourcePathOutsideConfigRoot/blah/MountPointRoot/baz/baz.baz.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourcePathOutsideConfigRoot.verified/TestAssets.TemplateWithSourcePathOutsideConfigRoot/blah/MountPointRoot/mount.baz.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourcePathOutsideConfigRoot.verified/TestAssets.TemplateWithSourcePathOutsideConfigRoot/blah/MountPointRoot/mount.baz.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.Renames_SourcePathOutsideConfigRoot.verified/TestAssets.TemplateWithSourcePathOutsideConfigRoot/blah/MountPointRoot/mount.baz.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.TemplateWithTagsBasicTest.verified/TestAssets.TemplateWithTags/TestAssets.TemplateWithTags.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.TemplateWithTagsBasicTest.verified/TestAssets.TemplateWithTags/TestAssets.TemplateWithTags.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.TemplateWithTagsBasicTest.verified/TestAssets.TemplateWithTags/TestAssets.TemplateWithTags.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/FirstLetterCaseForms/MyCamelTestValue.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/FirstLetterCaseForms/MyCamelTestValue.cs new file mode 100644 index 000000000000..282cf1244d5d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/FirstLetterCaseForms/MyCamelTestValue.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string MyCamelTestValue; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/FirstLetterCaseForms/myPascalTestValue.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/FirstLetterCaseForms/myPascalTestValue.cs new file mode 100644 index 000000000000..cbdecc6a3753 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/FirstLetterCaseForms/myPascalTestValue.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string myPascalTestValue; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/IdentityForms/MyPascalTestValue.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/IdentityForms/MyPascalTestValue.cs new file mode 100644 index 000000000000..631213b52c15 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/IdentityForms/MyPascalTestValue.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string MyPascalTestValue; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/IdentityForms/my test text.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/IdentityForms/my test text.cs new file mode 100644 index 000000000000..ca1af408a3bf --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/IdentityForms/my test text.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string param3 = "my test text"; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/IdentityForms/myCamelTestValue.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/IdentityForms/myCamelTestValue.cs new file mode 100644 index 000000000000..acb308b31990 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/IdentityForms/myCamelTestValue.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string myCamelTestValue; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/KebabCaseForms/my-pascal-test-value.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/KebabCaseForms/my-pascal-test-value.cs new file mode 100644 index 000000000000..7adf3731f417 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/KebabCaseForms/my-pascal-test-value.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string my-pascal-test-value; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/Test.Value6.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/Test.Value6.cs new file mode 100644 index 000000000000..849b70f544d6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/Test.Value6.cs @@ -0,0 +1,3 @@ +Test.Value6 +Test_Value! +Test?Value! \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/TitleCaseForms/My Test Text.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/TitleCaseForms/My Test Text.cs new file mode 100644 index 000000000000..c9616d438e0c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/TitleCaseForms/My Test Text.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string param3 = "My Test Text"; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/bar.cs new file mode 100644 index 000000000000..849b70f544d6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms.verified/TestAssets.TemplateWithValueForms/bar.cs @@ -0,0 +1,3 @@ +Test.Value6 +Test_Value! +Test?Value! \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms_DerivedSymbolWithValueForms.verified/TestAssets.TemplateWithDerivedSymbolWithValueForms/AppSeven.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms_DerivedSymbolWithValueForms.verified/TestAssets.TemplateWithDerivedSymbolWithValueForms/AppSeven.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms_DerivedSymbolWithValueForms.verified/TestAssets.TemplateWithDerivedSymbolWithValueForms/AppSeven.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms_DerivedSymbolWithValueForms.verified/TestAssets.TemplateWithDerivedSymbolWithValueForms/ContentTest.txt b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms_DerivedSymbolWithValueForms.verified/TestAssets.TemplateWithDerivedSymbolWithValueForms/ContentTest.txt new file mode 100644 index 000000000000..49a7eb0a396f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/LegacyTests.ValueForms_DerivedSymbolWithValueForms.verified/TestAssets.TemplateWithDerivedSymbolWithValueForms/ContentTest.txt @@ -0,0 +1,2 @@ +AppSeven +Zpp$even diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/PreferDefaultNameTest.Basic.verified/TestAssets.TemplateWithPreferDefaultName/TestAssets.TemplateWithPreferDefaultName.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/PreferDefaultNameTest.Basic.verified/TestAssets.TemplateWithPreferDefaultName/TestAssets.TemplateWithPreferDefaultName.cs new file mode 100644 index 000000000000..bc19d484baea --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/PreferDefaultNameTest.Basic.verified/TestAssets.TemplateWithPreferDefaultName/TestAssets.TemplateWithPreferDefaultName.cs @@ -0,0 +1,3 @@ +using System; + +Console.log("Hello there! This is a test"); diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/TemplateWithOnlyIfStatementTest.Basic.verified/TestAssets.TemplateWithOnlyIfStatement/test.json b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/TemplateWithOnlyIfStatementTest.Basic.verified/TestAssets.TemplateWithOnlyIfStatement/test.json new file mode 100644 index 000000000000..b82da4243574 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/TemplateWithOnlyIfStatementTest.Basic.verified/TestAssets.TemplateWithOnlyIfStatement/test.json @@ -0,0 +1,10 @@ +{ + "hostingManifest": { + "defaultPort": 3332, + "isPublic": false + }, + "hostingManifest_2": { + "dPort": 12345, + "isPublic": true + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/TemplateWithOnlyIfStatementTestForLocalhostTest.Basic.verified/TestAssets.TemplateWithOnlyIfForLocalhost/test.json b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/TemplateWithOnlyIfStatementTestForLocalhostTest.Basic.verified/TestAssets.TemplateWithOnlyIfForLocalhost/test.json new file mode 100644 index 000000000000..374e765a9a8d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Approvals/TemplateWithOnlyIfStatementTestForLocalhostTest.Basic.verified/TestAssets.TemplateWithOnlyIfForLocalhost/test.json @@ -0,0 +1,8 @@ +{ + "hostingManifest": { + "applicationUrl": "http://localhost:3332" + }, + "hostingManifest2": { + "applicationUrl": "http://check:12345" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/BasicTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/BasicTests.cs new file mode 100644 index 000000000000..377d5c86ecae --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/BasicTests.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.IDE.IntegrationTests.Utils; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests +{ + [Collection("Verify Tests")] + public class BasicTests : BootstrapperTestBase, IClassFixture + { + private readonly PackageManager _packageManager; + + public BasicTests(PackageManager packageManager) + { + _packageManager = packageManager; + } + + [Fact] + internal async Task GetCreationEffects_BasicTest_Folder() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + await InstallTestTemplateAsync(bootstrapper, "TemplateWithSourceName"); + + string output = TestUtils.CreateTemporaryFolder(); + var foundTemplates = await bootstrapper.GetTemplatesAsync( + new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithSourceName") }); + var result = await bootstrapper.GetCreationEffectsAsync(foundTemplates[0].Info, "test", output, new Dictionary()); + Assert.Equal(2, result.CreationEffects?.CreationResult.PrimaryOutputs.Count); + Assert.Equal(0, result.CreationEffects?.CreationResult.PostActions.Count); + Assert.Equal(2, result.CreationEffects?.FileChanges.Count); + + var expectedFileChanges = new FileChange[] + { + new FileChange("bar.cs", "test.cs", ChangeKind.Create), + new FileChange("bar/bar.cs", "test/test.cs", ChangeKind.Create), + }; + IFileChangeComparer comparer = new IFileChangeComparer(); + Assert.NotNull(result.CreationEffects?.FileChanges); + + Assert.Equal( + expectedFileChanges.OrderBy(s => s, comparer), + result.CreationEffects.FileChanges.OrderBy(s => s, comparer), + comparer); + } + + [Fact] + internal async Task Create_BasicTest_Folder() + { + using Bootstrapper bootstrapper = GetBootstrapper(additionalVirtualLocations: new string[] { "test" }); + await InstallTestTemplateAsync(bootstrapper, "TemplateWithSourceName"); + + string output = TestUtils.CreateTemporaryFolder(); + var foundTemplates = await bootstrapper.GetTemplatesAsync( + new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithSourceName") }); + + var result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "test", output, new Dictionary()); + + Assert.Equal(2, result.CreationResult?.PrimaryOutputs.Count); + Assert.Equal(0, result.CreationResult?.PostActions.Count); + Assert.True(File.Exists(Path.Combine(output, "test.cs"))); + Assert.True(File.Exists(Path.Combine(output, "test/test.cs"))); + } + + [Fact] + internal async Task GetCreationEffects_BasicTest_Package() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string packageLocation = await _packageManager.GetNuGetPackage("Microsoft.DotNet.Common.ProjectTemplates.5.0"); + await InstallTemplateAsync(bootstrapper, packageLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("console") }); + var result = await bootstrapper.GetCreationEffectsAsync(foundTemplates[0].Info, "test", output, new Dictionary()); + Assert.Equal(2, result.CreationEffects?.CreationResult.PrimaryOutputs.Count); + Assert.Equal(2, result.CreationEffects?.CreationResult.PostActions.Count); + Assert.Equal(2, result.CreationEffects?.FileChanges.Count); + + var expectedFileChanges = new FileChange[] + { + new FileChange("Company.ConsoleApplication1.csproj", "test.csproj", ChangeKind.Create), + new FileChange("Program.cs", "Program.cs", ChangeKind.Create), + }; + IFileChangeComparer comparer = new IFileChangeComparer(); + Assert.NotNull(result.CreationEffects?.FileChanges); + Assert.Equal( + expectedFileChanges.OrderBy(s => s, comparer), + result.CreationEffects.FileChanges.OrderBy(s => s, comparer), + comparer); + } + + [Fact] + internal async Task Create_BasicTest_Package() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string packageLocation = await _packageManager.GetNuGetPackage("Microsoft.DotNet.Common.ProjectTemplates.5.0"); + await InstallTemplateAsync(bootstrapper, packageLocation); + + string output = TestUtils.CreateTemporaryFolder(); + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("console") }); + var result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "test", output, new Dictionary()); + Assert.Equal(2, result.CreationResult?.PrimaryOutputs.Count); + Assert.Equal(2, result.CreationResult?.PostActions.Count); + + Assert.True(File.Exists(Path.Combine(output, "Program.cs"))); + Assert.True(File.Exists(Path.Combine(output, "test.csproj"))); + } + + [Fact] + internal async Task Create_TemplateWithBinaryFile_Folder() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithBinaryFile"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithBinaryFile") }); + await bootstrapper.CreateAsync(foundTemplates[0].Info, "my-test-folder", output, new Dictionary()); + + string sourceImage = Path.Combine(templateLocation, "image.png"); + string targetImage = Path.Combine(output, "image.png"); + + Assert.True(File.Exists(targetImage)); + + Assert.Equal( + new FileInfo(sourceImage).Length, + new FileInfo(targetImage).Length); + Assert.True(TestUtils.CompareFiles(sourceImage, targetImage), $"The content of {sourceImage} and {targetImage} is not same."); + } + + [Fact] + internal async Task Create_TemplateWithBinaryFile_Package() + { + Console.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription); + using Bootstrapper bootstrapper = GetBootstrapper(); + string packageLocation = PackTestTemplatesNuGetPackage(_packageManager); + await InstallTemplateAsync(bootstrapper, packageLocation); + string templateLocation = GetTestTemplateLocation("TemplateWithBinaryFile"); + + string output = TestUtils.CreateTemporaryFolder(); + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithBinaryFile") }); + await bootstrapper.CreateAsync(foundTemplates[0].Info, "my-test-folder", output, new Dictionary()); + + string sourceImage = Path.Combine(templateLocation, "image.png"); + string targetImage = Path.Combine(output, "image.png"); + + Assert.True(File.Exists(targetImage)); + + Assert.Equal( + new FileInfo(sourceImage).Length, + new FileInfo(targetImage).Length); + Assert.True(TestUtils.CompareFiles(sourceImage, targetImage), $"The content of {sourceImage} and {targetImage} is not same."); + } + + [Fact] + internal async Task GetTemplates_BasicTest() + { + using Bootstrapper bootstrapper = GetBootstrapper(loadTestTemplates: true); + + var result1 = await bootstrapper.GetTemplatesAsync(default); + var result2 = await bootstrapper.GetTemplatesAsync([], cancellationToken: default); + + Assert.NotEmpty(result1); + Assert.Equal(result1.Count, result2.Count); + } + + [Fact] + internal async Task SourceNameForms_BasicTest() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("SourceNameForms"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.SourceNameForms") }); + var result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "MyApp.1", output, new Dictionary()); + + Assert.Equal(Edge.Template.CreationResultStatus.Success, result.Status); + + string targetFile = Path.Combine(output, "myapp.12.cs"); + Assert.True(File.Exists(targetFile)); + string targetFile2 = Path.Combine(output, "MyApp.1.cs"); + Assert.True(File.Exists(targetFile2)); + + await Verify(File.ReadAllText(targetFile2)); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/BootstrapperTestBase.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/BootstrapperTestBase.cs new file mode 100644 index 000000000000..3eccff099dfc --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/BootstrapperTestBase.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Edge; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests +{ + public class BootstrapperTestBase : TestBase + { + private const string HostIdentifier = "IDE.IntegrationTests"; + private const string HostVersion = "v1.0.0"; + + internal static Bootstrapper GetBootstrapper( + IEnumerable? additionalVirtualLocations = null, + bool loadTestTemplates = false, + IEnumerable? addLoggerProviders = null, + string packageJsonContent = "") + { + ITemplateEngineHost host = CreateHost(loadTestTemplates, addLoggerProviders); + if (additionalVirtualLocations != null) + { + foreach (string virtualLocation in additionalVirtualLocations) + { + host.VirtualizeDirectory(virtualLocation); + } + } + + // Need to virtualize paths before mocking a file content. + var bootstrapper = new Bootstrapper(host, virtualizeConfiguration: true, loadDefaultComponents: true); + // Mocks .templateengine\package.json + if (!string.IsNullOrEmpty(packageJsonContent)) + { + var path = Path.Combine(new EngineEnvironmentSettings(host).Paths.GlobalSettingsDir, "packages.json"); + host.FileSystem.WriteAllText( + path.Replace('\\', '/'), + packageJsonContent); + } + + return bootstrapper; + } + + internal static async Task InstallTestTemplateAsync(Bootstrapper bootstrapper, params string[] templates) + { + List installRequests = new List(); + + foreach (string template in templates) + { + string path = GetTestTemplateLocation(template); + installRequests.Add(new InstallRequest(Path.GetFullPath(path))); + } + + IReadOnlyList installationResults = await bootstrapper.InstallTemplatePackagesAsync(installRequests); + if (installationResults.Any(result => !result.Success)) + { + throw new Exception($"Failed to install templates: {string.Join(";", installationResults.Select(result => $"path: {result.InstallRequest.PackageIdentifier}, details:{result.ErrorMessage}"))}"); + } + } + + internal static async Task InstallTemplateAsync(Bootstrapper bootstrapper, params string[] templates) + { + List installRequests = new List(); + foreach (string template in templates) + { + installRequests.Add(new InstallRequest(Path.GetFullPath(template))); + } + + IReadOnlyList installationResults = await bootstrapper.InstallTemplatePackagesAsync(installRequests); + if (installationResults.Any(result => !result.Success)) + { + throw new Exception($"Failed to install templates: {string.Join(";", installationResults.Select(result => $"path: {result.InstallRequest.PackageIdentifier}, details:{result.ErrorMessage}"))}"); + } + } + + private static ITemplateEngineHost CreateHost(bool loadTestTemplates = false, IEnumerable? addLoggerProviders = null) + { + var preferences = new Dictionary + { + { "prefs:language", "C#" } + }; + + var builtIns = new List<(Type, IIdentifiedComponent)>(); + if (loadTestTemplates) + { + builtIns.AddRange(BuiltInTemplatePackagesProviderFactory.GetComponents(TestTemplatesLocation)); + } + + TestLoggerFactory loggerFactory = new(); + addLoggerProviders?.ToList().ForEach(loggerFactory.AddProvider); + + return new DefaultTemplateEngineHost(HostIdentifier + Guid.NewGuid().ToString(), HostVersion, preferences, builtIns, [], loggerFactory); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/ConfigurationTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/ConfigurationTests.cs new file mode 100644 index 000000000000..61ce9af47a40 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/ConfigurationTests.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Edge; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests +{ + public class ConfigurationTests : BootstrapperTestBase + { + [Fact] + internal async Task PhysicalConfigurationTest() + { + var userProfileDir = Environment.GetEnvironmentVariable(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "USERPROFILE" : "HOME"); + Assert.NotNull(userProfileDir); + var hostDir = Path.Combine(userProfileDir!, ".templateengine", nameof(PhysicalConfigurationTest).ToString()); + try + { + var builtIns = BuiltInTemplatePackagesProviderFactory.GetComponents(TestTemplatesLocation); + var host = new DefaultTemplateEngineHost(nameof(PhysicalConfigurationTest).ToString(), "1.0.0", null, builtIns, []); + + Bootstrapper bootstrapper = new Bootstrapper(host, virtualizeConfiguration: false, loadDefaultComponents: true); + var result = await bootstrapper.GetTemplatesAsync(cancellationToken: default); + Assert.True(result.Any()); + bootstrapper.Dispose(); + Assert.True(Directory.Exists(hostDir)); + Assert.True(Directory.Exists(Path.Combine(hostDir, "1.0.0"))); + Assert.True(File.Exists(Path.Combine(hostDir, "1.0.0", "templatecache.json"))); + } + finally + { + Directory.Delete(hostDir, true); + } + } + + [Fact] + internal async Task VirtualConfigurationTest() + { + string? userProfileDir = Environment.GetEnvironmentVariable(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "USERPROFILE" : "HOME"); + Assert.NotNull(userProfileDir); + + string baseDir = Path.Combine(userProfileDir!, ".templateengine"); + var hostDir = Path.Combine(baseDir, nameof(VirtualConfigurationTest).ToString()); + + var builtIns = BuiltInTemplatePackagesProviderFactory.GetComponents(TestTemplatesLocation); + var host = new DefaultTemplateEngineHost(nameof(VirtualConfigurationTest).ToString(), "1.0.0", null, builtIns, []); + + Bootstrapper bootstrapper = new Bootstrapper(host, virtualizeConfiguration: true, loadDefaultComponents: true); + var result = await bootstrapper.GetTemplatesAsync(cancellationToken: default); + Assert.True(result.Any()); + + DateTime? packagesJsonModificationTime = null; + if (File.Exists(Path.Combine(baseDir, "packages.json"))) + { + packagesJsonModificationTime = File.GetLastWriteTimeUtc(Path.Combine(baseDir, "packages.json")); + } + + InstallRequest installRequest = new InstallRequest("Microsoft.DotNet.Web.ProjectTemplates.5.0", "5.0.0"); + IReadOnlyList installResult = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(installResult); + Assert.True(installResult[0].Success); + bootstrapper.Dispose(); + + if (packagesJsonModificationTime == null) + { + Assert.False(File.Exists(Path.Combine(baseDir, "packages.json"))); + } + else + { + Assert.Equal(packagesJsonModificationTime, File.GetLastWriteTimeUtc(Path.Combine(baseDir, "packages.json"))); + } + + Assert.False(Directory.Exists(hostDir)); + } + + [Fact] + internal async Task PhysicalConfigurationTest_WithChangedHostLocation() + { + var userProfileDir = Environment.GetEnvironmentVariable(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "USERPROFILE" : "HOME"); + Assert.NotNull(userProfileDir); + var unexpectedHostDir = Path.Combine(userProfileDir!, ".templateengine", nameof(PhysicalConfigurationTest_WithChangedHostLocation).ToString()); + var expectedHostDir = TestUtils.CreateTemporaryFolder(); + + var builtIns = BuiltInTemplatePackagesProviderFactory.GetComponents(TestTemplatesLocation); + var host = new DefaultTemplateEngineHost(nameof(PhysicalConfigurationTest_WithChangedHostLocation).ToString(), "1.0.0", null, builtIns, []); + + Bootstrapper bootstrapper = new Bootstrapper(host, virtualizeConfiguration: false, loadDefaultComponents: true, hostSettingsLocation: expectedHostDir); + var result = await bootstrapper.GetTemplatesAsync(cancellationToken: default); + Assert.True(result.Any()); + bootstrapper.Dispose(); + var hostDir = Path.Combine(expectedHostDir, nameof(PhysicalConfigurationTest_WithChangedHostLocation).ToString()); + Assert.True(Directory.Exists(hostDir)); + Assert.True(Directory.Exists(Path.Combine(hostDir, "1.0.0"))); + Assert.True(File.Exists(Path.Combine(hostDir, "1.0.0", "templatecache.json"))); + + Assert.False(Directory.Exists(unexpectedHostDir)); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/End2EndTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/End2EndTests.cs new file mode 100644 index 000000000000..9072b12a3ad8 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/End2EndTests.cs @@ -0,0 +1,585 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.RegularExpressions; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Edge.Template; +using Microsoft.TemplateEngine.TestHelper; +using ITemplateMatchInfo = Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo; +using WellKnownSearchFilters = Microsoft.TemplateEngine.Utils.WellKnownSearchFilters; + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests +{ + [Collection("Verify Tests")] + public class End2EndTests : BootstrapperTestBase + { + [Fact] + internal async Task SourceNameFormsTest() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("SourceNameForms"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.SourceNameForms") }); + var result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "MyApp.1", output, new Dictionary()); + + Assert.Equal(CreationResultStatus.Success, result.Status); + + string targetFile = Path.Combine(output, "myapp.12.cs"); + Assert.True(File.Exists(targetFile)); + string targetFile2 = Path.Combine(output, "MyApp.1.cs"); + Assert.True(File.Exists(targetFile2)); + + await Verify(File.ReadAllText(targetFile2)); + } + + [Fact] + internal async Task ValueForms_DerivedSymbolTest() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("ValueForms/DerivedSymbol"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.ValueForms.DerivedSymbol") }); + var result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "Real.Web.App", output, new Dictionary()); + + Assert.Equal(CreationResultStatus.Success, result.Status); + + string targetFile = Path.Combine(output, "Real.Web.App.txt"); + Assert.True(File.Exists(targetFile)); + + await Verify(File.ReadAllText(targetFile)); + } + +#pragma warning disable xUnit1004 // Test methods should not be skipped + [Fact(Skip = "https://github.com/dotnet/templating/issues/5115")] +#pragma warning restore xUnit1004 // Test methods should not be skipped + internal async Task ValueForms_DerivedSymbolFromGeneratedSymbolTest() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("ValueForms/DerivedSymbolFromGeneratedSymbol"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.ValueForms.DerivedSymbolFromGeneratedSymbol") }); + var result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "Real.Web.App", output, new Dictionary()); + + Assert.Equal(CreationResultStatus.Success, result.Status); + + string targetFile = Path.Combine(output, "Real.Web.App.txt"); + Assert.True(File.Exists(targetFile)); + + await Verify(File.ReadAllText(targetFile)); + } + + [Fact] + internal async Task PortAndCoalesceTest_WithFallbackInput() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithPortsAndCoalesce"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + IReadOnlyList foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithPortsAndCoalesce") }); + ITemplateCreationResult result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "test-template", output, new Dictionary()); + + Assert.Equal(CreationResultStatus.Success, result.Status); + + string targetFile = Path.Combine(output, "bar.cs"); + Assert.True(File.Exists(targetFile)); + string fileContent = File.ReadAllText(targetFile); + Assert.True(Regex.Match( + fileContent, + """ + The port is (\d{4,5}) + The port is (\d{4,5}) + + """).Success); + Assert.NotEqual( + """ + The port is 1234 + The port is 1235 + + """, + fileContent); + } + + [Fact] + internal async Task PortAndCoalesceTest_WithUserInput() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithPortsAndCoalesce"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + Dictionary parameters = new() + { + { "userPort1", "4000" }, + { "userPort2", "3000" }, + }; + + IReadOnlyList foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithPortsAndCoalesce") }); + ITemplateCreationResult result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "test-template", output, parameters); + + Assert.Equal(CreationResultStatus.Success, result.Status); + + string targetFile = Path.Combine(output, "bar.cs"); + Assert.True(File.Exists(targetFile)); + string fileContent = File.ReadAllText(targetFile); + Assert.Equal( + """ + The port is 4000 + The port is 3000 + + """, + fileContent); + } + + [Fact] + internal async Task PortAndCoalesceTest_WithUserInputEqualToDefaults() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithPortsAndCoalesce"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + Dictionary parameters = new() + { + { "userPort1", "0" }, + { "userPort2", "0" }, + }; + + IReadOnlyList foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithPortsAndCoalesce") }); + ITemplateCreationResult result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "test-template", output, parameters); + + Assert.Equal(CreationResultStatus.Success, result.Status); + + string targetFile = Path.Combine(output, "bar.cs"); + Assert.True(File.Exists(targetFile)); + string fileContent = File.ReadAllText(targetFile); + Assert.Equal( + """ + The port is 0 + The port is 0 + + """, + fileContent); + } + + [Fact] + internal async Task StringCoalesceTest_WithFallbackInput() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithStringCoalesce"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + IReadOnlyList foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithStringCoalesce") }); + ITemplateCreationResult result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "test-template", output, new Dictionary()); + + Assert.Equal(CreationResultStatus.Success, result.Status); + + string targetFile = Path.Combine(output, "bar.cs"); + Assert.True(File.Exists(targetFile)); + string fileContent = File.ReadAllText(targetFile); + Assert.Equal( + """ + var str = "fallback"; + + """, + fileContent); + } + + [Fact] + internal async Task StringCoalesceTest_WithUserInput() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithStringCoalesce"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + Dictionary parameters = new() + { + { "userVal", "myVal" }, + }; + + IReadOnlyList foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithStringCoalesce") }); + ITemplateCreationResult result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "test-template", output, parameters); + + Assert.Equal(CreationResultStatus.Success, result.Status); + + string targetFile = Path.Combine(output, "bar.cs"); + Assert.True(File.Exists(targetFile)); + string fileContent = File.ReadAllText(targetFile); + Assert.Equal( + """ + var str = "myVal"; + + """, + fileContent); + } + + [Fact] + internal async Task StringCoalesceTest_WithEmptyUserInput() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithStringCoalesce"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); +#pragma warning disable SA1122 // Use string.Empty for empty strings + Dictionary parameters = new() + { + { "userVal", "" }, + }; +#pragma warning restore SA1122 // Use string.Empty for empty strings + + IReadOnlyList foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithStringCoalesce") }); + ITemplateCreationResult result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "test-template", output, parameters); + + Assert.Equal(CreationResultStatus.Success, result.Status); + + string targetFile = Path.Combine(output, "bar.cs"); + Assert.True(File.Exists(targetFile)); + string fileContent = File.ReadAllText(targetFile); + Assert.Equal( + """ + var str = "fallback"; + + """, + fileContent); + } + + [Fact] + internal async Task StringCoalesceTest_WithNullUserInput() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithStringCoalesce"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + Dictionary parameters = new() + { + { "userVal", null }, + }; + + IReadOnlyList foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithStringCoalesce") }); + ITemplateCreationResult result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "test-template", output, parameters); + + Assert.Equal(CreationResultStatus.Success, result.Status); + + string targetFile = Path.Combine(output, "bar.cs"); + Assert.True(File.Exists(targetFile)); + string fileContent = File.ReadAllText(targetFile); + Assert.Equal( + """ + var str = "A"; + + """, + fileContent); + } + + [Fact] + internal async Task Test_CreateAsync_OnInvalidParamsPassed() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithPortsAndCoalesce"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + IReadOnlyList foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithPortsAndCoalesce") }); + + Dictionary parameters = new() + { + { "userPort1", "non-int" }, + { "userPort2", string.Empty } + }; + + ITemplateCreationResult result = await bootstrapper.CreateAsync(foundTemplates[0].Info, "Test", output, parameters); + + Assert.Equal(CreationResultStatus.InvalidParamValues, result.Status); + Assert.Equal("userPort1, userPort2", result.ErrorMessage); + } + + [Fact] + internal async Task Test_DryRunAsync_OnInvalidParamsPassed() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithPortsAndCoalesce"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + IReadOnlyList foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithPortsAndCoalesce") }); + + Dictionary parameters = new() + { + { "userPort1", "non-int" }, + { "userPort2", string.Empty } + }; + + ITemplateCreationResult result = await bootstrapper.GetCreationEffectsAsync(foundTemplates[0].Info, "Test", output, parameters); + + Assert.Equal(CreationResultStatus.InvalidParamValues, result.Status); + Assert.Equal("userPort1, userPort2", result.ErrorMessage); + } + + [Fact] + internal async Task Test_CreateAsync_OnTemplateWithConditions() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithConditions"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + IReadOnlyList foundTemplates = await bootstrapper + .GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithConditions") }); + + Dictionary parameters = new() + { + { "B", "true" }, + }; + + ITemplateCreationResult result = await bootstrapper + .CreateAsync(foundTemplates[0].Info, "Test", output, parameters); + + Assert.Equal(CreationResultStatus.Success, result.Status); + + foreach (var expectResult in ExpectedOutputWithConditions()) + { + string targetFile = Path.Combine(output, expectResult.Key); + Assert.True(File.Exists(targetFile)); + Assert.Equal(expectResult.Value.UnixifyLineBreaks(), File.ReadAllText(targetFile).UnixifyLineBreaks()); + } + } + + [Theory] + [InlineData(null, "theDefaultName.cs")] + [InlineData("fileName", "fileName.cs")] + internal async Task Test_CreateAsync_PreferDefaultNameValidParameters(string? name, string expectedFileName) + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithPreferDefaultName"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + IReadOnlyList foundTemplates = await bootstrapper + .GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithPreferDefaultName") }); + + // Using this parameter with no real info so bootstrapper.CreateAsync is not an ambiguous call + Dictionary parameters = new() + { + { "some", "parameter" }, + }; + + ITemplateCreationResult result = await bootstrapper + .CreateAsync(foundTemplates[0].Info, name, output, parameters); + + Assert.Equal(CreationResultStatus.Success, result.Status); + string expectedName = Path.Combine(output, expectedFileName); + Assert.True(File.Exists(expectedName)); + } + + [Fact] + internal async Task Test_CreateAsync_PreferDefaultNameInvalidParameters() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithPreferDefaultNameButNoDefaultName"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + + IReadOnlyList foundTemplates = await bootstrapper + .GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.TemplateWithPreferDefaultName") }); + + // Using this parameter with no real info so bootstrapper.CreateAsync is not an ambiguous call + Dictionary parameters = new() + { + { "some", "parameter" }, + }; + + ITemplateCreationResult result = await bootstrapper + .CreateAsync(foundTemplates[0].Info, null, output, parameters); + + Assert.Equal(CreationResultStatus.TemplateIssueDetected, result.Status); + Assert.Equal( + "Failed to create template: the template name is not specified. Template configuration does not configure a default name that can be used when name is not specified. Specify the name for the template when instantiating or configure a default name in the template configuration.", + result.ErrorMessage); + } + + [Fact] + internal async Task PostAction_WithFileRename_Test() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("PostActions/WithFileRename"); + await InstallTemplateAsync(bootstrapper, templateLocation); + + string output = TestUtils.CreateTemporaryFolder(); + IReadOnlyList foundTemplates = await bootstrapper + .GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter("TestAssets.PostActions.AddJsonProperty.WithSourceNameChangeInJson") }); + + // Using this parameter with no real info so bootstrapper.CreateAsync is not an ambiguous call + Dictionary parameters = new(); + + ITemplateCreationResult result = await bootstrapper + .CreateAsync(foundTemplates[0].Info, "CompanyProject", output, parameters); + + IPostAction postAction = Assert.Single(result.CreationResult!.PostActions); + Assert.Equal("testfile.json", postAction.Args["jsonFileName"]); + Assert.Equal("moduleConfiguration:edgeAgent:properties.desired:modules", postAction.Args["parentPropertyPath"]); + Assert.Equal("CompanyProject", postAction.Args["newJsonPropertyName"]); + Assert.Equal("${MODULEDIR<../CompanyProject>}", postAction.Args["newJsonPropertyValue"]); + Assert.Equal("Add CompanyProject property to testfile.json manually.", postAction.ManualInstructions); + } + + private Dictionary ExpectedOutputWithConditions() + { + var expects = new Dictionary + { + { +".dockerignore", +@"# comment bar +bar +baz +" + }, + { +".editorconfig", +@"# comment bar +bar +baz +" + }, + { +".gitattributes", +@"# comment bar +bar +baz +" + }, + { +".gitignore", +@"# comment bar +bar +baz +" + }, + { +"Dockerfile", +@"# comment bar +bar +baz +" + }, + { +"nuget.config", +@" +bar +baz +" + }, + { +"Package.appxmanifest", +@" + + + + + + + UWP App Example + Microsoft Corporation + Assets\StoreLogo-sdk.png + + + + + + + + + + + + + + + + + +" + }, + { +"test.axaml", +@" + + + + + + +" + }, + { +"test.cake", +@"// comment bar +bar +baz +" + }, + { +"test.md", +@" +bar +baz +" + }, + { +"test.ps1", +@"# comment B true +B true +common text +" + }, + { +"test.sln", +@"# comment bar +bar +baz +" + }, + { +"test.slnx", +@" +bar +baz +" + }, + { +"test.yaml", +@"# comment bar +bar +baz +" + } + }; + return expects; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/FileRenameTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/FileRenameTests.cs new file mode 100644 index 000000000000..4b6999b6cff3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/FileRenameTests.cs @@ -0,0 +1,356 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.IDE.IntegrationTests.Utils; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests +{ + public class FileRenameTests : BootstrapperTestBase, IClassFixture + { + private readonly PackageManager _packageManager; + + public FileRenameTests(PackageManager packageManager) + { + _packageManager = packageManager; + } + + public static IEnumerable Get_FileRename_TestData() + { + yield return new object[] + { + "TemplateWithRenames", + "--foo baz --testForms TestProject", + new MockCreationEffects() + .WithPrimaryOutputs("TestProject1.cs", "testproject2.cs", "TESTPROJECT3.cs", "baz.cs", "BAZ.cs") + .WithFileChange(new MockFileChange("bar/bar.cs", "baz/baz.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("bar.cs", "baz.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("uc/bar_uc.cs", "uc/BAZ.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("MyProject1.cs", "TestProject1.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("myproject2.cs", "testproject2.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("MYPROJECT3.cs", "TESTPROJECT3.cs", ChangeKind.Create)) + .Without("bar.cs", "bar/bar.cs", "uc/bar_uc.cs", "MyProject1.cs", "myproject2.cs", "MYPROJECT3.cs") + }; + + yield return new object[] + { + "TemplateWithSourceName", + "--name baz", + new MockCreationEffects() + .WithPrimaryOutputs("baz.cs", "baz/baz.cs") + .WithFileChange(new MockFileChange("bar/bar.cs", "baz/baz.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("bar.cs", "baz.cs", ChangeKind.Create)) + .Without("bar.cs", "bar/bar.cs") + }; + + yield return new object[] + { + "TemplateWithUnspecifiedSourceName", + "--name baz", + new MockCreationEffects() + .WithPrimaryOutputs("bar.cs", "bar/bar.cs") + .WithFileChange(new MockFileChange("bar/bar.cs", "bar/bar.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("bar.cs", "bar.cs", ChangeKind.Create)) + .Without("baz.cs", "baz/baz.cs") + }; + + //tests are not working due to bugs: + // -file changes are not taking into account source modifiers https://github.com/dotnet/templating/issues/2746 + yield return new object[] + { + "TemplateWithSourceNameAndCustomSourcePath", + "--name bar", + new MockCreationEffects() + .WithPrimaryOutputs("bar.name.txt", "bar/bar.cs") + .WithFileChange(new MockFileChange("Custom/Path/foo/foo.cs", "bar/bar.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("Custom/Path/foo.name.txt", "bar.name.txt", ChangeKind.Create)) + .Without("Custom/Path/") + }; + + yield return new object[] + { + "TemplateWithSourceNameAndCustomTargetPath", + "--name bar", + new MockCreationEffects() + .WithPrimaryOutputs("Custom/Path/bar.name.txt", "Custom/Path/bar/bar.cs") + .WithFileChange(new MockFileChange("foo/foo.cs", "Custom/Path/bar/bar.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("foo.name.txt", "Custom/Path/bar.name.txt", ChangeKind.Create)) + .Without("foo.name.txt", "foo/") + }; + + yield return new object[] + { + "TemplateWithSourceNameAndCustomSourceAndTargetPaths", + "--name bar", + new MockCreationEffects() + .WithPrimaryOutputs("Target/Output/bar/bar.cs", "Target/Output/bar.name.txt") + .WithFileChange(new MockFileChange("Src/Custom/Path/foo/foo.cs", "Target/Output/bar/bar.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("Src/Custom/Path/foo.name.txt", "Target/Output/bar.name.txt", ChangeKind.Create)) + .Without("Src/Custom/Path/") + }; + + yield return new object[] + { + "TemplateWithSourcePathOutsideConfigRoot", + "--name baz", + new MockCreationEffects() + .WithPrimaryOutputs("blah/MountPointRoot/mount.baz.cs", "blah/MountPointRoot/baz/baz.baz.cs", "blah/MountPointRoot/baz/bar/bar.baz.cs") + .WithFileChange(new MockFileChange("../../../MountPointRoot/mount.foo.cs", "blah/MountPointRoot/mount.baz.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("../../../MountPointRoot/foo/foo.foo.cs", "blah/MountPointRoot/baz/baz.baz.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("../../../MountPointRoot/foo/bar/bar.foo.cs", "blah/MountPointRoot/baz/bar/bar.baz.cs", ChangeKind.Create)) + .Without("MountPointRoot/") + }; + + yield return new object[] + { + "TemplateWithSourceNameInTargetPathGetsRenamed", + "--name baz", + new MockCreationEffects() + .WithPrimaryOutputs("bar/baz/baz.cs") + .WithFileChange(new MockFileChange("foo.cs", "bar/baz/baz.cs", ChangeKind.Create)) + .Without("bar/foo/") + }; + + yield return new object[] + { + "TemplateWithDerivedSymbolFileRename", + "--name Last.Part.Is.For.Rename", + new MockCreationEffects() + .WithPrimaryOutputs("Rename.cs") + .WithFileChange(new MockFileChange("Application1.cs", "Rename.cs", ChangeKind.Create)) + .Without("Application1.cs") + }; + + yield return new object[] + { + "TemplateWithMultipleRenamesOnSameFile", + "--fooRename base --barRename ball", + new MockCreationEffects() + .WithPrimaryOutputs("ballandbase.txt", "baseball.txt") + .WithFileChange(new MockFileChange("foobar.txt", "baseball.txt", ChangeKind.Create)) + .WithFileChange(new MockFileChange("barandfoo.txt", "ballandbase.txt", ChangeKind.Create)) + .Without("foobar.txt", "barfoo.txt") + }; + + yield return new object[] + { + "TemplateWithMultipleRenamesOnSameFileHandlesOverlap", + "--fooRename pin --oobRename ball", + new MockCreationEffects() + .WithPrimaryOutputs("pinb.txt") + .WithFileChange(new MockFileChange("foob.txt", "pinb.txt", ChangeKind.Create)) + .Without("foob.txt", "fball.txt") + }; + + yield return new object[] + { + "TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap", + "--fooRename bar --barRename baz", + new MockCreationEffects() + .WithPrimaryOutputs("bar.txt") + .WithFileChange(new MockFileChange("foo.txt", "bar.txt", ChangeKind.Create)) + .Without("foo.txt", "baz.txt") + }; + + yield return new object[] + { + "TemplateWithCaseSensitiveNameBasedRenames", + "--name NewName", + new MockCreationEffects() + .WithPrimaryOutputs("Norenamepart/FileNorenamepart.txt", "Norenamepart/FileYesNewName.txt", "YesNewName/FileNorenamepart.txt", "YesNewName/FileYesNewName.txt") + .WithFileChange(new MockFileChange("Norenamepart/FileNorenamepart.txt", "Norenamepart/FileNorenamepart.txt", ChangeKind.Create)) + .WithFileChange(new MockFileChange("Norenamepart/FileYesRenamePart.txt", "Norenamepart/FileYesNewName.txt", ChangeKind.Create)) + .WithFileChange(new MockFileChange("YesRenamePart/FileNorenamepart.txt", "YesNewName/FileNorenamepart.txt", ChangeKind.Create)) + .WithFileChange(new MockFileChange("YesRenamePart/FileYesRenamePart.txt", "YesNewName/FileYesNewName.txt", ChangeKind.Create)) + .Without("YesNewName/FileNoNewName.txt", "NoNewName/", "bar_uc.cs", "Norenamepart/FileNoNewName.txt") + }; + + yield return new object[] + { + "TemplateWithJoinAndFolderRename", + "--product Office", + new MockCreationEffects() + .WithPrimaryOutputs("Source/Api/Microsoft/Office/bar.cs") + .WithFileChange(new MockFileChange("Api/bar.cs", "Source/Api/Microsoft/Office/bar.cs", ChangeKind.Create)) + .Without("Api/bar.cs") + }; + + yield return new object[] + { + "TemplateWithSourceBasedRenames", + "--barRename NewName", + new MockCreationEffects() + .WithPrimaryOutputs("baz.cs", "NewName.cs") + .WithFileChange(new MockFileChange("foo.cs", "baz.cs", ChangeKind.Create)) + .WithFileChange(new MockFileChange("foo.cs", "NewName.cs", ChangeKind.Create)) + .Without("foo.cs") + }; + } + + [Theory] + [MemberData(nameof(Get_FileRename_TestData))] + internal async Task GetCreationEffectsTest(string templateName, string parameters, MockCreationEffects expectedResult) + { + using Bootstrapper bootstrapper = GetBootstrapper(); + await InstallTestTemplateAsync(bootstrapper, templateName); + + string name = BasicParametersParser.GetNameFromParameterString(parameters); + string output = BasicParametersParser.GetOutputFromParameterString(parameters); + Dictionary parametersDict = BasicParametersParser.ParseParameterString(parameters); + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter(templateName) }); + ITemplateInfo template = foundTemplates.Single(template => template.Info.ShortNameList.Contains($"TestAssets.{templateName}")).Info; + Edge.Template.ITemplateCreationResult result = await bootstrapper.GetCreationEffectsAsync(template, name, output, parametersDict); + + Assert.Equal(expectedResult.CreationResult.PrimaryOutputs.Count, result.CreationEffects?.CreationResult.PrimaryOutputs.Count); + + Assert.NotNull(result.CreationEffects); + Assert.NotNull(result.CreationEffects.CreationResult.PrimaryOutputs); + Assert.NotNull(result.CreationEffects.FileChanges); + + Assert.Equal( + expectedResult.CreationResult.PrimaryOutputs.Select(po => po.Path).OrderBy(s => s, StringComparer.OrdinalIgnoreCase), + result.CreationEffects.CreationResult.PrimaryOutputs.Select(po => po.Path).OrderBy(s => s, StringComparer.OrdinalIgnoreCase), + StringComparer.OrdinalIgnoreCase); + + IFileChangeComparer comparer = new IFileChangeComparer(); + Assert.Equal(expectedResult.FileChanges.Count, result.CreationEffects.FileChanges.Count); + Assert.Equal( + expectedResult.FileChanges.OrderBy(s => s, comparer), + result.CreationEffects.FileChanges.OrderBy(s => s, comparer), + comparer); + } + + [Theory] + [MemberData(nameof(Get_FileRename_TestData))] + internal async Task CreateTest(string templateName, string parameters, MockCreationEffects expectedResult) + { + using Bootstrapper bootstrapper = GetBootstrapper(); + await InstallTestTemplateAsync(bootstrapper, templateName); + + string name = BasicParametersParser.GetNameFromParameterString(parameters); + string output = BasicParametersParser.GetOutputFromParameterString(parameters); + Dictionary parametersDict = BasicParametersParser.ParseParameterString(parameters); + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter(templateName) }); + ITemplateInfo template = foundTemplates.Single(template => template.Info.ShortNameList.Contains($"TestAssets.{templateName}")).Info; + var result = await bootstrapper.CreateAsync(template, name, output, parametersDict); + + Assert.NotNull(result.CreationResult); + + Assert.Equal(expectedResult.CreationResult.PrimaryOutputs.Count, result.CreationResult.PrimaryOutputs.Count); + Assert.Equal( + expectedResult.CreationResult.PrimaryOutputs.Select(po => po.Path).OrderBy(s => s, StringComparer.OrdinalIgnoreCase), + result.CreationResult.PrimaryOutputs.Select(po => po.Path).OrderBy(s => s, StringComparer.OrdinalIgnoreCase), + StringComparer.OrdinalIgnoreCase); + + foreach (string file in expectedResult.FileChanges.Where(fc => fc.ChangeKind != ChangeKind.Delete).Select(fc => fc.TargetRelativePath)) + { + string expectedFilePath = Path.Combine(output, file); + Assert.True(File.Exists(expectedFilePath)); + } + foreach (string file in expectedResult.FileChanges.Where(fc => fc.ChangeKind == ChangeKind.Delete).Select(fc => fc.TargetRelativePath)) + { + string expectedFilePath = Path.Combine(output, file); + Assert.False(File.Exists(expectedFilePath)); + } + + foreach (string file in expectedResult.AbsentFiles) + { + string expectedFilePath = Path.Combine(output, file); + Assert.False(File.Exists(expectedFilePath)); + } + + foreach (string dir in expectedResult.AbsentDirectories) + { + string expectedPath = Path.Combine(output, dir); + Assert.False(Directory.Exists(expectedPath)); + } + } + + [Theory] + [MemberData(nameof(Get_FileRename_TestData))] + internal async Task GetCreationEffectsTest_Package(string templateName, string parameters, MockCreationEffects expectedResult) + { + using Bootstrapper bootstrapper = GetBootstrapper(); + PackTestTemplatesNuGetPackage(_packageManager); + await InstallTestTemplateAsync(bootstrapper, templateName); + + string name = BasicParametersParser.GetNameFromParameterString(parameters); + string output = BasicParametersParser.GetOutputFromParameterString(parameters); + Dictionary parametersDict = BasicParametersParser.ParseParameterString(parameters); + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter(templateName) }); + ITemplateInfo template = foundTemplates.Single(template => template.Info.ShortNameList.Contains($"TestAssets.{templateName}")).Info; + Edge.Template.ITemplateCreationResult result = await bootstrapper.GetCreationEffectsAsync(template, name, output, parametersDict); + + Assert.NotNull(result.CreationEffects); + Assert.NotNull(result.CreationEffects.CreationResult.PrimaryOutputs); + Assert.NotNull(result.CreationEffects.FileChanges); + + Assert.Equal(expectedResult.CreationResult.PrimaryOutputs.Count, result.CreationEffects.CreationResult.PrimaryOutputs.Count); + Assert.Equal( + expectedResult.CreationResult.PrimaryOutputs.Select(po => po.Path).OrderBy(s => s, StringComparer.OrdinalIgnoreCase), + result.CreationEffects.CreationResult.PrimaryOutputs.Select(po => po.Path).OrderBy(s => s, StringComparer.OrdinalIgnoreCase), + StringComparer.OrdinalIgnoreCase); + + IFileChangeComparer comparer = new IFileChangeComparer(); + Assert.Equal(expectedResult.FileChanges.Count, result.CreationEffects.FileChanges.Count); + Assert.Equal( + expectedResult.FileChanges.OrderBy(s => s, comparer), + result.CreationEffects.FileChanges.OrderBy(s => s, comparer), + comparer); + } + + [Theory] + [MemberData(nameof(Get_FileRename_TestData))] + internal async Task CreateTest_Package(string templateName, string parameters, MockCreationEffects expectedResult) + { + using Bootstrapper bootstrapper = GetBootstrapper(); + PackTestTemplatesNuGetPackage(_packageManager); + await InstallTestTemplateAsync(bootstrapper, templateName); + + string name = BasicParametersParser.GetNameFromParameterString(parameters); + string output = BasicParametersParser.GetOutputFromParameterString(parameters); + Dictionary parametersDict = BasicParametersParser.ParseParameterString(parameters); + + var foundTemplates = await bootstrapper.GetTemplatesAsync(new[] { WellKnownSearchFilters.NameFilter(templateName) }); + ITemplateInfo template = foundTemplates.Single(template => template.Info.ShortNameList.Contains($"TestAssets.{templateName}")).Info; + var result = await bootstrapper.CreateAsync(template, name, output, parametersDict); + + Assert.NotNull(result.CreationResult); + + Assert.Equal(expectedResult.CreationResult.PrimaryOutputs.Count, result.CreationResult.PrimaryOutputs.Count); + Assert.Equal( + expectedResult.CreationResult.PrimaryOutputs.Select(po => po.Path).OrderBy(s => s, StringComparer.OrdinalIgnoreCase), + result.CreationResult.PrimaryOutputs.Select(po => po.Path).OrderBy(s => s, StringComparer.OrdinalIgnoreCase), + StringComparer.OrdinalIgnoreCase); + + foreach (string file in expectedResult.FileChanges.Where(fc => fc.ChangeKind != ChangeKind.Delete).Select(fc => fc.TargetRelativePath)) + { + string expectedFilePath = Path.Combine(output, file); + Assert.True(File.Exists(expectedFilePath)); + } + foreach (string file in expectedResult.FileChanges.Where(fc => fc.ChangeKind == ChangeKind.Delete).Select(fc => fc.TargetRelativePath)) + { + string expectedFilePath = Path.Combine(output, file); + Assert.False(File.Exists(expectedFilePath)); + } + + foreach (string file in expectedResult.AbsentFiles) + { + string expectedFilePath = Path.Combine(output, file); + Assert.False(File.Exists(expectedFilePath)); + } + + foreach (string dir in expectedResult.AbsentDirectories) + { + string expectedPath = Path.Combine(output, dir); + Assert.False(Directory.Exists(expectedPath)); + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/LocalizationTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/LocalizationTests.cs new file mode 100644 index 000000000000..9865394a6844 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/LocalizationTests.cs @@ -0,0 +1,231 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests +{ + [CollectionDefinition("Localization Tests")] + public class LocalizationTestsCollection + { + //this collection is used in order that the tests are run sequentially, as they change UI localization. + } + + [Collection("Localization Tests")] + public class LocalizationTests : BootstrapperTestBase + { + [Fact] + public async Task SkipsLocalizationOnInstall_WhenInvalidFormat() + { + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + using Bootstrapper bootstrapper = GetBootstrapper(addLoggerProviders: new[] { loggerProvider }); + string testTemplateLocation = GetTestTemplateLocation("Invalid/Localization/InvalidFormat"); + + List installRequests = new() { new InstallRequest(testTemplateLocation) }; + IReadOnlyList installationResults = await bootstrapper.InstallTemplatePackagesAsync(installRequests); + Assert.True(installationResults.Single().Success); + + loggedMessages.Clear(); + var foundTemplates = await bootstrapper.GetTemplatesAsync(default); + Assert.Single(foundTemplates); + + (LogLevel level, string message) = Assert.Single(loggedMessages, m => m.Level == LogLevel.Warning); + Assert.Contains("Failed to read or parse localization file", message); + Assert.Contains("localize/templatestrings.de-DE.json", message); + } + + [Fact] + public async Task SkipsLocalizationOnInstall_WhenLocalizationValidationFails() + { + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + using Bootstrapper bootstrapper = GetBootstrapper(addLoggerProviders: new[] { loggerProvider }); + string testTemplateLocation = GetTestTemplateLocation("Invalid/Localization/ValidationFailure"); + + string[] expectedErrors = new[] + { + """ + The template 'name' (TestAssets.Invalid.Localization.ValidationFailure) has the following validation errors in 'de-DE' localization: + [Error][LOC001] In localization file under the post action with id 'pa1', there are localized strings for manual instruction(s) with ids 'do-not-exist'. These manual instructions do not exist in the template.json file and should be removed from localization file. + [Error][LOC002] Post action(s) with id(s) 'pa0' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + + """, + """ + The template 'name' (TestAssets.Invalid.Localization.ValidationFailure) has the following validation errors in 'tr' localization: + [Error][LOC002] Post action(s) with id(s) 'pa6' specified in the localization file do not exist in the template.json file. Remove the localized strings from the localization file. + + """ + }; + + string[] expectedWarnings = new[] + { + """ + The template 'name' (TestAssets.Invalid.Localization.ValidationFailure) has the following validation warnings: + [Warning][CONFIG0201] Id of the post action 'pa2' at index '3' is not unique. Only the first post action that uses this id will be localized. + + """, + "Failed to load the 'de-DE' localization the template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the localization file is not valid. The localization will be skipped.", + "Failed to load the 'tr' localization the template 'name' (TestAssets.Invalid.Localization.ValidationFailure): the localization file is not valid. The localization will be skipped." + }; + + List installRequests = new() { new InstallRequest(testTemplateLocation) }; + IReadOnlyList installationResults = await bootstrapper.InstallTemplatePackagesAsync(installRequests); + Assert.True(installationResults.Single().Success); + + loggedMessages.Clear(); + var foundTemplates = await bootstrapper.GetTemplatesAsync(default); + Assert.Single(foundTemplates); + + var errors = loggedMessages.Where(m => m.Level == LogLevel.Error).Select(m => m.Message); + var warnings = loggedMessages.Where(m => m.Level == LogLevel.Warning).Select(m => m.Message); + + Assert.Equal(expectedErrors.Length, errors.Count()); + Assert.Equal(expectedWarnings.Length, warnings.Count()); + + foreach (string error in expectedErrors) + { + Assert.Contains(error, errors); + } + foreach (string warning in expectedWarnings) + { + Assert.Contains(warning, warnings); + } + } + + [Fact] + public async Task SkipsLocalizationOnInstantiate_WhenInvalidFormat() + { + var oldCulture = CultureInfo.CurrentUICulture; + try + { + CultureInfo.CurrentUICulture = new CultureInfo("de-DE"); + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + using Bootstrapper bootstrapper = GetBootstrapper(addLoggerProviders: new[] { loggerProvider }); + string validTestTemplateLocation = GetTestTemplateLocation("TemplateWithLocalization"); + string invalidTestTemplateLocation = GetTestTemplateLocation("Invalid/Localization/InvalidFormat"); + + string tmpTemplateLocation = TestUtils.CreateTemporaryFolder(); + string output = TestUtils.CreateTemporaryFolder(); + TestUtils.DirectoryCopy(validTestTemplateLocation, tmpTemplateLocation, copySubDirs: true); + + List installRequests = new() { new InstallRequest(tmpTemplateLocation) }; + IReadOnlyList installationResults = await bootstrapper.InstallTemplatePackagesAsync(installRequests); + Assert.True(installationResults.Single().Success); + + loggedMessages.Clear(); + var foundTemplates = await bootstrapper.GetTemplatesAsync(default); + var templateToRun = Assert.Single(foundTemplates); + + Assert.DoesNotContain(loggedMessages, m => m.Level == LogLevel.Error); + Assert.DoesNotContain(loggedMessages, m => m.Level == LogLevel.Warning); + + loggedMessages.Clear(); + + //replace localization with bad file + File.Copy( + Path.Combine(invalidTestTemplateLocation, ".template.config", "localize", "templatestrings.de-DE.json"), + Path.Combine(tmpTemplateLocation, ".template.config", "localize", "templatestrings.de-DE.json"), + overwrite: true); + + var result = await bootstrapper.CreateAsync(templateToRun, "MyApp.1", output, new Dictionary()); + + Assert.True(result.Status == Edge.Template.CreationResultStatus.Success); + + var errors = loggedMessages.Where(m => m.Level == LogLevel.Error).Select(m => m.Message); + var warnings = loggedMessages.Where(m => m.Level == LogLevel.Warning).Select(m => m.Message); + + Assert.Empty(errors); + string warning = Assert.Single(warnings); + Assert.Equal($"Fehler beim Lesen oder parsen der Lokalisierungsdatei {tmpTemplateLocation}{Path.DirectorySeparatorChar}.template.config/localize/templatestrings.de-DE.json. Sie wird bei der weiteren Verarbeitung übersprungen.", warning); + } + finally + { + CultureInfo.CurrentUICulture = oldCulture; + } + } + + [Fact] + public async Task SkipsLocalizationOnInstantiate_WhenLocalizationValidationFails() + { + var oldCulture = CultureInfo.CurrentUICulture; + try + { + CultureInfo.CurrentUICulture = new CultureInfo("de-DE"); + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + using Bootstrapper bootstrapper = GetBootstrapper(addLoggerProviders: new[] { loggerProvider }); + string validTestTemplateLocation = GetTestTemplateLocation("TemplateWithLocalization"); + string invalidTestTemplateLocation = GetTestTemplateLocation("Invalid/Localization/ValidationFailure"); + string tmpTemplateLocation = TestUtils.CreateTemporaryFolder(); + string output = TestUtils.CreateTemporaryFolder(); + TestUtils.DirectoryCopy(validTestTemplateLocation, tmpTemplateLocation, copySubDirs: true); + + string[] expectedErrors = new[] + { + """ + Die Vorlage 'name' (TestAssets.TemplateWithLocalization) weist die folgenden Überprüfungsfehler in Lokalisierung „de-DE“ auf: + [Error][LOC001] In der Lokalisierungsdatei unter der POST-Aktion mit der ID „pa1“ befinden sich lokalisierte Zeichenfolgen für manuelle Anweisungen mit den IDs „do-not-exist“. Diese manuellen Anweisungen sind in der Datei „template.json“ nicht vorhanden und sollten aus der Lokalisierungsdatei entfernt werden. + + """ + }; + + string[] expectedWarnings = new[] + { + "Lokalisierung „de-DE“ der Vorlage 'name' (TestAssets.TemplateWithLocalization) konnte nicht geladen werden: Die Lokalisierungsdatei ist ungültig. Die Lokalisierung wird übersprungen.", + }; + List installRequests = new() { new InstallRequest(tmpTemplateLocation) }; + IReadOnlyList installationResults = await bootstrapper.InstallTemplatePackagesAsync(installRequests); + Assert.True(installationResults.Single().Success); + + loggedMessages.Clear(); + var foundTemplates = await bootstrapper.GetTemplatesAsync(default); + var templateToRun = Assert.Single(foundTemplates); + + Assert.DoesNotContain(loggedMessages, m => m.Level == LogLevel.Error); + Assert.DoesNotContain(loggedMessages, m => m.Level == LogLevel.Warning); + + loggedMessages.Clear(); + + //replace localization with bad file + File.Copy( + Path.Combine(invalidTestTemplateLocation, ".template.config", "localize", "templatestrings.de-DE.json"), + Path.Combine(tmpTemplateLocation, ".template.config", "localize", "templatestrings.de-DE.json"), + overwrite: true); + + var result = await bootstrapper.CreateAsync(templateToRun, "MyApp.1", output, new Dictionary()); + + Assert.True(result.Status == Edge.Template.CreationResultStatus.Success); + + var errors = loggedMessages.Where(m => m.Level == LogLevel.Error).Select(m => m.Message); + var warnings = loggedMessages.Where(m => m.Level == LogLevel.Warning).Select(m => m.Message); + + Assert.Equal(expectedErrors.Length, errors.Count()); + Assert.Equal(expectedWarnings.Length, warnings.Count()); + + foreach (string error in expectedErrors) + { + Assert.Contains(error, errors); + } + foreach (string warning in expectedWarnings) + { + Assert.Contains(warning, warnings); + } + + } + finally + { + CultureInfo.CurrentUICulture = oldCulture; + } + } + } + +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Microsoft.TemplateEngine.IDE.IntegrationTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Microsoft.TemplateEngine.IDE.IntegrationTests.csproj new file mode 100644 index 000000000000..3f2316d34148 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Microsoft.TemplateEngine.IDE.IntegrationTests.csproj @@ -0,0 +1,36 @@ + + + + $(NetCurrent);$(NetFrameworkCurrent) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/SnapshotTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/SnapshotTests.cs new file mode 100644 index 000000000000..f6fcb86504f7 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/SnapshotTests.cs @@ -0,0 +1,220 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Authoring.TemplateApiVerifier; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Xunit.Abstractions; + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests +{ + [Collection("Verify Tests")] + public class SnapshotTests : TestBase + { + private readonly ILogger _log; + + public SnapshotTests(ITestOutputHelper log) + { + _log = new XunitLoggerProvider(log).CreateLogger("TestRun"); + } + + [Fact] + public Task PreferDefaultNameTest() + { + string templateLocation = GetTestTemplateLocation("TemplateWithPreferDefaultName"); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithPreferDefaultName") + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Approvals", + DoNotPrependTemplateNameToScenarioName = true, + DoNotAppendTemplateArgsToScenarioName = true, + ScenarioName = "Basic" + } + .WithInstantiationThroughTemplateCreatorApi(new Dictionary()); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TemplateWithOnlyIfStatementTest() + { + string templateLocation = GetTestTemplateLocation("TemplateWithOnlyIfStatement"); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithOnlyIfStatement") + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Approvals", + DoNotPrependTemplateNameToScenarioName = true, + DoNotAppendTemplateArgsToScenarioName = true, + ScenarioName = "Basic" + } + .WithInstantiationThroughTemplateCreatorApi(new Dictionary() { { "default-port", "3332" } }); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TemplateWithOnlyIfStatementTestForLocalhostTest() + { + string templateLocation = GetTestTemplateLocation("TemplateWithOnlyIfForLocalhost"); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithOnlyIfForLocalhost") + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Approvals", + DoNotPrependTemplateNameToScenarioName = true, + DoNotAppendTemplateArgsToScenarioName = true, + ScenarioName = "Basic" + } + .WithInstantiationThroughTemplateCreatorApi(new Dictionary() { { "default-port", "3332" } }); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Theory] + [InlineData("no options", null)] + [InlineData("options without value", new[] { "A", null, "B", null, "C", null, "D", null })] + [InlineData("option equals to false", new[] { "A", "false", "B", "false", "C", "false", "D", "false" })] + [InlineData("option equals to true", new[] { "A", "true", "B", "true", "C", "true", "D", "true" })] + public async Task BooleanConditionsTest(string testName, string?[]? parametersArray) + { + string workingDirectory = TestUtils.CreateTemporaryFolder(); + string templateShortName = "TestAssets.TemplateWithBooleanParameters"; + + //get the template location + string templateLocation = Path.Combine(TestTemplatesLocation, "TemplateWithBooleanParameters"); + + Dictionary parameters = ConvertToParameters(parametersArray); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Approvals", + OutputDirectory = workingDirectory, + DoNotPrependTemplateNameToScenarioName = true, + DoNotAppendTemplateArgsToScenarioName = true, + ScenarioName = testName, + } + .WithInstantiationThroughTemplateCreatorApi(parameters); + + VerificationEngine engine = new(_log); + await engine.Execute(options); + } + + [Theory] + [InlineData("DefaultIfOptionWithoutValue_NoValueDefaultsNotUsedIfSwitchesNotSpecified", "DefaultIfOptionWithoutValue", "TestAssets.DefaultIfOptionWithoutValue", null)] + [InlineData("DefaultIfOptionWithoutValue_NoValueDefaultForChoiceParamIsUsed", "DefaultIfOptionWithoutValue", "TestAssets.DefaultIfOptionWithoutValue", new[] { "MyChoice", null })] + [InlineData("DefaultIfOptionWithoutValue_NoValueDefaultForStringParamIsUsed", "DefaultIfOptionWithoutValue", "TestAssets.DefaultIfOptionWithoutValue", new[] { "MyString", null })] + [InlineData("DefaultIfOptionWithoutValue_NoValueDefault_UserProvidedValuesAreIsUsed", "DefaultIfOptionWithoutValue", "TestAssets.DefaultIfOptionWithoutValue", new[] { "MyString", "UserString", "MyChoice", "OtherChoice" })] + [InlineData("Renames_FileRenames", "TemplateWithRenames", "TestAssets.TemplateWithRenames", new[] { "foo", "baz", "testForms", "TestProject" })] + [InlineData("Renames_SourceNameFileRenames", "TemplateWithSourceName", "TestAssets.TemplateWithSourceName", new[] { "name", "baz" })] + [InlineData("Renames_NegativeFileRenames", "TemplateWithUnspecifiedSourceName", "TestAssets.TemplateWithUnspecifiedSourceName", new[] { "name", "baz" })] + [InlineData("Renames_CustomSourcePathRename", "TemplateWithSourceNameAndCustomSourcePath", "TestAssets.TemplateWithSourceNameAndCustomSourcePath", new[] { "name", "bar" })] + [InlineData("Renames_CustomTargetPathRename", "TemplateWithSourceNameAndCustomTargetPath", "TestAssets.TemplateWithSourceNameAndCustomTargetPath", new[] { "name", "bar" })] + [InlineData("Renames_CustomSourceAndTargetPathRename", "TemplateWithSourceNameAndCustomSourceAndTargetPaths", "TestAssets.TemplateWithSourceNameAndCustomSourceAndTargetPaths", new[] { "name", "bar" })] + [InlineData("Renames_SourcePathOutsideConfigRoot", "TemplateWithSourcePathOutsideConfigRoot", "TestAssets.TemplateWithSourcePathOutsideConfigRoot", new[] { "name", "baz" })] + [InlineData("Renames_SourceNameInTargetPathGetsRenamed", "TemplateWithSourceNameInTargetPathGetsRenamed", "TestAssets.TemplateWithSourceNameInTargetPathGetsRenamed", new[] { "name", "baz" })] + [InlineData("Renames_PlaceholderFiles", "TemplateWithPlaceholderFiles", "TestAssets.TemplateWithPlaceholderFiles", null)] + [InlineData("Renames_DerivedSymbolFileRename", "TemplateWithDerivedSymbolFileRename", "TestAssets.TemplateWithDerivedSymbolFileRename", new[] { "name", "Last.Part.Is.For.Rename" })] + [InlineData("Renames_MultipleRenamesOnSameFile", "TemplateWithMultipleRenamesOnSameFile", "TestAssets.TemplateWithMultipleRenamesOnSameFile", new[] { "fooRename", "base", "barRename", "ball" })] + [InlineData("Renames_MultipleRenamesOnSameFileHandlesOverlap", "TemplateWithMultipleRenamesOnSameFileHandlesOverlap", "TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesOverlap", new[] { "fooRename", "pin", "oobRename", "ball" })] + [InlineData("Renames_MultipleRenamesOnSameFileHandlesInducedOverlap", "TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap", "TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap", new[] { "fooRename", "bar", "barRename", "baz" })] + [InlineData("Renames_CaseSensitiveNameBasedRenames", "TemplateWithCaseSensitiveNameBasedRenames", "TestAssets.TemplateWithCaseSensitiveNameBasedRenames", new[] { "name", "NewName" })] + [InlineData("Renames_JoinAndFolderRename", "TemplateWithJoinAndFolderRename", "TestAssets.TemplateWithJoinAndFolderRename", new[] { "name", "NewName", "product", "Office" })] + [InlineData("KitchenSink_ConfigurationKitchenSink", "ConfigurationKitchenSink", "TestAssets.ConfigurationKitchenSink", new[] { "replaceThings", "Stuff", "replaceThere", "You" })] + [InlineData("RegexMatch_RegexMatchMacroPositive", "TemplateWithRegexMatchMacro", "TestAssets.TemplateWithRegexMatchMacro", new[] { "name", "hello" })] + [InlineData("RegexMatch_RegexMatchMacroNegative", "TemplateWithRegexMatchMacro", "TestAssets.TemplateWithRegexMatchMacro", new[] { "name", "there" })] + [InlineData("TemplateWithTagsBasicTest", "TemplateWithTags", "TestAssets.TemplateWithTags", null)] + [InlineData("ValueForms", "TemplateWithValueForms", "TestAssets.TemplateWithValueForms", new[] { "foo", "Test.Value6", "param1", "MyPascalTestValue", "param2", "myCamelTestValue", "param3", "my test text" })] + [InlineData("ValueForms_DerivedSymbolWithValueForms", "TemplateWithDerivedSymbolWithValueForms", "TestAssets.TemplateWithDerivedSymbolWithValueForms", new[] { "n", "Test.AppSeven" })] + public async Task LegacyTests(string scenarioName, string templateFolderName, string templateShortName, string?[]? parametersArray) + { + string workingDirectory = TestUtils.CreateTemporaryFolder(); + string templateLocation = Path.Combine(TestTemplatesLocation, templateFolderName); + Dictionary parameters = ConvertToParameters(parametersArray); + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Approvals", + OutputDirectory = workingDirectory, + DoNotPrependTemplateNameToScenarioName = true, + DoNotAppendTemplateArgsToScenarioName = false, + ScenarioName = scenarioName, + } + .WithInstantiationThroughTemplateCreatorApi(parameters); + + VerificationEngine engine = new(_log); + await engine.Execute(options); + } + + [Fact] + public async Task LegacyTest_PortsAndCoalesceRenames() + { + string workingDirectory = TestUtils.CreateTemporaryFolder(); + string templateLocation = Path.Combine(TestTemplatesLocation, "TemplateWithPortsAndCoalesce"); + Dictionary parameters = new() + { + { "userPort2", "9999" } + }; + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithPortsAndCoalesce") + { + TemplatePath = templateLocation, + SnapshotsDirectory = "Approvals", + OutputDirectory = workingDirectory, + DoNotPrependTemplateNameToScenarioName = true, + DoNotAppendTemplateArgsToScenarioName = false, + ScenarioName = "PortsAndCoalesceRenames", + } + .WithCustomDirectoryVerifier( + async (contentDir, contentFetcher) => + { + await foreach (var (filePath, content) in contentFetcher.Value) + { + if (Path.GetFileName(filePath).Equals("bar.cs")) + { + Assert.DoesNotContain("The port is 1234", content); + Assert.Contains("The port is 9999", content); + } + } + }) + .WithInstantiationThroughTemplateCreatorApi(parameters); + + VerificationEngine engine = new(_log); + await engine.Execute(options); + } + + private Dictionary ConvertToParameters(string?[]? parametersArray) + { + Dictionary parameters = new(); + if (parametersArray != null) + { + if (parametersArray.Length % 2 != 0) + { + throw new ArgumentException($"{nameof(parametersArray)} should contain even number of elements"); + } + for (int i = 0; i < parametersArray.Length; i += 2) + { + if (parametersArray[i] == null) + { + throw new ArgumentException($"Even elements of {nameof(parametersArray)} should be parameter names and should not be null."); + } + parameters[parametersArray[i]!] = parametersArray[i + 1]; + } + } + + return parameters; + } + } +} + +#endif diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/TemplatePackagesTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/TemplatePackagesTests.cs new file mode 100644 index 000000000000..49e808b6239c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/TemplatePackagesTests.cs @@ -0,0 +1,478 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; +using Microsoft.TemplateEngine.Edge.Settings; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests +{ + public class TemplatePackagesTests : BootstrapperTestBase, IClassFixture + { + private readonly PackageManager _packageManager; + private const string ValidPackageJsonFile = /*lang=json,strict*/ """ +{ + "Packages": [ + { + "Details": { + "PackageId": "Sln", + "Author": "Enrico Sada", + "NuGetSource": "https://api.nuget.org/v3/index.json", + "Version": "0.2.0" + }, + "InstallerId": "015dcbac-b4a5-49ea-94a6-061616eb60e2", + "LastChangeTime": "2023-04-13T15:17:16.4866397Z", + "MountPointUri": "packages\\Sln.0.3.0.nupkg" + }, + { + "Details": { + "PackageId": "Boxed.Templates", + "Author": "Muhammad Rehan Saeed (RehanSaeed.com)", + "NuGetSource": "https://api.nuget.org/v3/index.json", + "Version": "7.4.0" + }, + "InstallerId": "015dcbac-b4a5-49ea-94a6-061616eb60e2", + "LastChangeTime": "2023-06-01T11:32:14.867341Z", + "MountPointUri": "packages\\Boxed.Templates.7.4.0.nupkg" + } + ] +} +"""; + + public TemplatePackagesTests(PackageManager packageManager) + { + _packageManager = packageManager; + } + + [Fact] + internal async Task CanInstall_LocalNuGetPackage() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string packageLocation = await _packageManager.GetNuGetPackage("Microsoft.DotNet.Common.ProjectTemplates.5.0"); + + InstallRequest installRequest = new InstallRequest(Path.GetFullPath(packageLocation)); + + IReadOnlyList result = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + Assert.Equal(InstallerErrorCode.Success, result[0].Error); + result[0].ErrorMessage.Should().BeNullOrEmpty(); + Assert.Equal(installRequest, result[0].InstallRequest); + + IManagedTemplatePackage? source = result[0].TemplatePackage; + Assert.NotNull(source); + Assert.Equal("Microsoft.DotNet.Common.ProjectTemplates.5.0", source!.Identifier); + Assert.Equal("Global Settings", source.Provider.Factory.DisplayName); + Assert.Equal("NuGet", source.Installer.Factory.Name); + Assert.Equal("Microsoft", source.GetDetails()["Author"]); + source.Version.Should().NotBeNullOrEmpty(); + + IReadOnlyList managedTemplatesPackages = await bootstrapper.GetManagedTemplatePackagesAsync(CancellationToken.None); + + Assert.Single(managedTemplatesPackages); + managedTemplatesPackages[0].Should().BeEquivalentTo(source); + + IReadOnlyList templatePackages = await bootstrapper.GetTemplatePackagesAsync(CancellationToken.None); + + Assert.Single(templatePackages); + Assert.IsAssignableFrom(templatePackages[0]); + templatePackages[0].Should().BeEquivalentTo((ITemplatePackage)source); + } + + [Fact] + internal async Task CanInstall_RemoteNuGetPackage() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + InstallRequest installRequest = new InstallRequest( + "Take.Blip.Client.Templates", + "0.5.135", + details: new Dictionary + { + { InstallerConstants.NuGetSourcesKey, "https://api.nuget.org/v3/index.json" } + }); + + IReadOnlyList result = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + Assert.Equal(InstallerErrorCode.Success, result[0].Error); + Assert.True(string.IsNullOrEmpty(result[0].ErrorMessage)); + Assert.Equal(installRequest, result[0].InstallRequest); + + IManagedTemplatePackage? source = result[0].TemplatePackage; + Assert.NotNull(source); + Assert.Equal("Take.Blip.Client.Templates", source!.Identifier); + Assert.Equal("Global Settings", source.Provider.Factory.DisplayName); + Assert.Equal("NuGet", source.Installer.Factory.Name); + source.GetDetails()["Author"].Should().NotBeNullOrEmpty(); + Assert.Equal("https://api.nuget.org/v3/index.json", source.GetDetails()["NuGetSource"]); + Assert.Equal("0.5.135", source.Version); + + IReadOnlyList managedTemplatesPackages = await bootstrapper.GetManagedTemplatePackagesAsync(CancellationToken.None); + + Assert.Single(managedTemplatesPackages); + managedTemplatesPackages[0].Should().BeEquivalentTo(source); + + IReadOnlyList templatePackages = await bootstrapper.GetTemplatePackagesAsync(CancellationToken.None); + + Assert.Single(templatePackages); + Assert.IsAssignableFrom(templatePackages[0]); + templatePackages[0].Should().BeEquivalentTo((ITemplatePackage)source); + } + + [Fact] + internal async Task CanInstall_Folder() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithSourceName"); + + InstallRequest installRequest = new InstallRequest(Path.GetFullPath(templateLocation)); + + IReadOnlyList result = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + Assert.Equal(InstallerErrorCode.Success, result[0].Error); + result[0].ErrorMessage.Should().BeNullOrEmpty(); + Assert.Equal(installRequest, result[0].InstallRequest); + + IManagedTemplatePackage? source = result[0].TemplatePackage; + Assert.NotNull(source); + Assert.Equal(Path.GetFullPath(templateLocation), source!.Identifier); + Assert.Equal("Global Settings", source.Provider.Factory.DisplayName); + Assert.Equal("Folder", source.Installer.Factory.Name); + source.Version.Should().BeNullOrEmpty(); + + IReadOnlyList managedTemplatesPackages = await bootstrapper.GetManagedTemplatePackagesAsync(CancellationToken.None); + + Assert.Single(managedTemplatesPackages); + managedTemplatesPackages[0].Should().BeEquivalentTo(source); + + IReadOnlyList templatePackages = await bootstrapper.GetTemplatePackagesAsync(CancellationToken.None); + + Assert.Single(templatePackages); + Assert.IsAssignableFrom(templatePackages[0]); + templatePackages[0].Should().BeEquivalentTo((ITemplatePackage)source); + } + + [Fact] + internal async Task CanCheckForLatestVersion_NuGetPackage() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + InstallRequest installRequest = new InstallRequest("Microsoft.DotNet.Common.ProjectTemplates.5.0", "5.0.0"); + + IReadOnlyList result = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + IManagedTemplatePackage? source = result[0].TemplatePackage; + Assert.NotNull(source); + IReadOnlyList checkUpdateResults = await bootstrapper.GetLatestVersionsAsync(new[] { source! }, CancellationToken.None); + + Assert.Single(checkUpdateResults); + Assert.True(checkUpdateResults[0].Success); + Assert.Equal(InstallerErrorCode.Success, checkUpdateResults[0].Error); + Assert.True(string.IsNullOrEmpty(checkUpdateResults[0].ErrorMessage)); + Assert.Equal(source, checkUpdateResults[0].TemplatePackage); + Assert.False(checkUpdateResults[0].IsLatestVersion); + Assert.NotEqual("5.0.0", checkUpdateResults[0].LatestVersion); + } + + [Fact] + internal async Task CanCheckForLatestVersion_Folder() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithSourceName"); + + InstallRequest installRequest = new InstallRequest(Path.GetFullPath(templateLocation)); + + IReadOnlyList result = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + IManagedTemplatePackage? source = result[0].TemplatePackage; + Assert.NotNull(source); + IReadOnlyList checkUpdateResults = await bootstrapper.GetLatestVersionsAsync(new[] { source! }, CancellationToken.None); + + Assert.Single(checkUpdateResults); + Assert.True(checkUpdateResults[0].Success); + Assert.Equal(InstallerErrorCode.Success, checkUpdateResults[0].Error); + Assert.True(string.IsNullOrEmpty(checkUpdateResults[0].ErrorMessage)); + Assert.Equal(source, checkUpdateResults[0].TemplatePackage); + Assert.True(checkUpdateResults[0].IsLatestVersion); + } + + [Fact] + internal async Task CanUpdateAllPackagesMetadataOnGetLatestVersion() + { + using Bootstrapper bootstrapper = GetBootstrapper(packageJsonContent: ValidPackageJsonFile); + var installedPackages = await bootstrapper.GetManagedTemplatePackagesAsync(CancellationToken.None); + + // implicitly populates packages metadata + await bootstrapper.GetLatestVersionsAsync(installedPackages, CancellationToken.None); + + var updatedPackages = await bootstrapper.GetManagedTemplatePackagesAsync(CancellationToken.None); + + Assert.Equal(2, updatedPackages.Count); + var slnPackage = updatedPackages[0]; + Assert.Equal("0.2.0", slnPackage.Version); + var slnPackageDetails = slnPackage.GetDetails(); + Assert.Equal("enricosada", slnPackageDetails["Owners"]); + Assert.False(bool.Parse(slnPackageDetails["Reserved"])); + + var boxPackage = updatedPackages[1]; + Assert.Equal("7.4.0", boxPackage.Version); + var boxPackageDetails = boxPackage.GetDetails(); + Assert.Equal("BlackLight", boxPackageDetails["Owners"]); + Assert.True(bool.Parse(boxPackageDetails["Reserved"])); + } + + [Fact] + internal async Task CanUpdateSpecifiedPackageMetadataOnGetLatestVersion() + { + using Bootstrapper bootstrapper = GetBootstrapper(packageJsonContent: ValidPackageJsonFile); + var installedPackages = await bootstrapper.GetManagedTemplatePackagesAsync(CancellationToken.None); + var boxedTemplatePackage = installedPackages.FirstOrDefault(ip => ip.Identifier == "Boxed.Templates"); + + // implicitly populates package metadata + await bootstrapper.GetLatestVersionsAsync(new[] { boxedTemplatePackage! }, CancellationToken.None); + + var updatedPackages = await bootstrapper.GetManagedTemplatePackagesAsync(CancellationToken.None); + Assert.Equal(2, updatedPackages.Count); + var slnPackageDetails = updatedPackages[0].GetDetails(); + Assert.False(slnPackageDetails.TryGetValue("Owners", out var _)); + Assert.False(bool.Parse(slnPackageDetails["Reserved"])); + + // the specified package has updated metadata + var boxPackageDetails = updatedPackages[1].GetDetails(); + Assert.Equal("BlackLight", boxPackageDetails["Owners"]); + Assert.True(bool.Parse(boxPackageDetails["Reserved"])); + } + + [Fact] + internal async Task CanUpdate_NuGetPackage() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + InstallRequest installRequest = new InstallRequest("Microsoft.DotNet.Common.ProjectTemplates.5.0", "5.0.0"); + + IReadOnlyList result = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + IManagedTemplatePackage? source = result[0].TemplatePackage; + Assert.NotNull(source); + UpdateRequest updateRequest = new UpdateRequest(source!, "5.0.1"); + + IReadOnlyList updateResults = await bootstrapper.UpdateTemplatePackagesAsync(new[] { updateRequest }, CancellationToken.None); + + Assert.Single(updateResults); + Assert.True(updateResults[0].Success); + Assert.Equal(InstallerErrorCode.Success, updateResults[0].Error); + Assert.True(string.IsNullOrEmpty(updateResults[0].ErrorMessage)); + Assert.Equal(updateRequest, updateResults[0].UpdateRequest); + + IManagedTemplatePackage? updatedSource = updateResults[0].TemplatePackage; + Assert.NotNull(updatedSource); + Assert.Equal("Global Settings", updatedSource!.Provider.Factory.DisplayName); + Assert.Equal("NuGet", updatedSource.Installer.Factory.Name); + Assert.Equal("5.0.1", updatedSource.Version); + + IReadOnlyList managedTemplatesPackages = await bootstrapper.GetManagedTemplatePackagesAsync(CancellationToken.None); + + Assert.Single(managedTemplatesPackages); + managedTemplatesPackages[0].Should().BeEquivalentTo(updatedSource); + + IReadOnlyList templatePackages = await bootstrapper.GetTemplatePackagesAsync(CancellationToken.None); + + Assert.Single(templatePackages); + Assert.IsAssignableFrom(templatePackages[0]); + templatePackages[0].Should().BeEquivalentTo((ITemplatePackage)updatedSource); + } + + [Fact] + internal async Task CanUninstall_NuGetPackage() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + InstallRequest installRequest = new InstallRequest("Microsoft.DotNet.Common.ProjectTemplates.5.0", "5.0.0"); + + IReadOnlyList result = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + IManagedTemplatePackage? source = result[0].TemplatePackage; + Assert.NotNull(source); + IReadOnlyList managedTemplatesPackages = await bootstrapper.GetManagedTemplatePackagesAsync(CancellationToken.None); + + Assert.Single(managedTemplatesPackages); + managedTemplatesPackages[0].Should().BeEquivalentTo(source); + + IReadOnlyList uninstallResults = await bootstrapper.UninstallTemplatePackagesAsync(new[] { source! }, CancellationToken.None); + + Assert.Single(uninstallResults); + Assert.True(uninstallResults[0].Success); + Assert.Equal(InstallerErrorCode.Success, uninstallResults[0].Error); + uninstallResults[0].ErrorMessage.Should().BeNullOrEmpty(); + Assert.Equal(source, uninstallResults[0].TemplatePackage); + + IReadOnlyList templatePackages = await bootstrapper.GetTemplatePackagesAsync(CancellationToken.None); + + Assert.Empty(templatePackages); + } + + [Fact] + internal async Task CanUninstall_Folder() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithSourceName"); + + InstallRequest installRequest = new InstallRequest(Path.GetFullPath(templateLocation)); + + IReadOnlyList result = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + IManagedTemplatePackage? source = result[0].TemplatePackage; + Assert.NotNull(source); + Assert.Equal(templateLocation, source!.MountPointUri); + + IReadOnlyList managedTemplatesPackages = await bootstrapper.GetManagedTemplatePackagesAsync(CancellationToken.None); + + Assert.Single(managedTemplatesPackages); + managedTemplatesPackages[0].Should().BeEquivalentTo(source); + + IReadOnlyList uninstallResults = await bootstrapper.UninstallTemplatePackagesAsync(new[] { source }, CancellationToken.None); + + Assert.Single(uninstallResults); + Assert.True(uninstallResults[0].Success); + Assert.Equal(InstallerErrorCode.Success, uninstallResults[0].Error); + uninstallResults[0].ErrorMessage.Should().BeNullOrEmpty(); + Assert.Equal(source, uninstallResults[0].TemplatePackage); + + IReadOnlyList templatePackages = await bootstrapper.GetTemplatePackagesAsync(CancellationToken.None); + + Assert.Empty(templatePackages); + + Directory.Exists(templateLocation); + } + + [Fact] + internal async Task CanReInstallPackage_WhenForceIsSpecified() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string packageLocation = await _packageManager.GetNuGetPackage("Microsoft.DotNet.Common.ProjectTemplates.5.0"); + + InstallRequest installRequest = new InstallRequest(Path.GetFullPath(packageLocation)); + + IReadOnlyList result = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + Assert.Equal(InstallerErrorCode.Success, result[0].Error); + result[0].ErrorMessage.Should().BeNullOrEmpty(); + Assert.Equal(installRequest, result[0].InstallRequest); + + InstallRequest repeatedInstallRequest = new InstallRequest(Path.GetFullPath(packageLocation), force: true); + + result = await bootstrapper.InstallTemplatePackagesAsync(new[] { repeatedInstallRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + Assert.Equal(InstallerErrorCode.Success, result[0].Error); + result[0].ErrorMessage.Should().BeNullOrEmpty(); + Assert.Equal(repeatedInstallRequest, result[0].InstallRequest); + + InstallRequest repeatedInstallRequestWithoutForce = new InstallRequest(Path.GetFullPath(packageLocation)); + + result = await bootstrapper.InstallTemplatePackagesAsync(new[] { repeatedInstallRequestWithoutForce }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.False(result[0].Success); + Assert.Equal(InstallerErrorCode.DownloadFailed, result[0].Error); + } + + [Fact] + internal async Task CanReInstallRemotePackage_WhenForceIsSpecified() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + InstallRequest installRequest = new InstallRequest("Microsoft.DotNet.Common.ProjectTemplates.5.0", version: "5.0.0"); + + IReadOnlyList result = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + Assert.Equal(InstallerErrorCode.Success, result[0].Error); + Assert.True(string.IsNullOrEmpty(result[0].ErrorMessage)); + Assert.Equal(installRequest, result[0].InstallRequest); + + InstallRequest repeatedInstallRequest = new InstallRequest("Microsoft.DotNet.Common.ProjectTemplates.5.0", version: "5.0.0", force: true); + + result = await bootstrapper.InstallTemplatePackagesAsync(new[] { repeatedInstallRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + Assert.Equal(InstallerErrorCode.Success, result[0].Error); + result[0].ErrorMessage.Should().BeNullOrEmpty(); + Assert.Equal(repeatedInstallRequest, result[0].InstallRequest); + + InstallRequest repeatedInstallRequestWithoutForce = new InstallRequest("Microsoft.DotNet.Common.ProjectTemplates.5.0", version: "5.0.0"); + + result = await bootstrapper.InstallTemplatePackagesAsync(new[] { repeatedInstallRequestWithoutForce }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.False(result[0].Success); + Assert.Equal(InstallerErrorCode.AlreadyInstalled, result[0].Error); + } + + [Fact] + internal async Task CanReInstallFolder_WhenForceIsSpecified() + { + using Bootstrapper bootstrapper = GetBootstrapper(); + string templateLocation = GetTestTemplateLocation("TemplateWithSourceName"); + + InstallRequest installRequest = new InstallRequest(Path.GetFullPath(templateLocation)); + + IReadOnlyList result = await bootstrapper.InstallTemplatePackagesAsync(new[] { installRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + Assert.Equal(InstallerErrorCode.Success, result[0].Error); + result[0].ErrorMessage.Should().BeNullOrEmpty(); + Assert.Equal(installRequest, result[0].InstallRequest); + + InstallRequest repeatedInstallRequest = new InstallRequest(Path.GetFullPath(templateLocation), force: true); + + result = await bootstrapper.InstallTemplatePackagesAsync(new[] { repeatedInstallRequest }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.True(result[0].Success); + Assert.Equal(InstallerErrorCode.Success, result[0].Error); + result[0].ErrorMessage.Should().BeNullOrEmpty(); + Assert.Equal(repeatedInstallRequest, result[0].InstallRequest); + + InstallRequest repeatedInstallRequestWithoutForce = new InstallRequest(Path.GetFullPath(templateLocation)); + + result = await bootstrapper.InstallTemplatePackagesAsync(new[] { repeatedInstallRequestWithoutForce }, InstallationScope.Global, CancellationToken.None); + + Assert.Single(result); + Assert.False(result[0].Success); + Assert.Equal(InstallerErrorCode.AlreadyInstalled, result[0].Error); + } + + internal class MountPointFactoryMock : IMountPointFactory + { + public Guid Id => Guid.Empty; + + public bool TryMount(IEngineEnvironmentSettings environmentSettings, IMountPoint? parent, string mountPointUri, out IMountPoint? mountPoint) + { + mountPoint = new MockMountPoint(environmentSettings); + return true; + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Utils/BasicParametersParser.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Utils/BasicParametersParser.cs new file mode 100644 index 000000000000..a6fab87f9196 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Utils/BasicParametersParser.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests.Utils +{ + internal static class BasicParametersParser + { + internal static string GetNameFromParameterString(string parameters) + { + string[] parametersArray = parameters.Split(null); + + int nameIndex = Array.IndexOf(parametersArray, "--name"); + if (nameIndex >= 0 && nameIndex + 1 < parameters.Length) + { + return parametersArray[nameIndex + 1]; + } + return "test"; + } + + internal static string GetOutputFromParameterString(string parameters) + { + string[] parametersArray = parameters.Split(null); + + int outputIndex = Array.IndexOf(parametersArray, "--output"); + if (outputIndex >= 0 && outputIndex + 1 < parameters.Length) + { + return parametersArray[outputIndex + 1]; + } + return TestUtils.CreateTemporaryFolder(); + } + + internal static Dictionary ParseParameterString(string parameters) + { + Dictionary parsedParameters = new Dictionary(); + string[] parametersArray = parameters.Split(null); + int i = 0; + + while (i < parametersArray.Length) + { + if (parametersArray[i] is "--name" or "--output") + { + i += 2; + continue; + } + if (!parametersArray[i].StartsWith("--")) + { + i++; + continue; + } + + if (i + 1 < parametersArray.Length && !parametersArray[i + 1].StartsWith("--")) + { + parsedParameters[parametersArray[i].Substring(2)] = parametersArray[i + 1]; + i += 2; + continue; + } + parsedParameters[parametersArray[i].Substring(2)] = string.Empty; + i++; + } + return parsedParameters; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Utils/IFileChangeEqualityComparer.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Utils/IFileChangeEqualityComparer.cs new file mode 100644 index 000000000000..63c3ed28127a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/Utils/IFileChangeEqualityComparer.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests.Utils +{ + internal class IFileChangeComparer : IEqualityComparer, IComparer + { + public int Compare(IFileChange? x, IFileChange? y) + { + if (Equals(x, y)) + { + return 0; + } + if (x == null) + { + return -1; + } + if (y == null) + { + return 1; + } + if (x.ChangeKind > y.ChangeKind) + { + return 1; + } + if (x.ChangeKind < y.ChangeKind) + { + return -1; + } + + int compareResult = ComparePaths(x.TargetRelativePath, y.TargetRelativePath); + if (compareResult != 0) + { + return compareResult; + } + return ComparePaths((x as IFileChange2)?.SourceRelativePath, (y as IFileChange2)?.SourceRelativePath); + } + + public bool Equals(IFileChange? x, IFileChange? y) + { + if (x == null && y == null) + { + return true; + } + if (x == null || y == null) + { + return false; + } + + return ComparePaths(x.TargetRelativePath, y.TargetRelativePath) == 0 + && x.ChangeKind == y.ChangeKind + && ComparePaths((x as IFileChange2)?.SourceRelativePath, (y as IFileChange2)?.SourceRelativePath) == 0; + } + + public int GetHashCode(IFileChange obj) + { + if (obj == null) + { + return 0; + } + + return (GetHashValue(obj.TargetRelativePath), obj.ChangeKind, obj is IFileChange2 obj2 ? GetHashValue(obj2.SourceRelativePath) : null).GetHashCode(); + } + + private static int ComparePaths(string? x, string? y) + { + if (string.IsNullOrWhiteSpace(x)) + { + return string.IsNullOrWhiteSpace(y) ? 0 : -1; + } + if (string.IsNullOrWhiteSpace(y)) + { + return 1; + } + return string.Compare(Path.GetFullPath(x), Path.GetFullPath(y), StringComparison.OrdinalIgnoreCase); + } + + private string GetHashValue(string x) + { + if (string.IsNullOrWhiteSpace(x)) + { + return string.Empty; + } + return Path.GetFullPath(x).ToLowerInvariant(); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/VerifySettingsFixture.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/VerifySettingsFixture.cs new file mode 100644 index 000000000000..f44508b1159d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/VerifySettingsFixture.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using VerifyTests.DiffPlex; + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests +{ + public class VerifySettingsFixture : IDisposable + { + private static bool s_called; + + public VerifySettingsFixture() + { + if (s_called) + { + return; + } + s_called = true; + + DerivePathInfo( + (_, _, type, method) => new( + directory: "Approvals", + typeName: type.Name, + methodName: method.Name)); + + // Customize diff output of verifier + VerifyDiffPlex.Initialize(OutputType.Compact); + } + + public void Dispose() { } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/VerifyTestCollection.cs b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/VerifyTestCollection.cs new file mode 100644 index 000000000000..80db2339a2d3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.IDE.IntegrationTests/VerifyTestCollection.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.IDE.IntegrationTests +{ + [CollectionDefinition("Verify Tests")] + public class VerifyTestCollection : IClassFixture + { + //intentionally empty + //defines test class collection to share the fixture + //usage [Collection("Verify Tests")] on the test class. + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks.XunitV3/Microsoft.TemplateEngine.Mocks.XunitV3.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks.XunitV3/Microsoft.TemplateEngine.Mocks.XunitV3.csproj new file mode 100644 index 000000000000..568aa39c5231 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks.XunitV3/Microsoft.TemplateEngine.Mocks.XunitV3.csproj @@ -0,0 +1,17 @@ + + + + + + $(DefineConstants);XUNIT_V3 + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/Microsoft.TemplateEngine.Mocks.Shared.props b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/Microsoft.TemplateEngine.Mocks.Shared.props new file mode 100644 index 000000000000..e4e5ea4ba0fa --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/Microsoft.TemplateEngine.Mocks.Shared.props @@ -0,0 +1,24 @@ + + + + $(NetCurrent);$(NetFrameworkCurrent) + true + true + + + + + + annotations + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/Microsoft.TemplateEngine.Mocks.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/Microsoft.TemplateEngine.Mocks.csproj new file mode 100644 index 000000000000..80e835c4109f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/Microsoft.TemplateEngine.Mocks.csproj @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockCreationEffects.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockCreationEffects.cs new file mode 100644 index 000000000000..0e96d4e7e4f9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockCreationEffects.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Abstractions; +#if XUNIT_V3 +using Xunit.Sdk; +#else +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockCreationEffects : ICreationEffects, ICreationEffects2, IXunitSerializable + { + private string[] _primaryOutputs = []; + + private MockFileChange[] _mockFileChanges = []; + + private string[] _absentFiles = []; + + public MockCreationEffects() + { + } + + public IReadOnlyList FileChanges => _mockFileChanges; + + public ICreationResult CreationResult => new MockCreationResult(primaryOutputs: _primaryOutputs.Select(p => new MockCreationPath(p)).ToList()); + + IReadOnlyList ICreationEffects2.FileChanges => _mockFileChanges; + + public IEnumerable AbsentFiles => _absentFiles.Where(path => !path.EndsWith("/") && !path.EndsWith("\\")); + + public IEnumerable AbsentDirectories => _absentFiles.Where(path => path.EndsWith("/") || path.EndsWith("\\")); + + public MockCreationEffects WithPrimaryOutputs(params string[] primaryOutputs) + { + _primaryOutputs = _primaryOutputs.Concat(primaryOutputs).ToArray(); + return this; + } + + public MockCreationEffects WithFileChange(params MockFileChange[] fileChanges) + { + _mockFileChanges = _mockFileChanges.Concat(fileChanges).ToArray(); + return this; + } + + public MockCreationEffects Without(params string[] files) + { + _absentFiles = _absentFiles.Concat(files).ToArray(); + return this; + } + + public void Deserialize(IXunitSerializationInfo info) + { +#if XUNIT_V3 + _primaryOutputs = info.GetValue("primaryOutputs")!; + _mockFileChanges = info.GetValue("fileChanges")!; + _absentFiles = info.GetValue("absentFiles")!; +#else + _primaryOutputs = info.GetValue("primaryOutputs"); + _mockFileChanges = info.GetValue("fileChanges"); + _absentFiles = info.GetValue("absentFiles"); +#endif + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue("primaryOutputs", _primaryOutputs, typeof(string[])); + info.AddValue("fileChanges", _mockFileChanges, typeof(MockFileChange[])); + info.AddValue("absentFiles", _absentFiles, typeof(string[])); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + _ = sb.Append("Primary outputs:" + string.Join("|", _primaryOutputs) + ";"); + _ = sb.Append("File changes:" + string.Join("|", _mockFileChanges) + ";"); + + return sb.ToString(); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockCreationPath.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockCreationPath.cs new file mode 100644 index 000000000000..300c5bd9d58d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockCreationPath.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockCreationPath : ICreationPath + { + public MockCreationPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new System.ArgumentException($"'{nameof(path)}' cannot be null or whitespace.", nameof(path)); + } + + Path = path; + } + + public string Path { get; set; } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockCreationResult.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockCreationResult.cs new file mode 100644 index 000000000000..fad03c5b4040 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockCreationResult.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockCreationResult : ICreationResult + { + public MockCreationResult(IReadOnlyList? postActions = null, IReadOnlyList? primaryOutputs = null) + { + PostActions = postActions ?? []; + PrimaryOutputs = primaryOutputs ?? []; + } + + public IReadOnlyList PostActions { get; } + + public IReadOnlyList PrimaryOutputs { get; } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockDirectory.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockDirectory.cs new file mode 100644 index 000000000000..4ea780bc8a81 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockDirectory.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockDirectory : IDirectory + { + private readonly List? _children; + + public MockDirectory(string fullPath, IMountPoint mountPoint) + { + if (fullPath[fullPath.Length - 1] != '/') + { + fullPath += '/'; + } + + FullPath = fullPath; + Name = fullPath.Trim('/').Split('/').Last(); + MountPoint = mountPoint; + Exists = false; + } + + public MockDirectory(string fullPath, string name, IMountPoint mountPoint, IDirectory? parent) + { + if (fullPath[fullPath.Length - 1] != '/') + { + fullPath += '/'; + } + + FullPath = fullPath; + Name = name; + _children = new List(); + Exists = true; + Parent = parent; + MountPoint = mountPoint; + } + + public bool Exists { get; } + + public string FullPath { get; } + + public FileSystemInfoKind Kind => FileSystemInfoKind.Directory; + + public IDirectory? Parent { get; } + + public string Name { get; } + + public IMountPoint MountPoint { get; } + + public IEnumerable EnumerateFileSystemInfos(string patten, SearchOption searchOption) + { + foreach (IFileSystemInfo child in _children!) + { + yield return child; + + if (searchOption == SearchOption.AllDirectories) + { + if (child is IDirectory childDir) + { + foreach (IFileSystemInfo nestedChild in childDir.EnumerateFileSystemInfos(patten, searchOption)) + { + yield return nestedChild; + } + } + } + } + } + + public IEnumerable EnumerateFiles(string pattern, SearchOption searchOption) + { + foreach (IFileSystemInfo child in _children!) + { + if (child is IFile childFile) + { + yield return childFile; + } + else if (searchOption == SearchOption.AllDirectories) + { + if (child is IDirectory childDir) + { + foreach (IFileSystemInfo nestedChild in childDir.EnumerateFileSystemInfos(pattern, searchOption)) + { + childFile = (nestedChild as IFile)!; + + if (childFile != null) + { + yield return childFile; + } + } + } + } + } + } + + public IEnumerable EnumerateDirectories(string pattern, SearchOption searchOption) + { + foreach (IFileSystemInfo child in _children!) + { + if (child is IDirectory childDir) + { + yield return childDir; + + if (searchOption == SearchOption.AllDirectories) + { + foreach (IFileSystemInfo nestedChild in childDir.EnumerateFileSystemInfos(pattern, searchOption)) + { + childDir = (nestedChild as IDirectory)!; + if (childDir != null) + { + yield return childDir; + } + } + } + } + } + } + + public MockDirectory AddDirectory(string name) + { + MockDirectory dir = new MockDirectory(FullPath + name, name, MountPoint, this); + _children!.Add(dir); + return dir; + } + + public MockDirectory AddFile(string name, byte[] contents) + { + MockFile file = new MockFile(this, name, MountPoint, contents); + _children!.Add(file); + return this; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockEnvironment.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockEnvironment.cs new file mode 100644 index 000000000000..76e8a3d8fc60 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockEnvironment.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockEnvironment : IEnvironment + { + private readonly Dictionary _environmentVariables = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public MockEnvironment(Dictionary? environmentVariablesToOverride = null) + { + var env = Environment.GetEnvironmentVariables(); + foreach (string key in env.Keys.OfType()) + { + _environmentVariables[key] = (env[key] as string) ?? string.Empty; + } + + if (environmentVariablesToOverride == null) + { + return; + } + + foreach (var item in environmentVariablesToOverride) + { + _environmentVariables[item.Key] = item.Value; + } + } + + public string NewLine { get; set; } = Environment.NewLine; + + public int ConsoleBufferWidth { get; set; } = 160; + + public string ExpandEnvironmentVariables(string name) + { + return Environment.ExpandEnvironmentVariables(name); + } + + public string GetEnvironmentVariable(string name) + { + return _environmentVariables[name]; + } + + public IReadOnlyDictionary GetEnvironmentVariables() + { + return _environmentVariables; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFile.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFile.cs new file mode 100644 index 000000000000..0b77824c0801 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFile.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. + +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockFile : IFile + { + private readonly byte[]? _contents; + + public MockFile(string fullpath, IMountPoint mountPoint) + { + FullPath = fullpath; + Name = fullpath.Trim('/').Split('/').Last(); + MountPoint = mountPoint; + Exists = false; + } + + public MockFile(IDirectory parent, string name, IMountPoint mountPoint, byte[] contents) + { + FullPath = parent.FullPath + name; + Name = name; + MountPoint = mountPoint; + _contents = contents; + Parent = parent; + Exists = true; + } + + public bool Exists { get; } + + public string FullPath { get; } + + public FileSystemInfoKind Kind => FileSystemInfoKind.File; + + public IDirectory? Parent { get; } + + public string Name { get; } + + public IMountPoint MountPoint { get; } + + public Stream OpenRead() + { + return new MemoryStream(_contents); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFileChange.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFileChange.cs new file mode 100644 index 000000000000..d6f3550460f5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFileChange.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +#if XUNIT_V3 +using Xunit.Sdk; +#else +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockFileChange : IFileChange, IFileChange2, IXunitSerializable + { + private string? _sourcePath; + private string? _targetPath; + + /// + /// This is deserialization constructor only, do not use it. Requirement of . + /// + public MockFileChange() + { + } + + public MockFileChange(string source, string target, ChangeKind kind) + { + _sourcePath = source; + _targetPath = target; + ChangeKind = kind; + } + + /// + /// Should be set prior to use. + /// + public string SourceRelativePath + { + get => _sourcePath ?? throw new Exception($"{nameof(SourceRelativePath)} was not set."); + + set => _sourcePath = value; + } + + /// + /// Should be set prior to use. + /// + public string TargetRelativePath + { + get => _targetPath ?? throw new Exception($"{nameof(TargetRelativePath)} was not set."); + + set => _targetPath = value; + } + + public ChangeKind ChangeKind { get; private set; } + + public byte[] Contents => []; + + public void Deserialize(IXunitSerializationInfo info) + { + _sourcePath = info.GetValue("sourcePath"); + _targetPath = info.GetValue("targetPath"); + ChangeKind = (ChangeKind)info.GetValue("kind"); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue("sourcePath", SourceRelativePath, typeof(string)); + info.AddValue("targetPath", TargetRelativePath, typeof(string)); + info.AddValue("kind", (int)ChangeKind, typeof(int)); + } + + public override string ToString() + { + return $"{SourceRelativePath}=>{TargetRelativePath}({ChangeKind})"; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFileStream.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFileStream.cs new file mode 100644 index 000000000000..9131235714eb --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFileStream.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockFileStream : MemoryStream + { + private readonly Action _onFlush; + + public MockFileStream(Action onFlush) + { + _onFlush = onFlush; + } + + public override void Flush() + { + _onFlush(ToArray()); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFileSystem.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFileSystem.cs new file mode 100644 index 000000000000..0d9c1a358f11 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockFileSystem.cs @@ -0,0 +1,261 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockFileSystem : IPhysicalFileSystem + { + private readonly HashSet _directories = new HashSet(StringComparer.Ordinal); + + private readonly Dictionary _files = new Dictionary(StringComparer.Ordinal); + + private readonly List _directoriesScanned = new List(); + + public string? CurrentDirectory { get; set; } + + public IReadOnlyList DirectoriesScanned => _directoriesScanned; + + public MockFileSystem Add(string filePath, string contents, Encoding? encoding = null, DateTime? lastWriteTime = null) + { + _files[filePath] = new FileSystemFile() + { + Data = (encoding ?? Encoding.UTF8).GetBytes(contents), + LastWriteTimeUtc = lastWriteTime ?? DateTime.UtcNow + }; + + string currentParent = Path.GetDirectoryName(filePath); + + while (currentParent.IndexOfAny(new[] { '\\', '/' }) != currentParent.Length - 1) + { + _directories.Add(currentParent); + currentParent = Path.GetDirectoryName(currentParent); + } + + return this; + } + + public bool DirectoryExists(string directory) + { + return _directories.Contains(directory); + } + + public bool FileExists(string file) + { + return _files.ContainsKey(file); + } + + public void FileDelete(string file) + { + if (!_files.ContainsKey(file)) + { + throw new Exception($"File {file} does not exist"); + } + + _files.Remove(file); + } + + public Stream CreateFile(string path) + { + if (!FileExists(path)) + { + _files[path] = new FileSystemFile(); + } + _files[path].Data = []; + return new MockFileStream(d => _files[path].Data = d); + } + + public Stream OpenRead(string path) + { + if (!_files.TryGetValue(path, out FileSystemFile file)) + { + throw new Exception($"File {path} does not exist"); + } + + MemoryStream s = new MemoryStream(file.Data); + return s; + } + + public void CreateDirectory(string path) + { + _directories.Add(path); + } + + public string GetCurrentDirectory() + { + return CurrentDirectory ?? string.Empty; + } + + public IEnumerable EnumerateFiles(string path, string pattern, SearchOption searchOption) + { + return _files.Keys.Where(x => x.StartsWith(path, StringComparison.Ordinal) && (x[path.Length] == Path.DirectorySeparatorChar || x[path.Length] == Path.AltDirectorySeparatorChar)); + } + + public IEnumerable EnumerateDirectories(string path, string pattern, SearchOption searchOption) + { + return _directories.Where(x => x.StartsWith(path, StringComparison.Ordinal) && (x[path.Length] == Path.DirectorySeparatorChar || x[path.Length] == Path.AltDirectorySeparatorChar)); + } + + public void WriteAllText(string path, string value) + { + if (!FileExists(path)) + { + _files[path] = new FileSystemFile(); + } + + _files[path].Data = Encoding.UTF8.GetBytes(value); + } + + public string ReadAllText(string path) + { + return Encoding.UTF8.GetString(_files[path].Data); + } + + public byte[] ReadAllBytes(string path) + { + return _files[path].Data ?? []; + } + + public void DirectoryDelete(string path, bool recursive) + { + if (!recursive + && _files.Any(x => x.Key.StartsWith(path, StringComparison.Ordinal) && (x.Key[path.Length] == Path.DirectorySeparatorChar || x.Key[path.Length] == Path.AltDirectorySeparatorChar)) + && _directories.Any(x => x.StartsWith(path, StringComparison.Ordinal) && (x[path.Length] == Path.DirectorySeparatorChar || x[path.Length] == Path.AltDirectorySeparatorChar))) + { + throw new Exception("Directory is not empty"); + } + + _directories.RemoveWhere(x => x.Equals(path, StringComparison.Ordinal) || (x.StartsWith(path, StringComparison.Ordinal) && (x[path.Length] == Path.DirectorySeparatorChar || x[path.Length] == Path.AltDirectorySeparatorChar))); + List toRemove = new List(); + + foreach (string key in _files.Keys) + { + if (key.StartsWith(path, StringComparison.Ordinal) && (key[path.Length] == Path.DirectorySeparatorChar || key[path.Length] == Path.AltDirectorySeparatorChar)) + { + toRemove.Add(key); + } + } + + foreach (string key in toRemove) + { + _files.Remove(key); + } + } + + public void FileCopy(string sourcePath, string targetPath, bool overwrite) + { + if (!overwrite && FileExists(targetPath)) + { + throw new Exception($"File {targetPath} already exists"); + } + + if (!FileExists(sourcePath)) + { + throw new Exception($"File {sourcePath} doesn't exist"); + } + + _files[targetPath] = new FileSystemFile(_files[targetPath]); + } + + public IEnumerable EnumerateFileSystemEntries(string directoryName, string pattern, SearchOption searchOption) + { + RecordDirectoryScan(directoryName, pattern, searchOption); + Glob g = Glob.Parse(searchOption != SearchOption.AllDirectories ? "**/" + pattern : pattern); + + foreach (string entry in _files.Keys.Union(_directories).Where(x => x.StartsWith(directoryName, StringComparison.Ordinal) || (x.StartsWith(directoryName, StringComparison.Ordinal) && (x[directoryName.Length] == Path.DirectorySeparatorChar || x[directoryName.Length] == Path.AltDirectorySeparatorChar)))) + { + string p = entry.Replace('\\', '/').TrimStart('/'); + if (g.IsMatch(p)) + { + yield return entry; + } + } + } + + public FileAttributes GetFileAttributes(string file) + { + if (!FileExists(file)) + { + throw new Exception($"File {file} doesn't exist"); + } + + return _files[file].Attributes; + } + + public void SetFileAttributes(string file, FileAttributes attributes) + { + if (!FileExists(file)) + { + throw new Exception($"File {file} doesn't exist"); + } + + _files[file].Attributes = attributes; + } + + public DateTime GetLastWriteTimeUtc(string file) + { + if (!FileExists(file)) + { + throw new Exception($"File {file} doesn't exist"); + } + + return _files[file].LastWriteTimeUtc; + } + + public void SetLastWriteTimeUtc(string file, DateTime lastWriteTimeUtc) + { + if (!FileExists(file)) + { + throw new Exception($"File {file} doesn't exist"); + } + + _files[file].LastWriteTimeUtc = lastWriteTimeUtc; + } + + public string PathRelativeTo(string target, string relativeTo) => throw new NotImplementedException(); + + public IDisposable WatchFileChanges(string filepath, FileSystemEventHandler fileChanged) => throw new NotImplementedException(); + + private void RecordDirectoryScan(string directoryName, string pattern, SearchOption searchOption) + { + _directoriesScanned.Add(new DirectoryScanParameters + { + DirectoryName = directoryName, + Pattern = pattern, + SearchOption = searchOption + }); + } + + public class DirectoryScanParameters + { + public string? DirectoryName { get; set; } + + public string? Pattern { get; set; } + + public SearchOption SearchOption { get; set; } + } + + private class FileSystemFile + { + public FileSystemFile() + { + } + + public FileSystemFile(FileSystemFile file) + { + Data = file.Data; + Attributes = file.Attributes; + LastWriteTimeUtc = file.LastWriteTimeUtc; + } + + public byte[]? Data { get; set; } + + public FileAttributes Attributes { get; set; } + + public DateTime LastWriteTimeUtc { get; set; } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockGlobalRunSpec.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockGlobalRunSpec.cs new file mode 100644 index 000000000000..15a6a3fdf2c8 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockGlobalRunSpec.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. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockGlobalRunSpec : IGlobalRunSpec + { + public MockGlobalRunSpec() + { + Exclude = new List(); + Include = new List(); + CopyOnly = new List(); + Operations = new List(); + Special = new List>(); + LocalizationOperations = new Dictionary>(); + Rename = new Dictionary(); + IgnoreFileNames = new[] { "-.-", "_._" }; + RootVariableCollection = null!; + } + + public IReadOnlyList Exclude { get; set; } + + public IReadOnlyList Include { get; set; } + + public IReadOnlyList CopyOnly { get; set; } + + public IReadOnlyList Operations { get; set; } + + public IVariableCollection RootVariableCollection { get; set; } + + public IReadOnlyList> Special { get; set; } + + public IReadOnlyDictionary> LocalizationOperations { get; set; } + + public IReadOnlyList IgnoreFileNames { get; set; } + + public IReadOnlyDictionary Rename { get; set; } + + public bool TryGetTargetRelPath(string sourceRelPath, out string targetRelPath) + { + return Rename.TryGetValue(sourceRelPath, out targetRelPath); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockInstallerFactory.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockInstallerFactory.cs new file mode 100644 index 000000000000..bc86ef40c21b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockInstallerFactory.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Installer; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockInstallerFactory : IInstallerFactory + { + private readonly Guid _factoryId = new Guid("00000000-0000-0000-0000-000000000000"); + + public string Name => "MockInstallerFactory"; + + public Guid Id => _factoryId; + + public IInstaller CreateInstaller(IEngineEnvironmentSettings settings, string installPath) => throw new NotImplementedException(); + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockManagedTemplatePackageProvider.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockManagedTemplatePackageProvider.cs new file mode 100644 index 000000000000..bd9920522e88 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockManagedTemplatePackageProvider.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.Installer; +using Microsoft.TemplateEngine.Abstractions.TemplatePackage; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockManagedTemplatePackageProvider : IManagedTemplatePackageProvider + { + public event Action? TemplatePackagesChanged + { + add => throw new NotSupportedException(); + remove { } + } + + public ITemplatePackageProviderFactory Factory => throw new NotImplementedException(); + + public Task> GetAllTemplatePackagesAsync(CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> GetLatestVersionsAsync(IEnumerable managedSources, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> InstallAsync(IEnumerable installRequests, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> UninstallAsync(IEnumerable managedSources, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> UpdateAsync(IEnumerable updateRequests, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task UpdateTemplatePackageMetadataAsync(IEnumerable templatePackages, CancellationToken cancellationToken) => throw new NotImplementedException(); + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockMountPoint.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockMountPoint.cs new file mode 100644 index 000000000000..f7557586b295 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockMountPoint.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockMountPoint : IMountPoint + { + public MockMountPoint(IEngineEnvironmentSettings environmentSettings) + { + EnvironmentSettings = environmentSettings; + MockRoot = new MockDirectory("/", "/", this, null); + MountPointUri = null!; + } + + public IDirectory Root => MockRoot; + + public MockDirectory MockRoot { get; } + + public IEngineEnvironmentSettings EnvironmentSettings { get; set; } + + public string MountPointUri { get; } + + public IFileSystemInfo FileSystemInfo(string fullPath) + { + string[] parts = fullPath.TrimStart('/').Split('/'); + + IDirectory current = Root; + + for (int i = 0; i < parts.Length; ++i) + { + IFileSystemInfo? info = current.EnumerateFileSystemInfos(parts[i], SearchOption.TopDirectoryOnly).FirstOrDefault(); + + if (info == null) + { + return new MockFile(fullPath, this); + } + + if (info is IDirectory dir) + { + current = dir; + continue; + } + + if (info is IFile file) + { + if (i == parts.Length - 1) + { + return file; + } + + return new MockFile(fullPath, this); + } + } + + return current; + } + + public IDirectory DirectoryInfo(string fullPath) + { + IFileSystemInfo info = FileInfo(fullPath); + + if (info is IDirectory resultDir) + { + return resultDir; + } + + return new MockDirectory(fullPath, this); + } + + public IFile FileInfo(string fullPath) + { + IFileSystemInfo info = FileSystemInfo(fullPath); + + if (info is IFile resultFile) + { + return resultFile; + } + + return new MockFile(fullPath, this); + } + + public void Dispose() + { + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockOperation.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockOperation.cs new file mode 100644 index 000000000000..8ef25e9fafe1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockOperation.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockOperation : IOperation + { + private readonly MatchHandler _onMatch; + + public MockOperation(string? id, MatchHandler onMatch, bool initialState, params byte[][] tokens) + : this(id, onMatch, initialState, tokens.Select(token => TokenConfig.LiteralToken(token)).ToArray()) + { + } + + public MockOperation(string? id, MatchHandler onMatch, bool initialState, params IToken[] tokens) + { + Tokens = tokens; + Id = id; + _onMatch = onMatch; + IsInitialStateOn = initialState; + } + + public delegate int MatchHandler(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token); + + public IReadOnlyList Tokens { get; } + + public string? Id { get; } + + public IOperationProvider Provider => new MockOperationProvider(this); + + public bool IsInitialStateOn { get; } + + public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token) + { + return _onMatch?.Invoke(processor, bufferLength, ref currentBufferPosition, token) ?? 0; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockOperationProvider.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockOperationProvider.cs new file mode 100644 index 000000000000..0bd3a06e162f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockOperationProvider.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.TemplateEngine.Core.Contracts; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockOperationProvider : IOperationProvider + { + private readonly MockOperation _operation; + + public MockOperationProvider(MockOperation operation) + { + _operation = operation; + } + + public string? Id => _operation.Id; + + public IOperation GetOperation(Encoding encoding, IProcessorState processorState) + { + return _operation; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockPostAction.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockPostAction.cs new file mode 100644 index 000000000000..5584df48f2bf --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockPostAction.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockPostAction : IPostAction + { + public MockPostAction(string? description, string? manualInstructions, Guid actionId, bool continueOnError, IReadOnlyDictionary args) + { + Description = description; + ManualInstructions = manualInstructions; + ActionId = actionId; + ContinueOnError = continueOnError; + Args = args; + } + + public string? Description { get; set; } + + public Guid ActionId { get; set; } + + public bool ContinueOnError { get; set; } + + public IReadOnlyDictionary Args { get; set; } + + public string? ManualInstructions { get; set; } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockTemplateInfo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockTemplateInfo.cs new file mode 100644 index 000000000000..073b2f25524a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockTemplateInfo.cs @@ -0,0 +1,326 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Text.Json; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Utils; +#if XUNIT_V3 +using Xunit.Sdk; +#else +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockTemplateInfo : ITemplateInfo, IXunitSerializable + { + private string? _identity; + private string? _name; + + private Dictionary _parameters = new Dictionary(); + + private string[] _baselineInfo = []; + + private string[] _classifications = []; + + private string[] _shortNameList = []; + + private readonly bool _preferDefaultName = true; + + private Guid[] _postActions = []; + + private TemplateConstraintInfo[] _constraints = []; + + private Dictionary _tags = new Dictionary(); + + public MockTemplateInfo() + { + } + + public MockTemplateInfo(string shortName, string? name = null, string? identity = null, string? groupIdentity = null, int precedence = 0, string? author = null) + : this(new string[] { shortName }, name, identity, groupIdentity, precedence, author) + { + } + + public MockTemplateInfo(string[] shortNames, string? name = null, string? identity = null, string? groupIdentity = null, int precedence = 0, string? author = null) : this() + { + _shortNameList = shortNames; + _name = string.IsNullOrEmpty(name) ? "Template " + shortNames[0] : name; + + _identity = string.IsNullOrEmpty(identity) ? shortNames[0] : identity; + + Precedence = precedence; + GroupIdentity = groupIdentity; + Author = author; + } + + public string? Author { get; private set; } + + public string? Description { get; private set; } + + public IReadOnlyList Classifications => _classifications; + + public string? DefaultName { get; } + + public string Identity => _identity ?? throw new Exception($"{nameof(_identity)} was not initialized."); + + public Guid GeneratorId { get; } + + public string? GroupIdentity { get; private set; } + + public int Precedence { get; private set; } + + public string Name => _name ?? throw new Exception($"{nameof(_name)} was not initialized."); + + public string ShortName + { + get + { + if (_shortNameList.Length > 0) + { + return _shortNameList[0]; + } + return string.Empty; + } + } + + public IReadOnlyList ShortNameList => _shortNameList; + + public bool PreferDefaultName => _preferDefaultName; + + [Obsolete("Use ParameterDefinitionSet instead.")] + IReadOnlyDictionary ITemplateInfo.Tags => throw new NotImplementedException(); + + [Obsolete("Use ParameterDefinitionSet instead.")] + IReadOnlyDictionary ITemplateInfo.CacheParameters => throw new NotImplementedException(); + + public IParameterDefinitionSet ParameterDefinitions + { + get + { + List parameters = new List(); + foreach (var param in _parameters) + { + parameters.Add(param.Value); + } + return new ParameterDefinitionSet(parameters); + } + } + + [Obsolete("Use ParameterDefinitionSet instead.")] + public IReadOnlyList Parameters => ParameterDefinitions; + + public string MountPointUri => "FakeMountPoint"; + + public string ConfigPlace => "FakeConfigPlace"; + + public string? LocaleConfigPlace { get; } + + public string? HostConfigPlace { get; } + + public string? ThirdPartyNotices { get; } + + public IReadOnlyDictionary BaselineInfo => _baselineInfo.ToDictionary(k => k, k => (IBaselineInfo)new BaselineInfo(new Dictionary())); + + bool ITemplateInfo.HasScriptRunningPostActions { get; set; } + + public DateTime? ConfigTimestampUtc { get; } + + public IReadOnlyDictionary TagsCollection => _tags; + + public IReadOnlyList PostActions => _postActions; + + public IReadOnlyList Constraints => _constraints; + + public MockTemplateInfo WithParameters(params string[] parameters) + { + foreach (var param in parameters) + { + _parameters[param] = new TemplateParameter(param, "parameter", "string", precedence: new TemplateParameterPrecedence(PrecedenceDefinition.Optional)); + } + return this; + } + + public MockTemplateInfo WithTag(string tagName, string value) + { + _tags.Add(tagName, value); + return this; + } + + public MockTemplateInfo WithChoiceParameter(string name, params string[] values) + { + _parameters.Add(name, new TemplateParameter( + name, + type: "parameter", + datatype: "choice", + precedence: new TemplateParameterPrecedence(PrecedenceDefinition.Optional), + choices: values.ToDictionary(v => v, v => new ParameterChoice(null, null)))); + return this; + } + + public MockTemplateInfo WithMultiChoiceParameter(string name, params string[] values) + { + _parameters.Add(name, new TemplateParameter( + name, + type: "parameter", + datatype: "choice", + precedence: new TemplateParameterPrecedence(PrecedenceDefinition.Optional), + allowMultipleValues: true, + choices: values.ToDictionary(v => v, v => new ParameterChoice(null, null)))); + return this; + } + + public MockTemplateInfo WithChoiceParameter(string name, string[] values, bool isRequired = false, string? defaultValue = null, string? defaultIfNoOptionValue = null, string? description = null, bool allowMultipleValues = false) + { +#pragma warning disable CS0618 // Type or member is obsolete + _parameters.Add(name, new TemplateParameter( + name, + type: "parameter", + datatype: "choice", + description: description, + precedence: (isRequired ? TemplateParameterPriority.Required : TemplateParameterPriority.Optional).ToTemplateParameterPrecedence(), + defaultValue: defaultValue, + defaultIfOptionWithoutValue: defaultIfNoOptionValue, + allowMultipleValues: allowMultipleValues, + choices: values.ToDictionary(v => v, v => new ParameterChoice(null, null)))); +#pragma warning restore CS0618 // Type or member is obsolete + return this; + } + + public MockTemplateInfo WithDescription(string? description) + { + Description = description; + return this; + } + + public MockTemplateInfo WithBaselineInfo(params string[] baseline) + { + _baselineInfo = _baselineInfo.Length == 0 ? baseline : _baselineInfo.Concat(baseline).ToArray(); + return this; + } + + public MockTemplateInfo WithClassifications(params string[] classifications) + { + _classifications = _classifications.Length == 0 ? classifications : _classifications.Concat(classifications).ToArray(); + return this; + } + + public MockTemplateInfo WithParameter(string paramName, string paramType = "string", bool isRequired = false, string? defaultValue = null, string? defaultIfNoOptionValue = null, string? description = null) + { +#pragma warning disable CS0618 // Type or member is obsolete + _parameters[paramName] = new TemplateParameter( + paramName, + "parameter", + paramType, + (isRequired ? TemplateParameterPriority.Required : TemplateParameterPriority.Optional).ToTemplateParameterPrecedence(), + description: description, + defaultValue: defaultValue, + defaultIfOptionWithoutValue: defaultIfNoOptionValue); +#pragma warning restore CS0618 // Type or member is obsolete + return this; + } + + public MockTemplateInfo WithPostActions(params Guid[] postActions) + { + _postActions = _postActions.Length == 0 ? postActions : _postActions.Concat(postActions).ToArray(); + return this; + } + + public MockTemplateInfo WithConstraints(params TemplateConstraintInfo[] constraintInfos) + { + _constraints = _constraints.Length == 0 ? constraintInfos : _constraints.Concat(constraintInfos).ToArray(); + return this; + } + + #region XUnitSerializable implementation + + public void Deserialize(IXunitSerializationInfo info) + { + _name = info.GetValue("template_name"); + Precedence = info.GetValue("template_precedence"); + _identity = info.GetValue("template_identity"); + GroupIdentity = info.GetValue("template_group"); + Description = info.GetValue("template_description"); + Author = info.GetValue("template_author"); +#if XUNIT_V3 + _tags = JsonSerializer.Deserialize>(info.GetValue("template_tags")!) + ?? throw new Exception("Deserialiation failed"); + _parameters = JsonSerializer.Deserialize>(info.GetValue("template_params")!) + ?? throw new Exception("Deserialiation failed"); + _baselineInfo = JsonSerializer.Deserialize(info.GetValue("template_baseline")!) + ?? throw new Exception("Deserialiation failed"); + _classifications = JsonSerializer.Deserialize(info.GetValue("template_classifications")!) + ?? throw new Exception("Deserialiation failed"); + _shortNameList = JsonSerializer.Deserialize(info.GetValue("template_shortname")!) + ?? throw new Exception("Deserialiation failed"); +#else + _tags = JsonSerializer.Deserialize>(info.GetValue("template_tags")) + ?? throw new Exception("Deserialiation failed"); + _parameters = JsonSerializer.Deserialize>(info.GetValue("template_params")) + ?? throw new Exception("Deserialiation failed"); + _baselineInfo = JsonSerializer.Deserialize(info.GetValue("template_baseline")) + ?? throw new Exception("Deserialiation failed"); + _classifications = JsonSerializer.Deserialize(info.GetValue("template_classifications")) + ?? throw new Exception("Deserialiation failed"); + _shortNameList = JsonSerializer.Deserialize(info.GetValue("template_shortname")) + ?? throw new Exception("Deserialiation failed"); +#endif + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue("template_name", Name, typeof(string)); + info.AddValue("template_shortname", JsonSerializer.Serialize(_shortNameList), typeof(string)); + info.AddValue("template_precedence", Precedence, typeof(int)); + info.AddValue("template_identity", Identity, typeof(string)); + info.AddValue("template_group", GroupIdentity, typeof(string)); + info.AddValue("template_description", Description, typeof(string)); + info.AddValue("template_author", Author, typeof(string)); + + info.AddValue("template_tags", JsonSerializer.Serialize(_tags), typeof(string)); + info.AddValue("template_params", JsonSerializer.Serialize(_parameters), typeof(string)); + info.AddValue("template_baseline", JsonSerializer.Serialize(_baselineInfo), typeof(string)); + info.AddValue("template_classifications", JsonSerializer.Serialize(_classifications), typeof(string)); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + _ = sb.Append("Short name:" + string.Join(",", _shortNameList) + ";"); + _ = sb.Append("Identity:" + Identity + ";"); + + _ = string.IsNullOrEmpty(GroupIdentity) ? sb.Append("Group:;") : sb.Append("Group:" + GroupIdentity + ";"); + if (Precedence != 0) + { + _ = sb.Append("Precedence:" + Precedence + ";"); + } + if (!string.IsNullOrEmpty(Author)) + { + _ = sb.Append("Author:" + Author + ";"); + } + if (Classifications.Any()) + { + _ = sb.Append("Classifications:" + string.Join(",", _classifications) + ";"); + } + if (_parameters.Any()) + { + _ = sb.Append("ParameterDefinitionSet:" + string.Join(",", _parameters) + ";"); + } + if (_baselineInfo.Any()) + { + _ = sb.Append("Baseline:" + string.Join(",", _baselineInfo) + ";"); + } + if (_tags.Any()) + { + _ = sb.Append("Tags:" + string.Join(",", _tags.Select(t => t.Key + "(" + t.Value + ")")) + ";"); + } + return sb.ToString(); + } + + #endregion XUnitSerializable implementation + } + +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockTemplatePackageInfo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockTemplatePackageInfo.cs new file mode 100644 index 000000000000..5a0203dcbf26 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Mocks/MockTemplatePackageInfo.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateEngine.Mocks +{ + public class MockTemplatePackageInfo : ITemplatePackageInfo + { + public MockTemplatePackageInfo(string name, string? version = null, long totalDownloads = 0, IEnumerable? owners = null) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or whitespace.", nameof(name)); + } + + Name = name; + Version = version; + TotalDownloads = totalDownloads; + Owners = owners?.ToArray() ?? []; + } + + public string Name { get; } + + public string? Version { get; } + + public long TotalDownloads { get; } + + public IReadOnlyList Owners { get; } + + public bool Reserved => false; + + public string? Description => null; + + public string? IconUrl => null; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/AllComponents.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/AllComponents.cs new file mode 100644 index 000000000000..eb12eead0679 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/AllComponents.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests +{ + public class AllComponents + { + [Fact] + public void TestAllComponents() + { + var assemblyCatalog = new AssemblyComponentCatalog(new[] { typeof(Components).Assembly }); + + IOrderedEnumerable expectedTypeNames = assemblyCatalog + .Where(pair => !pair.Item1.IsGenericType) + //obsolete type kept for backward compatibility. +#pragma warning disable CS0618 // Type or member is obsolete + .Where(pair => pair.Item1 != typeof(IDeferredMacro)) + .Where(pair => pair.Item1 != typeof(IDeterministicModeMacro)) +#pragma warning restore CS0618 // Type or member is obsolete + .Select(pair => pair.Item1.FullName + ";" + pair.Item2.GetType().FullName) + .OrderBy(name => name); + + IOrderedEnumerable actualTypeNames = Components.AllComponents.Select(t => t.Type.FullName + ";" + t.Instance.GetType().FullName).OrderBy(name => name); + + Assert.Equal(expectedTypeNames, actualTypeNames); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/MyProject.Helper.csproj new file mode 100644 index 000000000000..3694334097fe --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/Program.cs new file mode 100644 index 000000000000..ea61d037bb73 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine($"Use frame navigation. JoinMacroTest: stringforjoinTrue"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestCoalesce_EmptyStringForMultiChoices._.verified/TestAssets.TemplateWithMultipleChoicesAndCoalesce/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestCoalesce_EmptyStringForMultiChoices._.verified/TestAssets.TemplateWithMultipleChoicesAndCoalesce/Program.cs new file mode 100644 index 000000000000..70ffc2828600 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestCoalesce_EmptyStringForMultiChoices._.verified/TestAssets.TemplateWithMultipleChoicesAndCoalesce/Program.cs @@ -0,0 +1,12 @@ +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("Preset test is: unit|ui."); + Console.WriteLine("User's choice is: ."); + Console.WriteLine("The final set is: ."); + } + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym/MyProject.Helper.csproj new file mode 100644 index 000000000000..f87bd0ed10f6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym/MyProject.Helper.csproj @@ -0,0 +1,11 @@ + + + + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym/Nuget.Frameworks.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym/Nuget.Frameworks.cs new file mode 100644 index 000000000000..9ca76c24864c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym/Nuget.Frameworks.cs @@ -0,0 +1,8 @@ +using System; + +namespace MyProject.Con +{ + public class NugetExtensions + { + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym/Nuget.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym/Nuget.cs new file mode 100644 index 000000000000..92e7a2e29925 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym/Nuget.cs @@ -0,0 +1,8 @@ +using System; + +namespace MyProject.Con +{ + public class Nuget + { + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol_DifferentOrder._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol_DifferentOrder._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder/MyProject.Helper.csproj new file mode 100644 index 000000000000..f87bd0ed10f6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol_DifferentOrder._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder/MyProject.Helper.csproj @@ -0,0 +1,11 @@ + + + + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol_DifferentOrder._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder/Nuget.Frameworks.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol_DifferentOrder._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder/Nuget.Frameworks.cs new file mode 100644 index 000000000000..9ca76c24864c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol_DifferentOrder._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder/Nuget.Frameworks.cs @@ -0,0 +1,8 @@ +using System; + +namespace MyProject.Con +{ + public class NugetExtensions + { + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol_DifferentOrder._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder/Nuget.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol_DifferentOrder._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder/Nuget.cs new file mode 100644 index 000000000000..92e7a2e29925 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestGeneratedSymbolWithRefToDerivedSymbol_DifferentOrder._.verified/TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder/Nuget.cs @@ -0,0 +1,8 @@ +using System; + +namespace MyProject.Con +{ + public class Nuget + { + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestSelectionForMultiChoicesWhenThereAreMultiplePartialMatchesAndOnePreciseMatch._.verified/TestAssets.TemplateWithMultipleChoicesAndPartialMatches/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestSelectionForMultiChoicesWhenThereAreMultiplePartialMatchesAndOnePreciseMatch._.verified/TestAssets.TemplateWithMultipleChoicesAndPartialMatches/Program.cs new file mode 100644 index 000000000000..fe6dc114ffee --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestSelectionForMultiChoicesWhenThereAreMultiplePartialMatchesAndOnePreciseMatch._.verified/TestAssets.TemplateWithMultipleChoicesAndPartialMatches/Program.cs @@ -0,0 +1,10 @@ +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("User's choice is: aa."); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestSingleSelectionForMultiChoices._.verified/TestAssets.TemplateWithMultipleChoicesAndCoalesce/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestSingleSelectionForMultiChoices._.verified/TestAssets.TemplateWithMultipleChoicesAndCoalesce/Program.cs new file mode 100644 index 000000000000..fcb56da0262e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestSingleSelectionForMultiChoices._.verified/TestAssets.TemplateWithMultipleChoicesAndCoalesce/Program.cs @@ -0,0 +1,12 @@ +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("Preset test is: unit|ui."); + Console.WriteLine("User's choice is: unit."); + Console.WriteLine("The final set is: unit."); + } + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/MyProject.Helper.csproj new file mode 100644 index 000000000000..3694334097fe --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/Program.cs new file mode 100644 index 000000000000..dcdca7ca350c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/Program.cs @@ -0,0 +1,14 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("Don't use extensions navigation."); + + Console.WriteLine("Don't use frame navigation."); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/std-streams/stderr.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..a0daf7c1321f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithBrokenGeneratedInComputed._.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "TestAssets.TemplateWithBrokenGeneratedInComputed" was created successfully. \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithCircleDependencyInMacros._.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithCircleDependencyInMacros._.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..34f52503fd82 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithCircleDependencyInMacros._.verified/std-streams/stderr.txt @@ -0,0 +1,2 @@ +Failed to create template. +Details: Parameter conditions contain cyclic dependency: [switchCheck, switchCheck2, switchCheck] that is preventing deterministic evaluation. \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithCircleDependencyInMacros._.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithCircleDependencyInMacros._.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithCircleDependencyInMacros._.verified/std-streams/stdout.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/TestAssets.TemplateWithComputedInDerivedThroughGenerated/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/TestAssets.TemplateWithComputedInDerivedThroughGenerated/MyProject.Helper.csproj new file mode 100644 index 000000000000..3694334097fe --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/TestAssets.TemplateWithComputedInDerivedThroughGenerated/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/TestAssets.TemplateWithComputedInDerivedThroughGenerated/stringforjointrue.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/TestAssets.TemplateWithComputedInDerivedThroughGenerated/stringforjointrue.cs new file mode 100644 index 000000000000..2d4f2d2e0d29 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/TestAssets.TemplateWithComputedInDerivedThroughGenerated/stringforjointrue.cs @@ -0,0 +1,12 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine($"Use frame navigation. JoinMacroTest: STRINGFORJOINTrue"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/std-streams/stderr.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..b5f6b327f8d4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInDerivedThroughGenerated._.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "TestAssets.TemplateWithComputedInDerivedThroughGenerated" was created successfully. \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/MyProject.Helper.csproj new file mode 100644 index 000000000000..3694334097fe --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/Program.cs new file mode 100644 index 000000000000..ea61d037bb73 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/TestAssets.TemplateWithComputedInGenerated/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine($"Use frame navigation. JoinMacroTest: stringforjoinTrue"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/std-streams/stderr.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..339a9dc4b74e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithComputedInGenerated._.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "TestAssets.TemplateWithComputedInGenerated" was created successfully. \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/TestAssets.TemplateWithGeneratedInComputed/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/TestAssets.TemplateWithGeneratedInComputed/MyProject.Helper.csproj new file mode 100644 index 000000000000..3694334097fe --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/TestAssets.TemplateWithGeneratedInComputed/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/TestAssets.TemplateWithGeneratedInComputed/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/TestAssets.TemplateWithGeneratedInComputed/Program.cs new file mode 100644 index 000000000000..4792a8af97a6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/TestAssets.TemplateWithGeneratedInComputed/Program.cs @@ -0,0 +1,14 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("Use extensions navigation."); + + Console.WriteLine("Don't use frame navigation."); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/std-streams/stderr.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..126cfbaff86f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedInComputed._.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "TestAssets.TemplateWithGeneratedInComputed" was created successfully. \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/TestAssets.TemplateWithGeneratedSwitchInComputed/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/TestAssets.TemplateWithGeneratedSwitchInComputed/MyProject.Helper.csproj new file mode 100644 index 000000000000..3694334097fe --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/TestAssets.TemplateWithGeneratedSwitchInComputed/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/TestAssets.TemplateWithGeneratedSwitchInComputed/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/TestAssets.TemplateWithGeneratedSwitchInComputed/Program.cs new file mode 100644 index 000000000000..ddeb8d3bfcfa --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/TestAssets.TemplateWithGeneratedSwitchInComputed/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("Use extensions navigation."); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/std-streams/stderr.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..11f7c2be698a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithGeneratedSwitchInComputed._.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "TestAssets.TemplateWithGeneratedSwitchInComputed" was created successfully. \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/MyProject.Helper.csproj new file mode 100644 index 000000000000..3694334097fe --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/Program.cs new file mode 100644 index 000000000000..dcdca7ca350c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/TestAssets.TemplateWithBrokenGeneratedInComputed/Program.cs @@ -0,0 +1,14 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("Don't use extensions navigation."); + + Console.WriteLine("Don't use frame navigation."); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/std-streams/stderr.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/std-streams/stderr.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/std-streams/stderr.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/std-streams/stdout.txt b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..a0daf7c1321f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Approvals/TestTemplateWithVariablesInGeneratedThatUsedInComputed._.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "TestAssets.TemplateWithBrokenGeneratedInComputed" was created successfully. \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/BindSymbolTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/BindSymbolTests.cs new file mode 100644 index 000000000000..146e59302d7e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/BindSymbolTests.cs @@ -0,0 +1,723 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using FakeItEasy; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Components; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Edge; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests +{ + public class BindSymbolTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public BindSymbolTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Fact] + public async Task CreateAsyncTest_UseBindValuesWithReplace() + { + // + // Template content preparation + // + + var templateConfig = new + { + identity = "test.template", + name = "test", + shortName = "test", + symbols = new + { + hostPrefixed = new + { + type = "bind", + binding = "host:HostIdentifier", + replaces = "%R1%" + }, + envPrefixed = new + { + type = "bind", + binding = "env:MYENVVAR", + replaces = "%R2%" + }, + hostUnprefixed = new + { + type = "bind", + binding = "host:HostIdentifier", + replaces = "%R3%" + }, + envUnprefixed = new + { + type = "bind", + binding = "env:MYENVVAR", + replaces = "%R4%" + }, + } + }; + + string sourceSnippet = """ + %R1% + %R2% + %R3% + %R4% + """; + + string expectedSnippet = """ + TestHost + MyValue + TestHost + MyValue + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, JsonSerializer.Serialize(templateConfig, new JsonSerializerOptions { WriteIndented = true }) }, + { "sourceFile", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + + Environment.SetEnvironmentVariable("MYENVVAR", "MyValue"); + IEnvironment environment = new DefaultEnvironment(); + + IEngineEnvironmentSettings settings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: "TestHost", virtualize: true, environment: environment); + ((TestHost)settings.Host).HostParamDefaults["HostIdentifier"] = "TestHost"; + string sourceBasePath = settings.GetTempVirtualizedPath(); + string targetDir = settings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(settings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = settings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(templateConfig))!.AsObject()); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(settings, rpg, configModel, sourceMountPoint.Root); + + ParameterSetData parametersData = new ParameterSetData(runnableConfig); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Verifying the outputs + // + + string resultContent = settings.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourceFile")); + Assert.Equal(expectedSnippet, resultContent); + } + + [Fact] + public async Task CreateAsyncTest_UseBindValuesWithFileRename() + { + // + // Template content preparation + // + + var templateConfig = new + { + identity = "test.template", + name = "test", + shortName = "test", + symbols = new + { + hostPrefixed = new + { + type = "bind", + binding = "host:HostIdentifier", + fileRename = "_R1_" + }, + envPrefixed = new + { + type = "bind", + binding = "env:MYENVVAR", + fileRename = "_R2_" + }, + } + }; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, JsonSerializer.Serialize(templateConfig, new JsonSerializerOptions { WriteIndented = true }) }, + //content + { "_R1_.cs", string.Empty }, + { "_R2_.cs", string.Empty } + }; + + // + // Dependencies preparation and mounting + // + + Environment.SetEnvironmentVariable("MYENVVAR", "MyValue"); + IEnvironment environment = new DefaultEnvironment(); + + IEngineEnvironmentSettings settings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: "TestHost", virtualize: true, environment: environment); + ((TestHost)settings.Host).HostParamDefaults["HostIdentifier"] = "TestHost"; + string sourceBasePath = settings.GetTempVirtualizedPath(); + string targetDir = settings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(settings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = settings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(templateConfig))!.AsObject()); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(settings, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData(runnableConfig); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + _ = await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Verifying the outputs + // + + Assert.True(settings.Host.FileSystem.FileExists(Path.Combine(targetDir, "TestHost.cs"))); + Assert.True(settings.Host.FileSystem.FileExists(Path.Combine(targetDir, "MyValue.cs"))); + } + + [Fact] + public async Task CreateAsyncTest_UseBindValuesInMacros() + { + // + // Template content preparation + // + + var templateConfig = new + { + identity = "test.template", + name = "test", + shortName = "test", + symbols = new + { + hostPrefixed = new + { + type = "bind", + binding = "host:HostIdentifier", + }, + switchSymbol = new + { + type = "generated", + generator = "switch", + replaces = "%VAL%", + parameters = new + { + cases = new[] + { + new + { + condition = "(hostPrefixed == \"TestHost\")", + value = "Correct" + }, + new + { + condition = "(hostPrefixed != \"TestHost\")", + value = "Incorrect" + }, + } + } + } + } + }; + + string sourceSnippet = @"%VAL%"; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, JsonSerializer.Serialize(templateConfig, new JsonSerializerOptions { WriteIndented = true }) }, + //content + { "sourceFile", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + IEngineEnvironmentSettings settings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: "TestHost", virtualize: true); + ((TestHost)settings.Host).HostParamDefaults["HostIdentifier"] = "TestHost"; + string sourceBasePath = settings.GetTempVirtualizedPath(); + string targetDir = settings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(settings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = settings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(templateConfig))!.AsObject()); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(settings, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData(runnableConfig); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Verifying the outputs + // + + string resultContent = settings.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourceFile")); + Assert.Equal("Correct", resultContent); + + ((TestHost)settings.Host).HostParamDefaults["HostIdentifier"] = "NoTestHost"; + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Verifying the outputs + // + + resultContent = settings.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourceFile")); + Assert.Equal("Incorrect", resultContent); + } + + [Fact] + public async Task CreateAsyncTest_BindingConflict() + { + // + // Template content preparation + // + + var templateConfig = new + { + identity = "test.template", + name = "test", + shortName = "test", + symbols = new + { + testBindConflict = new + { + type = "bind", + binding = "Test", + replaces = "%VAL%" + }, + } + }; + + string sourceSnippet = @"%VAL%"; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, JsonSerializer.Serialize(templateConfig, new JsonSerializerOptions { WriteIndented = true }) }, + //content + { "sourceFile", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + + (Type, IIdentifiedComponent)[]? additionalComponents = new[] + { + (typeof(IBindSymbolSource), new TestBindSymbolSource(Guid.NewGuid())), + (typeof(IBindSymbolSource), new TestBindSymbolSource(Guid.NewGuid()) as IIdentifiedComponent) + }; + + List<(LogLevel, string)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + IEngineEnvironmentSettings settings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: "TestHost", virtualize: true, additionalComponents: additionalComponents, addLoggerProviders: new[] { loggerProvider }); + string sourceBasePath = settings.GetTempVirtualizedPath(); + string targetDir = settings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(settings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = settings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(templateConfig))!.AsObject()); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(settings, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData(runnableConfig); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Verifying the outputs + // + + string resultContent = settings.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourceFile")); + Assert.Equal("%VAL%", resultContent); + + IEnumerable warningMessages = loggedMessages.Where(log => log.Item1 == LogLevel.Warning).Select(log => log.Item2); + Assert.Equal(2, warningMessages.Count()); + Assert.Contains(string.Format(LocalizableStrings.BindSymbolEvaluator_Warning_ValueAvailableFromMultipleSources, "Test", "'Test', 'Test'", "'test:', 'test:'"), warningMessages); + Assert.Contains(string.Format(LocalizableStrings.BindSymbolEvaluator_Warning_EvaluationError, "testBindConflict"), warningMessages); + } + + [Fact] + public async Task CreateAsyncTest_ForcedPrefixBinding() + { + // + // Template content preparation + // + + var templateConfig = new + { + identity = "test.template", + name = "test", + shortName = "test", + symbols = new + { + notPrefixed = new + { + type = "bind", + binding = "Test", + replaces = "%VAL%" + }, + } + }; + + string sourceSnippet = @"%VAL%"; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, JsonSerializer.Serialize(templateConfig, new JsonSerializerOptions { WriteIndented = true }) }, + //content + { "sourceFile", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + + var symbolSource = new TestBindSymbolSource(Guid.NewGuid(), prefix: "test", requiresPrefixMatch: true); + (Type, IIdentifiedComponent)[]? additionalComponents = new[] + { + (typeof(IBindSymbolSource), (IIdentifiedComponent)symbolSource), + }; + + List<(LogLevel, string)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + IEngineEnvironmentSettings settings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: "TestHost", virtualize: true, additionalComponents: additionalComponents, addLoggerProviders: new[] { loggerProvider }); + string sourceBasePath = settings.GetTempVirtualizedPath(); + string targetDir = settings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(settings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = settings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(templateConfig))!.AsObject()); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(settings, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData(runnableConfig); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Verifying the outputs + // + + string resultContent = settings.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourceFile")); + Assert.Equal("%VAL%", resultContent); + + IEnumerable warningMessages = loggedMessages.Where(log => log.Item1 == LogLevel.Warning).Select(log => log.Item2); + Assert.Single(warningMessages); + Assert.Contains(string.Format(LocalizableStrings.BindSymbolEvaluator_Warning_EvaluationError, "notPrefixed"), warningMessages); + Assert.False(symbolSource.GetBoundValueAsync_WasCalled); + } + + [Fact] + public async Task CreateAsyncTest_CanUseDefaultValue() + { + // + // Template content preparation + // + + var templateConfig = new + { + identity = "test.template", + name = "test", + shortName = "test", + symbols = new + { + hostPrefixed = new + { + type = "bind", + binding = "host:HostIdentifier", + replaces = "%R1%", + defaultValue = "hostDefault" + }, + envPrefixed = new + { + type = "bind", + binding = "env:MYENVVAR", + replaces = "%R2%", + defaultValue = "envDefault" + }, + hostUnprefixed = new + { + type = "bind", + binding = "unknown", + replaces = "%R3%", + defaultValue = "expectedDefValue" + }, + } + }; + + string sourceSnippet = """ + %R1% + %R2% + %R3% + """; + + string expectedSnippet = """ + TestHost + MyValue + expectedDefValue + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, JsonSerializer.Serialize(templateConfig, new JsonSerializerOptions { WriteIndented = true }) }, + //content + { "sourceFile", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + + Environment.SetEnvironmentVariable("MYENVVAR", "MyValue"); + IEnvironment environment = new DefaultEnvironment(); + + IEngineEnvironmentSettings settings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: "TestHost", virtualize: true, environment: environment); + ((TestHost)settings.Host).HostParamDefaults["HostIdentifier"] = "TestHost"; + string sourceBasePath = settings.GetTempVirtualizedPath(); + string targetDir = settings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(settings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = settings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(templateConfig))!.AsObject()); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(settings, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData(runnableConfig); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Verifying the outputs + // + + string resultContent = settings.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourceFile")); + Assert.Equal(expectedSnippet, resultContent); + } + + [Fact] + public async Task CreateAsyncTest_CanConvertValueToDataType() + { + // + // Template content preparation + // + + var templateConfig = new + { + identity = "test.template", + symbols = new + { + envPrefixed = new + { + type = "bind", + binding = "env:MYENVVAR", + dataType = "string" + }, + computed = new + { + type = "computed", + value = "envPrefixed != \"\"" + } + } + }; + + string sourceSnippet = """ + //#if (computed) + success + //#endif + """; + + string expectedSnippet = """ + success + + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, JsonSerializer.Serialize(templateConfig, new JsonSerializerOptions { WriteIndented = true }) }, + //content + { "sourceFile.cs", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + + //int + Environment.SetEnvironmentVariable("MYENVVAR", "100"); + IEnvironment environment = new DefaultEnvironment(); + + IEngineEnvironmentSettings settings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: "TestHost", virtualize: true, environment: environment); + string sourceBasePath = settings.GetTempVirtualizedPath(); + string targetDir = settings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(settings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = settings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(templateConfig))!.AsObject()); + RunnableProjectConfig runnableConfig = new RunnableProjectConfig(settings, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData(runnableConfig); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Verifying the outputs + // + + string resultContent = settings.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourceFile.cs")); + Assert.Equal(expectedSnippet, resultContent); + } + + [Fact] + public async Task CreateAsyncTest_NoWarningOnUnknownBindingWithDefaultValue() + { + // + // Template content preparation + // + + var templateConfig = new + { + identity = "test.template", + symbols = new + { + env1 = new + { + type = "bind", + binding = "env:UNKNOWN1", + replaces = "%R1%", + defaultValue = "envDefault" + }, + env2 = new + { + type = "bind", + binding = "env:UNKNOWN2", + replaces = "%R2%" + }, + } + }; + + string sourceSnippet = """ + %R1% + %R2% + """; + + string expectedSnippet = """ + envDefault + %R2% + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, JsonSerializer.Serialize(templateConfig, new JsonSerializerOptions { WriteIndented = true }) }, + //content + { "sourceFile", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + IEngineEnvironmentSettings settings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: "TestHost", virtualize: true, addLoggerProviders: new[] { loggerProvider }); + + string sourceBasePath = settings.GetTempVirtualizedPath(); + string targetDir = settings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(settings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = settings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(templateConfig))!.AsObject()); + RunnableProjectConfig runnableConfig = new RunnableProjectConfig(settings, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData(runnableConfig); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Verifying the outputs + // + + string resultContent = settings.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourceFile")); + Assert.Equal(expectedSnippet, resultContent); + + (LogLevel, string Message) warningMessage = Assert.Single(loggedMessages, lm => lm.Level == LogLevel.Warning); + Assert.Equal("Failed to evaluate bind symbol 'env2', it will be skipped.", warningMessage.Message); + Assert.Contains(loggedMessages, lm => lm.Message == "Failed to evaluate bind symbol 'env1', the returned value is null. The default value 'envDefault' is used instead."); + } + + private class TestBindSymbolSource : IBindSymbolSource + { + public TestBindSymbolSource(Guid guid, string prefix = "test", bool requiresPrefixMatch = false) + { + Id = guid; + SourcePrefix = prefix; + RequiresPrefixMatch = requiresPrefixMatch; + } + + public string DisplayName => "Test"; + + public string? SourcePrefix { get; } + + public int Priority => 0; + + public Guid Id { get; } + + public bool RequiresPrefixMatch { get; } + + public bool GetBoundValueAsync_WasCalled { get; private set; } + + public Task GetBoundValueAsync(IEngineEnvironmentSettings settings, string bindname, CancellationToken cancellationToken) + { + GetBoundValueAsync_WasCalled = true; + return Task.FromResult((string?)("TestVal" + Id.ToString())); + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Fakes/FakeMacro.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Fakes/FakeMacro.cs new file mode 100644 index 000000000000..773b59768835 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Fakes/FakeMacro.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.Fakes +{ + internal class FakeMacro : BaseGeneratedSymbolMacro + { + public override string Type => "fake"; + + public override Guid Id => new Guid("{3DBC6AAB-5D13-40E9-3EC8-0467A7AA7334}"); + + public override FakeMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig deferredConfig) => new(this, deferredConfig); + + public override void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variableCollection, FakeMacroConfig config) + { + string fakeMessage = $"Hello {config.NameToGreet}!"; + variableCollection[config.VariableName] = fakeMessage; + } + + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Fakes/FakeMacroConfig.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Fakes/FakeMacroConfig.cs new file mode 100644 index 000000000000..0636eea37224 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Fakes/FakeMacroConfig.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.Fakes +{ + internal class FakeMacroConfig : BaseMacroConfig, IMacroConfigDependency + { + public FakeMacroConfig(FakeMacro macro, string variableName, string sourceName, string nameToGreet = "", string? dataType = null) : base(macro, variableName, dataType) + { + Source = sourceName; + NameToGreet = nameToGreet; + } + + public FakeMacroConfig(FakeMacro macro, IGeneratedSymbolConfig generatedSymbolConfig) : base(macro, generatedSymbolConfig.VariableName) + { + Source = GetMandatoryParameterValue(generatedSymbolConfig, "source"); + Fallback = GetOptionalParameterValue(generatedSymbolConfig, "fallback"); + NameToGreet = GetMandatoryParameterValue(generatedSymbolConfig, "name"); + } + + public string Source { get; } + + public string? Fallback { get; } + + public string NameToGreet { get; } + + public void ResolveSymbolDependencies(IReadOnlyList symbols) + { + MacroDependenciesResolved = true; + PopulateMacroConfigDependencies(Source, symbols); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroProcessorTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroProcessorTests.cs new file mode 100644 index 000000000000..07a4bc6fb5ec --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroProcessorTests.cs @@ -0,0 +1,490 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FakeItEasy; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.Fakes; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests +{ + public class MacroProcessorTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public MacroProcessorTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Fact] + public void CanThrow_WhenCannotProcessMacro() + { + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true, additionalComponents: new[] { (typeof(IMacro), (IIdentifiedComponent)new FailMacro()) }); + + var macros = new[] { new FailMacroConfig("test") }; + + MacroProcessingException e = Assert.Throws(() => MacroProcessor.ProcessMacros(engineEnvironmentSettings, macros, new VariableCollection())); + Assert.Equal("Failed to evaluate", e.InnerException?.Message); + } + + [Fact] + public void CanPrintWarningOnUnknownMacroConfig() + { + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment( + virtualize: true, environment: A.Fake(), addLoggerProviders: new[] { loggerProvider }); + var fakeMacroConfig = new FakeMacroConfig(new FakeMacro(), "testVariable", "dummy"); + fakeMacroConfig.ResolveSymbolDependencies(new List()); + + var macroConfigs = new[] { fakeMacroConfig }; + + MacroProcessor.ProcessMacros(engineEnvironmentSettings, macroConfigs, new VariableCollection()); + + Assert.True(loggedMessages.Count == 1); + Assert.Equal("Generated symbol 'testVariable': type 'fake' is unknown, processing is skipped.", loggedMessages.First().Message); + } + + [Fact] + public void CanProcessMacroWithCustomMacroAsDependency() + { + var fakeMacroVariableName = "testVariable"; + var coalesceVariableName = "coalesceTest"; + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + var generatedConfig = A.Fake(); + A.CallTo(() => generatedConfig.Parameters).Returns(new Dictionary() + { + { "sourceVariableName", JExtensions.ToJsonString("dummy") }, + { "fallbackVariableName", JExtensions.ToJsonString(fakeMacroVariableName) } + }); + A.CallTo(() => generatedConfig.VariableName).Returns(coalesceVariableName); + + var coalesceMacroConfig = new CoalesceMacroConfig(new CoalesceMacro(), generatedConfig); + coalesceMacroConfig.ResolveSymbolDependencies(new List() { fakeMacroVariableName }); + + var engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true, environment: A.Fake(), additionalComponents: new[] { (typeof(IMacro), (IIdentifiedComponent)new FakeMacro()) }, addLoggerProviders: new[] { loggerProvider }); + + var fakeMacro = new FakeMacro(); + var customGeneratedConfig = A.Fake(); + A.CallTo(() => customGeneratedConfig.Parameters).Returns(new Dictionary() + { + { "source", JExtensions.ToJsonString("dummy") }, + { "name", JExtensions.ToJsonString(fakeMacroVariableName) }, + }); + A.CallTo(() => customGeneratedConfig.VariableName).Returns(fakeMacroVariableName); + var fakeMacroConfig = new FakeMacroConfig(new FakeMacro(), customGeneratedConfig); + fakeMacroConfig.ResolveSymbolDependencies(new List()); + var variableCollection = new Dictionary() { { fakeMacroVariableName, fakeMacro } }; + + MacroProcessor.ProcessMacros(engineEnvironmentSettings, new[] { (IMacroConfig)coalesceMacroConfig, fakeMacroConfig }, new VariableCollection(default, variableCollection)); + + Assert.True(variableCollection.Count == 2); + Assert.True(variableCollection.Values.Select(v => v.GetHashCode() == fakeMacro.GetHashCode()).Count() == 2); + variableCollection.Select(v => v.Key).Should().Equal(new[] { fakeMacroVariableName, coalesceVariableName }); + Assert.True(coalesceMacroConfig.Dependencies.Count == 1); + Assert.Equal(fakeMacroVariableName, coalesceMacroConfig.Dependencies.First()); + } + + [Fact] + public void CanProcessCustomMacroWithDeps() + { + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + string coalesceMacroName = "coalesceMacro"; + IGeneratedSymbolConfig coalesceGeneratedConfig = A.Fake(); + A.CallTo(() => coalesceGeneratedConfig.Parameters).Returns(new Dictionary() + { + { "sourceVariableName", JExtensions.ToJsonString("dummy") }, + { "fallbackVariableName", JExtensions.ToJsonString("dummy2") } + }); + A.CallTo(() => coalesceGeneratedConfig.VariableName).Returns(coalesceMacroName); + CoalesceMacroConfig coalesceMacroConfig = new(new CoalesceMacro(), coalesceGeneratedConfig); + + string switchMacroName = "switchMacro"; + SwitchMacroConfig switchMacroConfig = new( + new SwitchMacro(), + switchMacroName, + "C++", + "string", + new List<(string?, string)>() + { + ("(dummy == \"A\")", "val1"), + (null, "defVal") + }); + switchMacroConfig.ResolveSymbolDependencies(new List()); + + string customMacroName = "customMacro"; + IGeneratedSymbolConfig customGeneratedConfig = A.Fake(); + A.CallTo(() => customGeneratedConfig.Parameters).Returns(new Dictionary() + { + { "source", JExtensions.ToJsonString(switchMacroName) }, + { "name", JExtensions.ToJsonString("dummy") } + }); + A.CallTo(() => customGeneratedConfig.VariableName).Returns(customMacroName); + FakeMacroConfig customMacroConfig = new(new FakeMacro(), customGeneratedConfig); + customMacroConfig.ResolveSymbolDependencies(new List()); + + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment( + virtualize: true, + environment: A.Fake(), + addLoggerProviders: new[] { loggerProvider }, + additionalComponents: new[] { (typeof(IMacro), (IIdentifiedComponent)new FakeMacro()) }); + VariableCollection variableCollection = new() + { + ["dummy"] = "A", + ["dummy2"] = "B", + ["testVariable"] = "test" + }; + + IReadOnlyList macroConfigs = new[] { (IMacroConfig)customMacroConfig, coalesceMacroConfig, switchMacroConfig }; + + IReadOnlyList sortedMacroConfigs = MacroProcessor.SortMacroConfigsByDependencies(new[] { "dummy", "dummy2", "coalesceMacro", "switchMacro", "customMacro", "testVariable" }, macroConfigs); + MacroProcessor.ProcessMacros(engineEnvironmentSettings, sortedMacroConfigs, variableCollection); + + // Custom macro was processed without errors + Assert.True(!loggedMessages.Any(lm => lm.Level == LogLevel.Error)); + Assert.Equal("A", variableCollection["coalesceMacro"]); + Assert.Equal("val1", variableCollection["switchMacro"]); + Assert.Equal("Hello dummy!", variableCollection["customMacro"]); + } + + [Fact] + public void CanSortCollectionWithCustomMacroWithDeps() + { + var coalesceMacroName = "coalesceMacro"; + var coalesceGeneratedConfig = A.Fake(); + A.CallTo(() => coalesceGeneratedConfig.Parameters).Returns(new Dictionary() + { + { "sourceVariableName", JExtensions.ToJsonString("dummy") }, + { "fallbackVariableName", JExtensions.ToJsonString("dummy") } + }); + A.CallTo(() => coalesceGeneratedConfig.VariableName).Returns(coalesceMacroName); + var coalesceMacroConfig = new CoalesceMacroConfig(new CoalesceMacro(), coalesceGeneratedConfig); + + var switchMacroName = "switchMacro"; + var switchMacroConfig = new SwitchMacroConfig(new SwitchMacro(), switchMacroName, string.Empty, string.Empty, new List<(string?, string)>()); + + var customMacroName = "customMacro"; + var customGeneratedConfig = A.Fake(); + A.CallTo(() => customGeneratedConfig.Parameters).Returns(new Dictionary() + { + { "source", JExtensions.ToJsonString(coalesceMacroName) }, + { "fallback", JExtensions.ToJsonString(switchMacroName) }, + { "name", JExtensions.ToJsonString("dummy") } + }); + A.CallTo(() => customGeneratedConfig.VariableName).Returns(customMacroName); + + var customMacroConfig = new FakeMacroConfig(new FakeMacro(), customGeneratedConfig); + _environmentSettingsHelper.CreateEnvironment( + virtualize: true, + environment: A.Fake(), + additionalComponents: new[] { (typeof(IMacro), (IIdentifiedComponent)new FakeMacro()) }); + + var sortedItems = MacroProcessor.SortMacroConfigsByDependencies(new[] { customMacroName, switchMacroName, coalesceMacroName }, new[] { (BaseMacroConfig)switchMacroConfig, customMacroConfig, coalesceMacroConfig }); + + sortedItems.Select(si => si.VariableName).Should().Equal(new[] { switchMacroName, coalesceMacroName, customMacroName }); + } + + [Fact] + public void CanSortMacrosWithDependencies() + { + var switchMacroName = "switchMacro"; + var coalesceMacroName = "coalesceMacro"; + var evaluateMacroName = "evaluateMacro"; + var joinMacroName = "joinMacro"; + + var symbols = new string[] { switchMacroName, coalesceMacroName, evaluateMacroName, joinMacroName }; + + var evaluateMacroConfig = new EvaluateMacroConfig(evaluateMacroName, string.Empty, "condition"); + var fakeCoalesceGeneratedSymbols = A.Fake(); + A.CallTo(() => fakeCoalesceGeneratedSymbols.Parameters).Returns(new Dictionary() + { + { "sourceVariableName", JExtensions.ToJsonString(evaluateMacroName) }, + { "fallbackVariableName", JExtensions.ToJsonString("dummy") } + }); + A.CallTo(() => fakeCoalesceGeneratedSymbols.VariableName).Returns(coalesceMacroName); + var coalesceMacroConfig = new CoalesceMacroConfig(new CoalesceMacro(), fakeCoalesceGeneratedSymbols); + + var switchMacroConfig = new SwitchMacroConfig(new SwitchMacro(), switchMacroName, string.Empty, string.Empty, new List<(string?, string)>() + { + ("evaluateMacro == 'blank'", "true"), + ("coalesceMacro == 'blank'", "false") + + }); + + var fakeJoinGeneratedSymbols = A.Fake(); + A.CallTo(() => fakeJoinGeneratedSymbols.VariableName).Returns(joinMacroName); + A.CallTo(() => fakeJoinGeneratedSymbols.Parameters).Returns(new Dictionary() + { + { "symbols", /*lang=json,strict*/ "[ {\"value\":\"switchMacro\" } ]" }, + { "fallbackVariableName", JExtensions.ToJsonString("dummy") } + }); + var joinMacroConfig = new JoinMacroConfig(new JoinMacro(), fakeJoinGeneratedSymbols); + var macroConfigs = new[] { (BaseMacroConfig)joinMacroConfig, switchMacroConfig, evaluateMacroConfig, coalesceMacroConfig }; + + var sortedItems = MacroProcessor.SortMacroConfigsByDependencies(symbols, macroConfigs); + + sortedItems.Select(si => si.VariableName).Should().Equal(new[] { evaluateMacroName, coalesceMacroName, switchMacroName, joinMacroName }); + } + + [Fact] + public void CanThrowErrorOnSortWhenMacrosHaveDepsCircle() + { + var switchMacroName = "switchMacro"; + var coalesceMacroName = "coalesceMacro"; + + var symbols = new string[] { switchMacroName, coalesceMacroName }; + + var fakeCoalesceGeneratedSymbols = A.Fake(); + A.CallTo(() => fakeCoalesceGeneratedSymbols.Parameters).Returns(new Dictionary() + { + { "sourceVariableName", JExtensions.ToJsonString(switchMacroName) }, + { "fallbackVariableName", JExtensions.ToJsonString("dummy") } + }); + A.CallTo(() => fakeCoalesceGeneratedSymbols.VariableName).Returns(coalesceMacroName); + var coalesceMacroConfig = new CoalesceMacroConfig(new CoalesceMacro(), fakeCoalesceGeneratedSymbols); + + var switchMacroConfig = new SwitchMacroConfig(new SwitchMacro(), switchMacroName, string.Empty, string.Empty, new List<(string?, string)> + { + ("coalesceMacroName == 'blank'", "true") + }); + var macroConfigs = new[] { (BaseMacroConfig)switchMacroConfig, coalesceMacroConfig }; + + Action sorting = () => { MacroProcessor.SortMacroConfigsByDependencies(symbols, macroConfigs); }; + sorting.Should().Throw() + .Which.Message.Should().Contain("Parameter conditions contain cyclic dependency: [switchMacro, coalesceMacro, switchMacro] that is preventing deterministic evaluation."); + } + + [Fact] + public void CanRunDeterministically_ComputedMacros() + { + UndeterministicMacro macro = new UndeterministicMacro(); + + IEnvironment environment = A.Fake(); + A.CallTo(() => environment.GetEnvironmentVariable("TEMPLATE_ENGINE_ENABLE_DETERMINISTIC_MODE")).Returns("true"); + + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true, environment: environment, additionalComponents: new[] { (typeof(IMacro), (IIdentifiedComponent)macro) }); + + IReadOnlyList macros = new[] { (IMacroConfig)new UndeterministicMacroConfig("test"), new GuidMacroConfig("test-guid", "string", "Nn", "n") }; + + IVariableCollection collection = new VariableCollection(); + + MacroProcessor.ProcessMacros(engineEnvironmentSettings, macros, collection); + Assert.Equal("deterministic", collection["test"]); + Assert.Equal(new Guid("12345678-1234-1234-1234-1234567890AB").ToString("n"), collection["test-guid"]); + + A.CallTo(() => environment.GetEnvironmentVariable("TEMPLATE_ENGINE_ENABLE_DETERMINISTIC_MODE")).Returns("false"); + collection = new VariableCollection(); + + MacroProcessor.ProcessMacros(engineEnvironmentSettings, macros, collection); + Assert.Equal("undeterministic", collection["test"]); + Assert.NotEqual(new Guid("12345678-1234-1234-1234-1234567890AB").ToString("n"), collection["test-guid"]); + } + + [Fact] + public void CanProcessMacroWithCustomMacroAsDependency_IndependentImplementation() + { + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + + string coalesceMacroName = "coalesceMacro"; + IGeneratedSymbolConfig coalesceGeneratedConfig = A.Fake(); + A.CallTo(() => coalesceGeneratedConfig.Parameters).Returns(new Dictionary() + { + { "sourceVariableName", JExtensions.ToJsonString("dummy") }, + { "fallbackVariableName", JExtensions.ToJsonString("dummy2") } + }); + A.CallTo(() => coalesceGeneratedConfig.VariableName).Returns(coalesceMacroName); + CoalesceMacroConfig coalesceMacroConfig = new(new CoalesceMacro(), coalesceGeneratedConfig); + + string switchMacroName = "switchMacro"; + SwitchMacroConfig switchMacroConfig = new( + new SwitchMacro(), + switchMacroName, + "C++", + "string", + new List<(string?, string)>() + { + ("(dummy == \"A\")", "val1"), + (null, "defVal") + }); + + string customMacroName = "customMacro"; + IGeneratedSymbolConfig customGeneratedConfig = A.Fake(); + A.CallTo(() => customGeneratedConfig.Parameters).Returns(new Dictionary() + { + { "source", JExtensions.ToJsonString(switchMacroName) }, + }); + A.CallTo(() => customGeneratedConfig.VariableName).Returns(customMacroName); + DependencyMacroConfig customMacroConfig = new(customMacroName, switchMacroName); + + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment( + virtualize: true, + environment: A.Fake(), + addLoggerProviders: new[] { loggerProvider }, + additionalComponents: new[] { (typeof(IMacro), (IIdentifiedComponent)new DependencyMacro()) }); + VariableCollection variableCollection = new() + { + ["dummy2"] = "B", + }; + + IReadOnlyList macroConfigs = new[] { (IMacroConfig)switchMacroConfig, customMacroConfig, coalesceMacroConfig }; + + IReadOnlyList sortedMacroConfigs = MacroProcessor.SortMacroConfigsByDependencies(new[] { "dummy", "dummy2", coalesceMacroName, switchMacroName, customMacroName }, macroConfigs); + MacroProcessor.ProcessMacros(engineEnvironmentSettings, sortedMacroConfigs, variableCollection); + + // Custom macro was processed without errors + Assert.True(!loggedMessages.Any(lm => lm.Level == LogLevel.Error)); + Assert.Equal("B", variableCollection[coalesceMacroName]); + Assert.Equal("defVal", variableCollection[switchMacroName]); + Assert.Equal("defVal", variableCollection[customMacroName]); + } + + private class FailMacro : IMacro, IGeneratedSymbolMacro + { + public string Type => "fail"; + + public Guid Id { get; } = new Guid("{3DBC6AAB-5D13-40E9-9EC8-0467A7AA7335}"); + + public IMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig generatedSymbolConfig) => new FailMacroConfig(generatedSymbolConfig.VariableName); + + public void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, FailMacroConfig config) => throw new Exception("Failed to evaluate"); + + public void EvaluateConfig(IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, IMacroConfig config) => throw new Exception("Failed to evaluate"); + } + + private class FailConfigMacro : IMacro, IGeneratedSymbolMacro + { + public string Type => "fail"; + + public Guid Id { get; } = new Guid("{3DBC6AAB-5D13-40E9-9EC8-0467A7AA7335}"); + + public IMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig generatedSymbolConfig) => new FailMacroConfig(generatedSymbolConfig.VariableName); + + public void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, FailMacroConfig config) => throw new Exception("Failed to evaluate"); + + public void EvaluateConfig(IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, IMacroConfig config) => throw new TemplateAuthoringException("bad config"); + } + + private class FailMacroConfig : BaseMacroConfig + { + public FailMacroConfig(string variableName) : base("fail", variableName, "string") + { + } + } + + private class UndeterministicMacro : IDeterministicModeMacro, IDeterministicModeMacro, IGeneratedSymbolMacro, IGeneratedSymbolMacro + { + public string Type => "undeterministic"; + + public Guid Id { get; } = new Guid("{3DBC6AAB-5D13-40E9-9EC8-0467A7AA7335}"); + + public UndeterministicMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig generatedSymbolConfig) + { + return new UndeterministicMacroConfig(generatedSymbolConfig.VariableName); + } + + public void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, UndeterministicMacroConfig config) + { + variables[config.VariableName] = "undeterministic"; + } + + public void EvaluateConfig(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, IMacroConfig config) + { + variables[config.VariableName] = "undeterministic"; + } + + public void EvaluateDeterministically(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, UndeterministicMacroConfig config) + { + variables[config.VariableName] = "deterministic"; + } + + public void EvaluateConfigDeterministically(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, IMacroConfig config) + { + variables[config.VariableName] = "deterministic"; + } + + IMacroConfig IGeneratedSymbolMacro.CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig generatedSymbolConfig) + { + return new UndeterministicMacroConfig(generatedSymbolConfig.VariableName); + } + } + + private class UndeterministicMacroConfig : IMacroConfig + { + public UndeterministicMacroConfig(string variableName) + { + VariableName = variableName; + } + + public string VariableName { get; } + + public string Type => "undeterministic"; + } + + private class DependencyMacro : IGeneratedSymbolMacro, IGeneratedSymbolMacro + { + public string Type => "dependency"; + + public Guid Id { get; } = new Guid("{545064DA-74B3-4A78-8B1A-A6B17B36E48D}"); + + public IMacroConfig CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig generatedSymbolConfig) + { + return new DependencyMacroConfig(generatedSymbolConfig.VariableName, generatedSymbolConfig.Parameters["dependentSymbolName"]); + } + + public void Evaluate(IEngineEnvironmentSettings environmentSettings, IVariableCollection variables, DependencyMacroConfig config) + { + variables[config.VariableName] = variables[config.DependentSymbolName]; + } + + public void EvaluateConfig(IEngineEnvironmentSettings environmentSettings, IVariableCollection vars, IMacroConfig config) + { + if (config is DependencyMacroConfig dmc) + { + vars[config.VariableName] = vars[dmc.DependentSymbolName]; + } + } + + DependencyMacroConfig IGeneratedSymbolMacro.CreateConfig(IEngineEnvironmentSettings environmentSettings, IGeneratedSymbolConfig generatedSymbolConfig) + { + return new DependencyMacroConfig(generatedSymbolConfig.VariableName, generatedSymbolConfig.Parameters["dependentSymbolName"]); + } + } + + private class DependencyMacroConfig : IMacroConfig, IMacroConfigDependency + { + public DependencyMacroConfig(string variableName, string dependentSymbolName) + { + VariableName = variableName; + DependentSymbolName = dependentSymbolName; + } + + public string VariableName { get; } + + public string DependentSymbolName { get; } + + public string Type => "dependency"; + + public HashSet Dependencies { get; private set; } = new HashSet(); + + public void ResolveSymbolDependencies(IReadOnlyList symbols) + { + Dependencies = new HashSet() + { + DependentSymbolName + }; + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/CaseChangeMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/CaseChangeMacroTests.cs new file mode 100644 index 000000000000..a38731927aa7 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/CaseChangeMacroTests.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class CaseChangeMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public CaseChangeMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + [Fact(DisplayName = nameof(TestCaseChangeToLowerConfig))] + public void TestCaseChangeToLowerConfig() + { + string variableName = "myString"; + string sourceVariable = "sourceString"; + bool toLower = true; + + CaseChangeMacro macro = new(); + CaseChangeMacroConfig macroConfig = new(macro, variableName, null, sourceVariable, toLower); + + IVariableCollection variables = new VariableCollection(); + string sourceValue = "Original Value SomethingCamelCase"; + variables[sourceVariable] = sourceValue; + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + + string convertedValue = (string)variables[variableName]; + Assert.Equal(convertedValue, sourceValue.ToLower()); + } + + [Fact(DisplayName = nameof(TestCaseChangeToUpperConfig))] + public void TestCaseChangeToUpperConfig() + { + string variableName = "myString"; + string sourceVariable = "sourceString"; + bool toLower = false; + + CaseChangeMacro macro = new(); + CaseChangeMacroConfig macroConfig = new(macro, variableName, null, sourceVariable, toLower); + + IVariableCollection variables = new VariableCollection(); + + string sourceValue = "Original Value SomethingCamelCase"; + variables[sourceVariable] = sourceValue; + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + + string convertedValue = (string)variables[variableName]; + Assert.Equal(convertedValue, sourceValue.ToUpper()); + } + + [Fact] + public void GeneratedSymbolTest() + { + string variableName = "myString"; + string sourceVariable = "sourceString"; + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString(sourceVariable) }, + { "toLower", JExtensions.ToJsonString(false) } + }; + CaseChangeMacro macro = new(); + GeneratedSymbol symbol = new(variableName, macro.Type, jsonParameters); + + IVariableCollection variables = new VariableCollection(); + + string sourceValue = "Original Value SomethingCamelCase"; + variables[sourceVariable] = sourceValue; + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + + string convertedValue = (string)variables[variableName]; + Assert.Equal(convertedValue, sourceValue.ToUpper()); + } + + [Fact] + public void MissingSourceSymbolTest() + { + string variableName = "myString"; + string sourceVariable = "sourceString"; + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString(sourceVariable) }, + { "toLower", JExtensions.ToJsonString(false) } + }; + CaseChangeMacro macro = new(); + GeneratedSymbol symbol = new(variableName, macro.Type, jsonParameters); + + VariableCollection variables = new(); + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + + Assert.False(variables.ContainsKey(variableName)); + } + + [Fact] + [Obsolete("IMacro.EvaluateConfig is deprecated")] + public void ObsoleteEvaluateConfigTest() + { + string variableName = "myString"; + string sourceVariable = "sourceString"; + bool toLower = true; + + CaseChangeMacro macro = new(); + CaseChangeMacroConfig macroConfig = new(macro, variableName, null, sourceVariable, toLower); + + IVariableCollection variables = new VariableCollection(); + string sourceValue = "Original Value SomethingCamelCase"; + variables[sourceVariable] = sourceValue; + + macro.EvaluateConfig(_engineEnvironmentSettings, variables, macroConfig); + + string convertedValue = (string)variables[variableName]; + Assert.Equal(convertedValue, sourceValue.ToLower()); + } + + [Fact] + public void InvalidConfigurationTest() + { + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + CaseChangeMacro macro = new(); + GeneratedSymbol symbol = new("test", macro.Type, jsonParameters); + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, symbol)); + Assert.Equal("Generated symbol 'test' of type 'casing' should have 'source' property defined.", ex.Message); + } + + [Fact] + public void DefaultConfigurationTest() + { + string variableName = "myString"; + string sourceVariable = "sourceString"; + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString(sourceVariable) }, + }; + CaseChangeMacro macro = new(); + GeneratedSymbol symbol = new(variableName, macro.Type, jsonParameters); + + VariableCollection variables = new(); + string sourceValue = "AbC"; + variables[sourceVariable] = sourceValue; + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + + //default configuration is lower-case + Assert.Equal("abc", variables[variableName]); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/CoalesceMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/CoalesceMacroTests.cs new file mode 100644 index 000000000000..de7959a3e7b3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/CoalesceMacroTests.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class CoalesceMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public CoalesceMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Theory] + [InlineData(null, null, null, null)] + [InlineData("", "", null, "")] + [InlineData(null, "fallback", null, "fallback")] + [InlineData("", "fallback", null, "fallback")] + [InlineData("def", "fallback", "def", "fallback")] + [InlineData("def", "fallback", "", "def")] + public void CoalesceMacroTest(string? sourceValue, string? fallbackValue, string? defaultValue, string? expectedResult) + { + CoalesceMacro macro = new(); + CoalesceMacroConfig macroConfig = new(macro, "test", "string", "varA", defaultValue, "varB"); + + VariableCollection variables = new(); + if (sourceValue != null) + { + variables["varA"] = sourceValue; + } + if (fallbackValue != null) + { + variables["varB"] = fallbackValue; + } + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + + if (expectedResult == null) + { + Assert.False(variables.ContainsKey("test")); + } + else + { + Assert.Equal(expectedResult, variables["test"]); + } + } + + [Theory] + [InlineData(null, null, null, null)] + [InlineData("", "", null, "")] + [InlineData(null, "fallback", null, "fallback")] + [InlineData("", "fallback", null, "fallback")] + [InlineData("def", "fallback", "def", "fallback")] + [InlineData("def", "fallback", "", "def")] + public void GeneratedSymbolTest(string? sourceValue, string? fallbackValue, string? defaultValue, string? expectedResult) + { + CoalesceMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "sourceVariableName", JExtensions.ToJsonString("varA") }, + { "fallbackVariableName", JExtensions.ToJsonString("varB") } + }; + if (defaultValue != null) + { + jsonParameters["defaultValue"] = JExtensions.ToJsonString(defaultValue); + } + + VariableCollection variables = new(); + if (sourceValue != null) + { + variables["varA"] = sourceValue; + } + if (fallbackValue != null) + { + variables["varB"] = fallbackValue; + } + + macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "coalesce", jsonParameters)); + + if (expectedResult == null) + { + Assert.False(variables.ContainsKey("test")); + } + else + { + Assert.Equal(expectedResult, variables["test"]); + } + } + + [Fact] + public void GeneratedSymbolTest_DefaultValueLeadsToFallback() + { + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true, addLoggerProviders: new[] { loggerProvider }); + + CoalesceMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "sourceVariableName", JExtensions.ToJsonString("varA") }, + { "fallbackVariableName", JExtensions.ToJsonString("varB") }, + { "defaultValue", JExtensions.ToJsonString("0") } + }; + + VariableCollection variables = new() + { + ["varA"] = 0, + ["varB"] = 10 + }; + + macro.Evaluate(environmentSettings, variables, new GeneratedSymbol("test", "coalesce", jsonParameters)); + Assert.Equal(10, variables["test"]); + Assert.Equal("[CoalesceMacro]: 'test': source value '0' is not used, because it is equal to default value '0'.", loggedMessages.First().Message); + } + + [Fact] + public void GeneratedSymbolTest_ExplicitDefaultValuesArePreserved() + { + List<(LogLevel Level, string Message)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true, addLoggerProviders: new[] { loggerProvider }); + + CoalesceMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "sourceVariableName", JExtensions.ToJsonString("varA") }, + { "fallbackVariableName", JExtensions.ToJsonString("varB") } + }; + + VariableCollection variables = new() + { + ["varA"] = 0, + ["varB"] = 10 + }; + + macro.Evaluate(environmentSettings, variables, new GeneratedSymbol("test", "coalesce", jsonParameters)); + Assert.Equal(0, variables["test"]); + } + + [Fact] + public void InvalidConfigurationTest_Source() + { + CoalesceMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "fallbackVariableName", JExtensions.ToJsonString("varB") } + }; + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "coalesce", jsonParameters))); + Assert.Equal("Generated symbol 'test' of type 'coalesce' should have 'sourceVariableName' property defined.", ex.Message); + } + + [Fact] + public void InvalidConfigurationTest_Fallback() + { + CoalesceMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "sourceVariableName", JExtensions.ToJsonString("varA") } + }; + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "coalesce", jsonParameters))); + Assert.Equal("Generated symbol 'test' of type 'coalesce' should have 'fallbackVariableName' property defined.", ex.Message); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/ConstantMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/ConstantMacroTests.cs new file mode 100644 index 000000000000..5fb69c72e42e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/ConstantMacroTests.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class ConstantMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public ConstantMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + [Fact(DisplayName = nameof(TestConstantConfig))] + public void TestConstantConfig() + { + string variableName = "myConstant"; + string value = "1048576"; + ConstantMacro macro = new(); + ConstantMacroConfig macroConfig = new(macro, null, variableName, value); + + IVariableCollection variables = new VariableCollection(); + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + + Assert.Equal(value, variables[variableName]); + } + + [Fact] + public void GeneratedSymbolTest() + { + string variableName = "myConstant"; + string value = "1048576"; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "value", JExtensions.ToJsonString(value) } + }; + + ConstantMacro macro = new(); + GeneratedSymbol symbol = new(variableName, macro.Type, jsonParameters); + + IVariableCollection variables = new VariableCollection(); + + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + Assert.Equal(value, variables[variableName]); + } + + [Fact] + public void GeneratedSymbolTest_Bool() + { + string variableName = "myConstant"; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "value", JExtensions.ToJsonString(true) } + }; + + ConstantMacro macro = new(); + GeneratedSymbol symbol = new(variableName, macro.Type, jsonParameters); + + IVariableCollection variables = new VariableCollection(); + + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + Assert.Equal("true", variables[variableName]); + } + + [Fact] + public void GeneratedSymbolTest_Int() + { + string variableName = "myConstant"; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "value", JExtensions.ToJsonString(1000) } + }; + + ConstantMacro macro = new(); + GeneratedSymbol symbol = new(variableName, macro.Type, jsonParameters); + + IVariableCollection variables = new VariableCollection(); + + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + Assert.Equal("1000", variables[variableName]); + } + + [Fact] + [Obsolete("EvaluateConfig is deprecated")] + public void ObsoleteEvaluateConfigTest() + { + string variableName = "myConstant"; + string value = "1048576"; + ConstantMacro macro = new(); + ConstantMacroConfig macroConfig = new(macro, null, variableName, value); + + IVariableCollection variables = new VariableCollection(); + + macro.EvaluateConfig(_engineEnvironmentSettings, variables, macroConfig); + + Assert.Equal(value, variables[variableName]); + } + + [Fact] + public void InvalidConfigurationTest() + { + ConstantMacro macro = new(); + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "constant", jsonParameters))); + Assert.Equal("Generated symbol 'test' of type 'constant' should have 'value' property defined.", ex.Message); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/EvaluateMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/EvaluateMacroTests.cs new file mode 100644 index 000000000000..d9b232a33c3a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/EvaluateMacroTests.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class EvaluateMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public EvaluateMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + [Theory(DisplayName = nameof(TestEvaluateConfig))] + [InlineData("(7 > 3)", true)] + [InlineData("(2 == 1)", false)] + public void TestEvaluateConfig(string predicate, bool expectedResult) + { + string variableName = "myPredicate"; + string evaluator = "C++"; + EvaluateMacroConfig macroConfig = new(variableName, "bool", predicate, evaluator); + + IVariableCollection variables = new VariableCollection(); + + EvaluateMacro macro = new(); + macro.Evaluate(_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(variableName, "bool", condition, evaluator); + + IVariableCollection variables = new VariableCollection + { + ["A"] = "A", + ["B"] = "B", + ["C"] = "C", + ["Param"] = new MultiValueParameter(multichoiceValues.TokenizeMultiValueParameter()) + }; + + EvaluateMacro macro = new(); + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + Assert.Equal(variables[variableName], expectedResult); + } + + [Fact] + [Obsolete("IMacro.EvaluateConfig is obsolete")] + public void ObsoleteEvaluateConfigTest() + { + string variableName = "myPredicate"; + string evaluator = "C++"; + EvaluateMacroConfig macroConfig = new(variableName, "bool", "(7 > 3)", evaluator); + + IVariableCollection variables = new VariableCollection(); + + EvaluateMacro macro = new(); + macro.EvaluateConfig(_engineEnvironmentSettings, variables, macroConfig); + Assert.Equal(variables[variableName], true); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/FakeMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/FakeMacroTests.cs new file mode 100644 index 000000000000..6e4b9f8f6443 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/FakeMacroTests.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.Fakes; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class FakeMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public FakeMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + [Fact(DisplayName = nameof(TestEvaluationOfFakeMacro))] + public void TestEvaluationOfFakeMacro() + { + string variableName = "myHelloMacro"; + string sourceVariable = "originalValue"; + + FakeMacro macro = new(); + FakeMacroConfig macroConfig = new(macro, variableName, sourceVariable, "name to greet"); + IVariableCollection variables = new VariableCollection(); + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + + string newValue = (string)variables[variableName]; + Assert.True(variables.Count == 1); + Assert.Equal("Hello name to greet!", newValue); + } + + [Fact(DisplayName = nameof(TestDependencyResolutionOfFakeMacro))] + public void TestDependencyResolutionOfFakeMacro() + { + string variableName = "myHelloMacro"; + string sourceVariable = "originalValue"; + + FakeMacro macro = new(); + FakeMacroConfig macroConfig = new(macro, variableName, sourceVariable); + + IVariableCollection variables = new VariableCollection(); + string sourceValue = "dependentMacro"; + variables[sourceVariable] = sourceValue; + + macroConfig.ResolveSymbolDependencies(variables.Select(v => v.Key).ToList()); + + Assert.True(macroConfig.Dependencies.Count == 1); + Assert.Equal(sourceVariable, macroConfig.Dependencies.First()); + } + + [Fact(DisplayName = nameof(TestExceptionOnAccessToDependenciesOfFakeMacro))] + public void TestExceptionOnAccessToDependenciesOfFakeMacro() + { + string variableName = "myHelloMacro"; + string sourceVariable = "originalValue"; + + FakeMacro macro = new(); + FakeMacroConfig macroConfig = new(macro, variableName, sourceVariable); + + var exception = Assert.Throws(() => macroConfig.Dependencies); + Assert.Equal("The method 'PopulateMacroConfigDependency' must be called prior 'Dependencies' property reading.", exception.Message); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/GeneratePortMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/GeneratePortMacroTests.cs new file mode 100644 index 000000000000..df5bf95727c2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/GeneratePortMacroTests.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class GeneratePortMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public GeneratePortMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + [Fact] + public void BasicMacroTest() + { + IVariableCollection variables = new VariableCollection(); + GeneratePortNumberMacro macro = new(); + GeneratePortNumberConfig config = new(macro, "test", "integer", 3000, 4000, 5000); + macro.Evaluate(_engineEnvironmentSettings, variables, config); + + Assert.Single(variables); + + int result = (int)variables["test"]; + + Assert.True(result >= 4000); + Assert.True(result <= 5000); + } + + [Fact] + public void GeneratedSymbolTest() + { + IVariableCollection variables = new VariableCollection(); + GeneratePortNumberMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "low", JExtensions.ToJsonString(4000) }, + { "high", JExtensions.ToJsonString(5000) }, + { "fallback", JExtensions.ToJsonString(3000) }, + }; + GeneratedSymbol symbol = new("test", macro.Type, jsonParameters); + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + + Assert.Single(variables); + + int result = (int)variables["test"]; + + Assert.True(result >= 4000); + Assert.True(result <= 5000); + } + + [Fact] + public void TestDeterministicMode() + { + IVariableCollection variables = new VariableCollection(); + GeneratePortNumberMacro macro = new(); + GeneratePortNumberConfig config = new(macro, "test", "integer", 3000, 4000, 5000); + macro.EvaluateDeterministically(_engineEnvironmentSettings, variables, config); + + Assert.Single(variables); + Assert.Equal(4000, variables["test"]); + } + + [Fact] + public void TestDeterministicMode_GenSymbol() + { + string variableName = "test"; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "low", JExtensions.ToJsonString(4000) }, + { "high", JExtensions.ToJsonString(5000) } + }; + + GeneratedSymbol deferredConfig = new(variableName, "port", jsonParameters, "integer"); + + IVariableCollection variables = new VariableCollection(); + GeneratePortNumberMacro macro = new(); + + macro.EvaluateDeterministically(_engineEnvironmentSettings, variables, macro.CreateConfig(_engineEnvironmentSettings, deferredConfig)); + Assert.Single(variables); + Assert.Equal(4000, variables["test"]); + } + + [Fact] + public void TestDeterministicMode_GenSymbol_Default() + { + string variableName = "test"; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + + GeneratedSymbol deferredConfig = new(variableName, "port", jsonParameters, "integer"); + + IVariableCollection variables = new VariableCollection(); + GeneratePortNumberMacro macro = new(); + + macro.EvaluateDeterministically(_engineEnvironmentSettings, variables, macro.CreateConfig(_engineEnvironmentSettings, deferredConfig)); + Assert.Single(variables); + Assert.Equal(GeneratePortNumberConfig.LowPortDefault, variables["test"]); + } + + [Fact] + public void DefaultConfigurationTest() + { + string variableName = "test"; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + GeneratePortNumberMacro macro = new(); + GeneratedSymbol symbol = new(variableName, "port", jsonParameters, "integer"); + GeneratePortNumberConfig config = new(NullLogger.Instance, macro, symbol); + + Assert.Equal(1024, config.Low); + Assert.Equal(65535, config.High); + Assert.Equal(0, config.Fallback); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/GuidMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/GuidMacroTests.cs new file mode 100644 index 000000000000..b8513a373ea7 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/GuidMacroTests.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class GuidMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public GuidMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + [Fact] + public void Test_SimpleMacro() + { + string variableName = "TestGuid"; + GuidMacroConfig macroConfig = new GuidMacroConfig(variableName, "string", string.Empty, null); + + IVariableCollection variables = new VariableCollection(); + + GuidMacro guidMacro = new(); + guidMacro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + ValidateGuidMacroCreatedParametersWithResolvedValues(variableName, variables); + } + + [Fact] + public void GeneratedSymbolTest() + { + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + string variableName = "myGuid1"; + GeneratedSymbol symbol = new(variableName, "GuidMacro", jsonParameters); + + GuidMacro guidMacro = new(); + IVariableCollection variables = new VariableCollection(); + guidMacro.Evaluate(_engineEnvironmentSettings, variables, symbol); + ValidateGuidMacroCreatedParametersWithResolvedValues(variableName, variables); + } + + [Fact] + [Obsolete("IMacro.EvaluateConfig is obsolete")] + public void ObsoleteEvaluateConfigTest() + { + string variableName = "TestGuid"; + GuidMacroConfig macroConfig = new GuidMacroConfig(variableName, "string", string.Empty, null); + + IVariableCollection variables = new VariableCollection(); + + GuidMacro guidMacro = new(); + guidMacro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + ValidateGuidMacroCreatedParametersWithResolvedValues(variableName, variables); + } + + private static void ValidateGuidMacroCreatedParametersWithResolvedValues(string variableName, IVariableCollection variables) + { + Assert.True(variables.ContainsKey(variableName)); + Assert.NotNull(variables[variableName]); + Guid paramValue = Guid.Parse((string)variables[variableName]!); + + // check that all the param name variants were created, and their values all resolve to the same guid. + string guidFormats = GuidMacroConfig.DefaultFormats; + for (int i = 0; i < guidFormats.Length; ++i) + { + string otherFormatVariableName = variableName + "-" + guidFormats[i]; + Assert.NotNull(variables[otherFormatVariableName]); + Guid testValue = Guid.Parse((string)variables[otherFormatVariableName]!); + Assert.Equal(paramValue, testValue); + + // Test the new formats - that distinguish upper and lower case by tags that are + // distinguishable regardless of casing comparison + otherFormatVariableName = + variableName + + (char.IsUpper(guidFormats[i]) ? GuidMacroConfig.UpperCaseDenominator : GuidMacroConfig.LowerCaseDenominator) + + guidFormats[i]; + + string resolvedValue = (string)variables[otherFormatVariableName]!; + testValue = Guid.Parse((string)variables[otherFormatVariableName]!); + Assert.Equal(paramValue, testValue); + Assert.Equal(char.IsUpper(guidFormats[i]), char.IsUpper(resolvedValue.First(char.IsLetter))); + } + } + + [Fact] + public void TestDefaultFormatIsCaseSensetive() + { + string paramNameLower = "TestGuidLower"; + GuidMacroConfig macroConfigLower = new GuidMacroConfig(paramNameLower, "string", string.Empty, "n"); + string paramNameUpper = "TestGuidUPPER"; + GuidMacroConfig macroConfigUpper = new GuidMacroConfig(paramNameUpper, "string", string.Empty, "N"); + + IVariableCollection variables = new VariableCollection(); + + GuidMacro guidMacro = new(); + guidMacro.Evaluate(_engineEnvironmentSettings, variables, macroConfigLower); + guidMacro.Evaluate(_engineEnvironmentSettings, variables, macroConfigUpper); + + Assert.True(variables.ContainsKey(paramNameLower)); + Assert.NotNull(variables[paramNameLower]); + Assert.All(((string)variables[paramNameLower]!).ToCharArray(), (c) => + { + Assert.True(char.IsLower(c) || char.IsDigit(c)); + }); + + Assert.True(variables.ContainsKey(paramNameUpper)); + Assert.NotNull(variables[paramNameUpper]); + Assert.All(((string)variables[paramNameUpper]!).ToCharArray(), (c) => + { + Assert.True(char.IsUpper(c) || char.IsDigit(c)); + }); + } + + [Fact] + public void TestDeterministicMode() + { + Guid deterministicModeValue = new("12345678-1234-1234-1234-1234567890AB"); + string variableName = "TestGuid"; + GuidMacroConfig macroConfig = new GuidMacroConfig(variableName, "string", "Nn", "n"); + + IVariableCollection variables = new VariableCollection(); + + GuidMacro guidMacro = new(); + guidMacro.EvaluateDeterministically(_engineEnvironmentSettings, variables, macroConfig); + + Assert.Equal(5, variables.Count); + Assert.Equal(deterministicModeValue.ToString("n"), variables["TestGuid-n"].ToString()); + Assert.Equal(deterministicModeValue.ToString("n"), variables["TestGuid-lc-n"].ToString()); + Assert.Equal(deterministicModeValue.ToString("n").ToUpperInvariant(), variables["TestGuid-N"].ToString()); + Assert.Equal(deterministicModeValue.ToString("n").ToUpperInvariant(), variables["TestGuid-uc-N"].ToString()); + Assert.Equal(deterministicModeValue.ToString("n"), variables["TestGuid"].ToString()); + } + + [Fact] + public void DefaultConfigurationTest() + { + string variableName = "test"; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + GuidMacro macro = new(); + GeneratedSymbol symbol = new(variableName, "guid", jsonParameters, "string"); + GuidMacroConfig config = new(macro, symbol); + + Assert.Equal("ndbpxNDPBX", config.Format); + Assert.Equal("D", config.DefaultFormat); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/JoinMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/JoinMacroTests.cs new file mode 100644 index 000000000000..3bbe495c30ef --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/JoinMacroTests.cs @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class JoinMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public JoinMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + [Theory(DisplayName = nameof(TestJoinConstantAndReferenceSymbolConfig))] + [InlineData(",", true)] + [InlineData("", true)] + [InlineData(null, true)] + [InlineData(",", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void TestJoinConstantAndReferenceSymbolConfig(string? separator, bool removeEmptyValues) + { + string variableName = "joinedParameter"; + string referenceSymbolName = "referenceSymbol"; + string referenceSymbolValue = "referenceValue"; + string referenceEmptySymbolName = "referenceEmptySymbol"; + string constantValue = "constantValue"; + + List<(JoinMacroConfig.JoinType, string)> definitions = new() + { + (JoinMacroConfig.JoinType.Const, constantValue), + (JoinMacroConfig.JoinType.Ref, referenceEmptySymbolName), + (JoinMacroConfig.JoinType.Ref, referenceSymbolName) + }; + + JoinMacro macro = new(); + JoinMacroConfig macroConfig = new(macro, variableName, null, definitions, separator, removeEmptyValues); + + IVariableCollection variables = new VariableCollection + { + [referenceSymbolName] = referenceSymbolValue + }; + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + string convertedValue = (string)variables[variableName]; + string expectedValue = + removeEmptyValues ? + string.Join(separator, constantValue, referenceSymbolValue) : + string.Join(separator, constantValue, null, referenceSymbolValue); + Assert.Equal(convertedValue, expectedValue); + } + + [Theory] + [InlineData(",")] + [InlineData("")] + public void GeneratedSymbolTest(string separator) + { + string variableName = "joinedParameter"; + string referenceSymbolName = "referenceSymbol"; + string referenceSymbolValue = "referenceValue"; + string constantValue = "constantValue"; + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + string symbols = + $"[ {{\"type\":\"const\" , \"value\":\"{constantValue}\" }}, {{\"type\":\"ref\" , \"value\":\"{referenceSymbolName}\" }} ]"; + jsonParameters.Add("symbols", symbols); + if (separator != null) + { + jsonParameters.Add("separator", JExtensions.ToJsonString(separator)); + } + GeneratedSymbol symbol = new(variableName, "JoinMacro", jsonParameters); + + IVariableCollection variables = new VariableCollection + { + [referenceSymbolName] = referenceSymbolValue + }; + + JoinMacro macro = new(); + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + + string convertedValue = (string)variables[variableName]; + string expectedValue = string.Join(separator, constantValue, referenceSymbolValue); + Assert.Equal(expectedValue, convertedValue); + } + + [Fact] + [Obsolete("IMacro.EvaluateConfig is obsolete")] + public void ObsoleteEvaluateConfigTest() + { + string variableName = "joinedParameter"; + string referenceSymbolName = "referenceSymbol"; + string referenceSymbolValue = "referenceValue"; + string referenceEmptySymbolName = "referenceEmptySymbol"; + string constantValue = "constantValue"; + + List<(JoinMacroConfig.JoinType, string)> definitions = new() + { + (JoinMacroConfig.JoinType.Const, constantValue), + (JoinMacroConfig.JoinType.Ref, referenceEmptySymbolName), + (JoinMacroConfig.JoinType.Ref, referenceSymbolName) + }; + + JoinMacro macro = new(); + JoinMacroConfig macroConfig = new(macro, variableName, null, definitions, ",", removeEmptyValues: true); + + IVariableCollection variables = new VariableCollection + { + [referenceSymbolName] = referenceSymbolValue + }; + + macro.EvaluateConfig(_engineEnvironmentSettings, variables, macroConfig); + string convertedValue = (string)variables[variableName]; + string expectedValue = string.Join(",", constantValue, referenceSymbolValue); + Assert.Equal(convertedValue, expectedValue); + } + + [Fact] + public void InvalidConfigurationTest_MissingSymbols() + { + JoinMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "separator", JExtensions.ToJsonString(",") } + }; + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "join", jsonParameters))); + Assert.Equal("Generated symbol 'test' of type 'join' should have 'symbols' property defined.", ex.Message); + } + + [Fact] + public void InvalidConfigurationTest_MissingSymbolValue() + { + JoinMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "separator", JExtensions.ToJsonString(",") } + }; + string symbols = $"[ {{\"type\":\"const\" }}, {{\"type\":\"ref\" , \"value\":\"ref\" }} ]"; + jsonParameters.Add("symbols", symbols); + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "join", jsonParameters))); + Assert.Equal("Generated symbol 'test': array 'symbols' should contain JSON objects with property 'value'.", ex.Message); + } + + [Fact] + public void InvalidConfigurationTest_EmptySymbolValue() + { + JoinMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "separator", JExtensions.ToJsonString(",") } + }; + string symbols = $"[ {{\"type\":\"ref\" , \"value\":\"\" }} ]"; + jsonParameters.Add("symbols", symbols); + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "join", jsonParameters))); + Assert.Equal("Generated symbol 'test': array 'symbols' should contain JSON objects with property non-empty 'value' when 'type' is 'Ref'.", ex.Message); + } + + [Fact] + public void DefaultConfigurationTest() + { + JoinMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + string symbols = $"[ {{\"value\":\"rep\" }} ]"; + jsonParameters.Add("symbols", symbols); + + JoinMacroConfig config = new(macro, new GeneratedSymbol("test", "join", jsonParameters)); + + Assert.Equal(string.Empty, config.Separator); + Assert.False(config.RemoveEmptyValues); + Assert.Equal(JoinMacroConfig.JoinType.Const, config.Symbols.Single().Type); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/NowMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/NowMacroTests.cs new file mode 100644 index 000000000000..8b9d4fd2878d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/NowMacroTests.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class NowMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public NowMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + // tests regular Now configuration, and Utc = true + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("yyyy-MM-dd HH:mm:ss")] + public void EvaluateNowConfig(string? format) + { + string variableName = "nowString"; + bool utc = true; + + NowMacro macro = new(); + NowMacroConfig macroConfig = new(macro, variableName, format, utc); + Assert.Equal("string", macroConfig.DataType); + + IVariableCollection variables = new VariableCollection(); + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + Assert.IsType(variables[variableName]); + string macroNowString = (string)variables[variableName]!; + DateTime macroNowTime = Convert.ToDateTime(macroNowString); + + TimeSpan difference = macroNowTime.Subtract(DateTime.UtcNow); + + // 10 seconds is quite a lot of wiggle room, but should be fine, and safe. + Assert.True(difference.TotalSeconds < 10); + } + + [Fact] + public void GeneratedSymbolTest() + { + string variableName = "nowString"; + string format = string.Empty; + bool utc = false; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "format", JExtensions.ToJsonString(format) }, + { "utc", JExtensions.ToJsonString(utc) } + }; + GeneratedSymbol symbol = new(variableName, "now", jsonParameters); + IVariableCollection variables = new VariableCollection(); + + NowMacro macro = new(); + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + Assert.IsType(variables[variableName]); + string macroNowString = (string)variables[variableName]!; + DateTime macroNowTime = Convert.ToDateTime(macroNowString); + + TimeSpan difference = macroNowTime.Subtract(DateTime.Now); + + // 10 seconds is quite a lot of wiggle room, but should be fine, and safe. + Assert.True(difference.TotalSeconds < 10); + } + + [Theory] + [InlineData(null, "string")] + [InlineData("", "string")] + [InlineData("string", "string")] + [InlineData("date", "date")] + public void EvaluateNowOverrideDatatypeInConfig(string? type, string expectedType) + { + string variableName = "nowString"; + string format = string.Empty; + bool utc = false; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "format", JExtensions.ToJsonString(format) }, + { "utc", JExtensions.ToJsonString(utc) } + }; + GeneratedSymbol deferredConfig = new(variableName, "NowMacro", jsonParameters, type); + NowMacro macro = new(); + IMacroConfig realConfig = macro.CreateConfig(_engineEnvironmentSettings, deferredConfig); + Assert.Equal(expectedType, (realConfig as NowMacroConfig)?.DataType); + } + + [Fact] + public void TestDeterministicMode() + { + IVariableCollection variables = new VariableCollection(); + NowMacro macro = new(); + NowMacroConfig config = new(macro, "test", "yyyy-MM-dd HH:mm:ss"); + macro.EvaluateDeterministically(_engineEnvironmentSettings, variables, config); + + Assert.Single(variables); + Assert.Equal("1900-01-01 00:00:00", variables["test"].ToString()); + } + + [Fact] + public void TestDeterministicMode_GenSymbol() + { + string variableName = "test"; + string format = "yyyy-MM-dd HH:mm:ss"; + bool utc = false; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "format", JExtensions.ToJsonString(format) }, + { "utc", JExtensions.ToJsonString(utc) } + }; + GeneratedSymbol deferredConfig = new(variableName, "NowMacro", jsonParameters, "string"); + + IVariableCollection variables = new VariableCollection(); + NowMacro macro = new(); + + macro.EvaluateDeterministically(_engineEnvironmentSettings, variables, macro.CreateConfig(_engineEnvironmentSettings, deferredConfig)); + + Assert.Single(variables); + Assert.Equal("1900-01-01 00:00:00", variables["test"].ToString()); + } + + [Fact] + [Obsolete("IMacro.EvaluateConfig is obsolete")] + public void ObsoleteEvaluateConfigTest() + { + string variableName = "nowString"; + + NowMacro macro = new(); + NowMacroConfig macroConfig = new(macro, variableName, "yyyy-MM-dd HH:mm:ss", false); + Assert.Equal("string", macroConfig.DataType); + + IVariableCollection variables = new VariableCollection(); + + macro.EvaluateConfig(_engineEnvironmentSettings, variables, macroConfig); + Assert.IsType(variables[variableName]); + string macroNowString = (string)variables[variableName]!; + DateTime macroNowTime = Convert.ToDateTime(macroNowString); + + TimeSpan difference = macroNowTime.Subtract(DateTime.Now); + + // 10 seconds is quite a lot of wiggle room, but should be fine, and safe. + Assert.True(difference.TotalSeconds < 10); + } + + [Fact] + public void DefaultConfigurationTest() + { + string variableName = "test"; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + NowMacro macro = new(); + GeneratedSymbol symbol = new(variableName, "now", jsonParameters); + NowMacroConfig config = new(macro, symbol); + + Assert.Null(config.Format); + Assert.False(config.Utc); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/RandomMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/RandomMacroTests.cs new file mode 100644 index 000000000000..a668b9d07a35 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/RandomMacroTests.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class RandomMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public RandomMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + [Theory(DisplayName = nameof(TestRandomConfig))] + [InlineData(0, 100)] + [InlineData(-1000, -900)] + [InlineData(50, 50)] + [InlineData(1000, null)] + [InlineData(0, null)] + public void TestRandomConfig(int low, int? high) + { + string variableName = "myRnd"; + RandomMacro macro = new(); + RandomMacroConfig macroConfig = new(macro, variableName, null, low, high); + + IVariableCollection variables = new VariableCollection(); + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + + long randomValue = (int)variables[variableName]; + Assert.True(randomValue >= low); + + if (high.HasValue) + { + Assert.True(randomValue <= high); + } + } + + [Theory] + [InlineData(1, 10)] + [InlineData(0, null)] + [InlineData(-1, 1)] + [InlineData(10000, null)] + [InlineData(123, 123)] + public void GeneratedSymbolTest(int low, int? high) + { + string variableName = "myRnd"; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "low", JExtensions.ToJsonString(low) } + }; + if (high.HasValue) + { + jsonParameters.Add("high", JExtensions.ToJsonString(high)); + } + + GeneratedSymbol symbol = new(variableName, "random", jsonParameters); + IVariableCollection variables = new VariableCollection(); + + RandomMacro macro = new(); + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + long randomValue = (int)variables[variableName]; + Assert.True(randomValue >= low); + + if (high.HasValue) + { + Assert.True(randomValue <= high); + } + } + + [Fact] + public void TestDeterministicMode() + { + IVariableCollection variables = new VariableCollection(); + RandomMacro macro = new(); + RandomMacroConfig config = new(macro, "test", "integer", 10, 100); + macro.EvaluateDeterministically(_engineEnvironmentSettings, variables, config); + + Assert.Single(variables); + Assert.Equal(10, variables["test"]); + } + + [Fact] + public void TestDeterministicMode_GenSymbol() + { + string variableName = "test"; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "low", JExtensions.ToJsonString(10) }, + { "high", JExtensions.ToJsonString(100) } + }; + GeneratedSymbol deferredConfig = new(variableName, "random", jsonParameters, "integer"); + + IVariableCollection variables = new VariableCollection(); + RandomMacro macro = new(); + + macro.EvaluateDeterministically(_engineEnvironmentSettings, variables, macro.CreateConfig(_engineEnvironmentSettings, deferredConfig)); + + Assert.Single(variables); + Assert.Equal(10, variables["test"]); + } + + [Fact] + public void InvalidConfigurationTest() + { + RandomMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "random", jsonParameters))); + Assert.Equal("Generated symbol 'test' of type 'random' should have 'low' property defined.", ex.Message); + } + + [Fact] + public void DefaultConfigurationTest() + { + RandomMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "low", JExtensions.ToJsonString(0) } + }; + RandomMacroConfig config = new(macro, new GeneratedSymbol("test", "random", jsonParameters)); + Assert.Equal(int.MaxValue, config.High); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/RegexMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/RegexMacroTests.cs new file mode 100644 index 000000000000..d510b430cdce --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/RegexMacroTests.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class RegexMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public RegexMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + [Fact(DisplayName = nameof(TestRegexMacro))] + public void TestRegexMacro() + { + string variableName = "myRegex"; + string sourceVariable = "originalValue"; + List<(string, string)> steps = new() + { + ("2+", "3"), + ("13", "Z") + }; + + RegexMacro macro = new(); + RegexMacroConfig macroConfig = new(macro, variableName, null, sourceVariable, steps); + + IVariableCollection variables = new VariableCollection(); + + string sourceValue = "QQQ121222112"; + string expectedValue = "QQQZZ1Z"; + + variables[sourceVariable] = sourceValue; + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + + string newValue = (string)variables[variableName]; + Assert.Equal(newValue, expectedValue); + } + + [Fact] + public void GeneratedSymbolTest() + { + string variableName = "myRegex"; + string sourceVariable = "originalValue"; + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString(sourceVariable) } + }; + + string jsonSteps = /*lang=json*/ """ + [ + { + "regex": "A", + "replacement": "Z" + } + ] + """; + jsonParameters.Add("steps", jsonSteps); + + GeneratedSymbol symbol = new(variableName, "regex", jsonParameters, "string"); + + IVariableCollection variables = new VariableCollection(); + + string sourceValue = "ABCAABBCC"; + string expectedValue = "ZBCZZBBCC"; + + variables[sourceVariable] = sourceValue; + + RegexMacro macro = new(); + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + string newValue = (string)variables[variableName]; + Assert.Equal(newValue, expectedValue); + } + + [Fact] + public void MissingSourceVariableTest() + { + string variableName = "myRegex"; + string sourceVariable = "originalValue"; + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString(sourceVariable) } + }; + + string jsonSteps = /*lang=json*/ """ + [ + { + "regex": "A", + "replacement": "Z" + } + ] + """; + jsonParameters.Add("steps", jsonSteps); + + GeneratedSymbol symbol = new(variableName, "regex", jsonParameters, "string"); + IVariableCollection variables = new VariableCollection(); + + RegexMacro macro = new(); + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + + Assert.False(variables.ContainsKey(variableName)); + } + + [Fact] + public void InvalidConfigurationTest_MissingSource() + { + RegexMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + string jsonSteps = /*lang=json*/ """ + [ + { + "regex": "A", + "replacement": "Z" + } + ] + """; + jsonParameters.Add("steps", jsonSteps); + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "regex", jsonParameters))); + Assert.Equal("Generated symbol 'test' of type 'regex' should have 'source' property defined.", ex.Message); + } + + [Fact] + public void InvalidConfigurationTest_MissingSteps() + { + RegexMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString("src") } + }; + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "regex", jsonParameters))); + Assert.Equal("Generated symbol 'test' of type 'regex' should have 'steps' property defined.", ex.Message); + } + + [Fact] + public void InvalidConfigurationTest_MissingRegex() + { + RegexMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString("src") } + }; + string jsonSteps = /*lang=json*/ """ + [ + { + "replacement": "Z" + } + ] + """; + jsonParameters.Add("steps", jsonSteps); + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "regex", jsonParameters))); + Assert.Equal("Generated symbol 'test': array 'steps' should contain JSON objects with property 'regex'.", ex.Message); + } + + [Fact] + public void InvalidConfigurationTest_MissingReplacement() + { + RegexMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString("src") } + }; + string jsonSteps = /*lang=json*/ """ + [ + { + "regex": "A" + } + ] + """; + jsonParameters.Add("steps", jsonSteps); + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "regex", jsonParameters))); + Assert.Equal("Generated symbol 'test': array 'steps' should contain JSON objects with property 'replacement'.", ex.Message); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/RegexMatchMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/RegexMatchMacroTests.cs new file mode 100644 index 000000000000..1d049badba48 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/RegexMatchMacroTests.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class RegexMatchMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public RegexMatchMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + [Fact(DisplayName = nameof(TestRegexMatchMacroTrue))] + public void TestRegexMatchMacroTrue() + { + const string variableName = "isMatch"; + const string sourceVariable = "originalValue"; + RegexMatchMacro macro = new(); + RegexMatchMacroConfig macroConfig = new(macro, variableName, null, sourceVariable, @"(((?<=\.)|^)(?=\d)|[^\w\.])"); + + IVariableCollection variables = new VariableCollection(); + + const string sourceValue = "1234test"; + const bool expectedValue = true; + variables[sourceVariable] = sourceValue; + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + + bool newValue = (bool)variables[variableName]; + Assert.Equal(expectedValue, newValue); + } + + [Fact(DisplayName = nameof(TestRegexMatchMacroFalse))] + public void TestRegexMatchMacroFalse() + { + const string variableName = "isMatch"; + const string sourceVariable = "originalValue"; + + RegexMatchMacro macro = new(); + RegexMatchMacroConfig macroConfig = new(macro, variableName, null, sourceVariable, @"(((?<=\.)|^)(?=\d)|[^\w\.])"); + + IVariableCollection variables = new VariableCollection(); + + const string sourceValue = "A1234test"; + const bool expectedValue = false; + variables[sourceVariable] = sourceValue; + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + + bool newValue = (bool)variables[variableName]; + Assert.Equal(expectedValue, newValue); + } + + [Fact] + public void GeneratedSymbolTest() + { + string variableName = "isMatch"; + string sourceVariable = "originalValue"; + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString(sourceVariable) }, + { "pattern", JExtensions.ToJsonString(@"(((?<=\.)|^)(?=\d)|[^\w\.])") } + }; + GeneratedSymbol symbol = new(variableName, "regexMatch", jsonParameters, "string"); + + IVariableCollection variables = new VariableCollection(); + + const string sourceValue = "1234test"; + const bool expectedValue = true; + variables[sourceVariable] = sourceValue; + + RegexMatchMacro macro = new(); + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + + bool newValue = (bool)variables[variableName]; + Assert.Equal(expectedValue, newValue); + } + + [Fact] + public void MissingSourceVariableTest() + { + string variableName = "isMatch"; + string sourceVariable = "originalValue"; + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString(sourceVariable) }, + { "pattern", JExtensions.ToJsonString(@"(((?<=\.)|^)(?=\d)|[^\w\.])") } + }; + GeneratedSymbol symbol = new(variableName, "regexMatch", jsonParameters, "string"); + + IVariableCollection variables = new VariableCollection(); + + RegexMatchMacro macro = new(); + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + + Assert.False(variables.ContainsKey(variableName)); + } + + [Fact] + public void InvalidConfigurationTest_MissingSource() + { + RegexMatchMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "pattern", JExtensions.ToJsonString(@"(((?<=\.)|^)(?=\d)|[^\w\.])") } + }; + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "regexMatch", jsonParameters))); + Assert.Equal("Generated symbol 'test' of type 'regexMatch' should have 'source' property defined.", ex.Message); + } + + [Fact] + public void InvalidConfigurationTest_MissingPattern() + { + RegexMatchMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString("src") }, + }; + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "regexMatch", jsonParameters))); + Assert.Equal("Generated symbol 'test' of type 'regexMatch' should have 'pattern' property defined.", ex.Message); + } + + [Fact] + public void InvalidConfigurationTest_InvalidPattern() + { + RegexMatchMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "source", JExtensions.ToJsonString("src") }, + { "pattern", JExtensions.ToJsonString(@"(()") } + }; + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "regexMatch", jsonParameters))); + Assert.Equal("Generated symbol 'test': the regex pattern '(()' is invalid.", ex.Message); + } + + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/SwtichMacroTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/SwtichMacroTests.cs new file mode 100644 index 000000000000..5a94b3e7962c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/MacroTests/SwtichMacroTests.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Core; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.MacroTests +{ + public class SwtichMacroTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public SwtichMacroTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + [Fact(DisplayName = nameof(TestSwitchConfig))] + public void TestSwitchConfig() + { + string variableName = "mySwitchVar"; + string evaluator = "C++"; + string dataType = "string"; + string expectedValue = "this one"; + List<(string?, string)> switches = new() + { + ("(3 > 10)", "three greater than ten - false"), + ("(false)", "false value"), + ("(10 > 0)", expectedValue), + ("(5 > 4)", "not this one") + }; + SwitchMacro macro = new(); + SwitchMacroConfig macroConfig = new(macro, variableName, evaluator, dataType, switches); + + IVariableCollection variables = new VariableCollection(); + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + + string resultValue = (string)variables[variableName]; + Assert.Equal(resultValue, expectedValue); + } + + [Fact] + public void GeneratedSymbolTest() + { + string variableName = "mySwitchVar"; + string evaluator = "C++"; + string dataType = "string"; + string expectedValue = "this one"; + string switchCases = @"[ + { + ""condition"": ""(3 > 10)"", + ""value"": ""three greater than ten"" + }, + { + ""condition"": ""(false)"", + ""value"": ""false value"" + }, + { + ""condition"": ""(10 > 0)"", + ""value"": """ + expectedValue + @""" + }, + { + ""condition"": ""(5 > 4)"", + ""value"": ""not this one"" + } + ]"; + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "evaluator", JExtensions.ToJsonString(evaluator) }, + { "datatype", JExtensions.ToJsonString(dataType) }, + { "cases", switchCases } + }; + + GeneratedSymbol symbol = new(variableName, "switch", jsonParameters, dataType); + + IVariableCollection variables = new VariableCollection(); + + SwitchMacro macro = new(); + macro.Evaluate(_engineEnvironmentSettings, variables, symbol); + + string resultValue = (string)variables[variableName]; + Assert.Equal(resultValue, expectedValue); + } + + [Theory] + [InlineData("A", "condition")] + [InlineData("B", "default")] + [InlineData(null, "default")] + public void DependantConditionTest(string? varValue, string expectedResult) + { + string variableName = "mySwitchVar"; + string evaluator = "C++"; + string dataType = "string"; + List<(string?, string)> switches = new() + { + ("(testVar == \"A\")", "condition"), + (null, "default") + }; + SwitchMacro macro = new(); + SwitchMacroConfig macroConfig = new(macro, variableName, evaluator, dataType, switches); + + VariableCollection variables = new(); + if (varValue is not null) + { + variables["testVar"] = varValue; + } + + macro.Evaluate(_engineEnvironmentSettings, variables, macroConfig); + + string resultValue = (string)variables[variableName]; + Assert.Equal(expectedResult, resultValue); + } + + [Fact] + public void InvalidConfigurationTest_MissingCases() + { + SwitchMacro macro = new(); + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase); + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "switch", jsonParameters))); + Assert.Equal("Generated symbol 'test' of type 'switch' should have 'cases' property defined.", ex.Message); + } + + [Fact] + public void InvalidConfigurationTest_MissingSymbolValue() + { + SwitchMacro macro = new(); + + string switchCases = /*lang=json*/ @"[ + { + ""condition"": ""(3 > 10)"" + }, + { + ""value"": ""default"" + } + ]"; + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "cases", switchCases } + }; + + VariableCollection variables = new(); + TemplateAuthoringException ex = Assert.Throws(() => macro.Evaluate(_engineEnvironmentSettings, variables, new GeneratedSymbol("test", "switch", jsonParameters))); + Assert.Equal("Generated symbol 'test': array 'cases' should contain JSON objects with property 'value'.", ex.Message); + } + + [Fact] + public void DefaultConfigurationTest() + { + SwitchMacro macro = new(); + + string switchCases = /*lang=json*/ @"[ + { + ""condition"": ""(3 > 10)"", + ""value"": ""v"" + }, + { + ""value"": ""default"" + } + ]"; + + Dictionary jsonParameters = new(StringComparer.OrdinalIgnoreCase) + { + { "cases", switchCases } + }; + + SwitchMacroConfig config = new(macro, new GeneratedSymbol("test", "switch", jsonParameters)); + + Assert.Equal(EvaluatorSelector.SelectStringEvaluator(EvaluatorType.CPP2), config.Evaluator); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.csproj new file mode 100644 index 000000000000..73604304db2b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.csproj @@ -0,0 +1,60 @@ + + + + $(NetCurrent);$(NetFrameworkCurrent) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/RunnableProjectConfigTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/RunnableProjectConfigTests.cs new file mode 100644 index 000000000000..72f63b74e089 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/RunnableProjectConfigTests.cs @@ -0,0 +1,293 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using FakeItEasy; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.Fakes; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.Serialization; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Validation; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests +{ + public class RunnableProjectConfigTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public RunnableProjectConfigTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + private const string InvalidMultiChoiceDefinition = /*lang=json,strict*/ """ + { + "type": "parameter", + "description": "sample switch", + "datatype": "choice", + "allowMultipleValues": true, + "choices": [ + { + "choice": "First|Choice", + "description": "First Sample Choice" + + }, + { + "choice": "SecondChoice", + "description": "Second Sample Choice" + }, + { + "choice": "ThirdChoice", + "description": "Third Sample Choice" + + } + ], + "defaultValue ": "ThirdChoice " + } + """; + + private const string ValidChoiceDefinition = /*lang=json,strict*/ """ + { + "type": "parameter", + "description": "sample switch", + "datatype": "choice", + "allowMultipleValues": true, + "choices": [ + { + "choice": "FirstChoice", + "description": "First Sample Choice" + + }, + { + "choice": "SecondChoice", + "description": "Second Sample Choice" + }, + { + "choice": "ThirdChoice", + "description": "Third Sample Choice" + + } + ], + "defaultValue ": "ThirdChoice " + } + """; + + private const string ValidComputedDefinition = /*lang=json,strict*/ """ + { + "type": "computed", + "datatype": "bool", + "value": "10 != 11" + } + """; + + private const string ValidCustomMacroDefinition = /*lang=json,strict*/ """ + { + "type": "fake", + "generator": "fake", + "parameters": { + "source": "dummy", + "pattern": "^hello$", + "name": "dummy" + } + } + """; + + [Theory] + [InlineData(ValidChoiceDefinition, false, true)] + [InlineData(InvalidMultiChoiceDefinition, false, true)] + [InlineData(ValidChoiceDefinition, true, true)] + [InlineData(InvalidMultiChoiceDefinition, true, false)] + public async Task PerformTemplateValidation_ChoiceValuesValidation(string paramDefinition, bool isMultichoice, bool expectedToBeValid) + { + // + // Template content preparation + // + + Guid inputTestGuid = new("12aa8f4e-a4aa-4ac1-927c-94cb99485ef1"); + string contentFileNamePrefix = "content - "; + JsonObject choiceParam = JExtensions.ParseJsonObject(paramDefinition); + choiceParam["AllowMultipleValues"] = isMultichoice; + TemplateConfigModel config = new TemplateConfigModel("test") + { + Name = "name", + ShortNameList = new[] { "shortName" }, + Symbols = new[] + { + new ParameterSymbol("ParamA", choiceParam, null) + } + }; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, config.ToJsonString() } + }; + + //content + foreach (string guidFormat in GuidMacroConfig.DefaultFormats.Select(c => c.ToString())) + { + templateSourceFiles.Add(contentFileNamePrefix + guidFormat, inputTestGuid.ToString(guidFormat)); + } + + // + // Dependencies preparation and mounting + // + + List<(LogLevel, string)> loggedMessages = new(); + InMemoryLoggerProvider loggerProvider = new(loggedMessages); + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(addLoggerProviders: new[] { loggerProvider }); + string sourceBasePath = environmentSettings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(environmentSettings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = environmentSettings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new(); + + using RunnableProjectConfig templateConfig = new RunnableProjectConfig(environmentSettings, rpg, config, sourceMountPoint.Root); + await templateConfig.ValidateAsync(ValidationScope.Instantiation, default); + + if (expectedToBeValid) + { + Assert.True(templateConfig.IsValid); + Assert.DoesNotContain(templateConfig.ValidationErrors, e => e is { Severity: IValidationEntry.SeverityLevel.Error } or { Severity: IValidationEntry.SeverityLevel.Warning }); + } + else + { + Assert.False(templateConfig.IsValid); + IValidationEntry validationError = Assert.Single(templateConfig.ValidationErrors, e => e.Severity is IValidationEntry.SeverityLevel.Error or IValidationEntry.SeverityLevel.Warning); + Assert.Equal("MV004", validationError.Code); + Assert.Equal( + "Choice parameter 'ParamA' is invalid. It allows multiple values ('AllowMultipleValues=true'), while some of the configured choices contain separator characters ('|', ','). Invalid choices: {First|Choice}", + validationError.ErrorMessage); + } + } + + [Fact] + public void VerifyComputedSymbolsParsedCorrectly() + { + // + // Template content preparation + // + + //fill test data here + TemplateConfigModel config = new TemplateConfigModel("test") + { + Name = "name", + ShortNameList = new[] { "shortName" }, + Symbols = new[] + { + new ComputedSymbol("computed1", JExtensions.ParseJsonObject(ValidComputedDefinition)), + new ComputedSymbol("computed2", JExtensions.ParseJsonObject(ValidComputedDefinition)) + } + }; + + // + // Dependencies preparation and mounting + // + + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(); + + string sourceBasePath = environmentSettings.GetTempVirtualizedPath(); + using IMountPoint mountPoint = environmentSettings.MountPath(sourceBasePath); + using RunnableProjectConfig templateConfig = new RunnableProjectConfig(environmentSettings, new RunnableProjectGenerator(), config, mountPoint.Root); + + //verify + Assert.Equal(7, templateConfig.GlobalOperationConfig.Macros.Count); + var evaluatedMacros = templateConfig.GlobalOperationConfig.Macros.Where(m => m.Type == "evaluate"); + Assert.Equal(2, evaluatedMacros.Count()); + evaluatedMacros.Select(m => m.VariableName).Should().Equal(new string[] { "computed1", "computed2" }); + // default symbols are different for OS + templateConfig.GlobalOperationConfig.SymbolNames.Count.Should().BeGreaterThanOrEqualTo(3); + templateConfig.GlobalOperationConfig.SymbolNames.Should().Contain(new string[] { "computed1", "computed2" }); + } + + [Fact] + public void VerifyCustomGeneratedSymbolsParsedCorrectly() + { + // + // Template content preparation + // + + //fill test data here + TemplateConfigModel config = new TemplateConfigModel("test") + { + Name = "name", + ShortNameList = new[] { "shortName" }, + Symbols = new[] + { + new GeneratedSymbol("fake1", JExtensions.ParseJsonObject(ValidCustomMacroDefinition)) + } + }; + + // + // Dependencies preparation and mounting + // + + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment( + additionalComponents: new[] + { + (typeof(IMacro), (IIdentifiedComponent)new FakeMacro()), + (typeof(IGeneratedSymbolMacro), new FakeMacro()) + }); + + string sourceBasePath = environmentSettings.GetTempVirtualizedPath(); + using IMountPoint mountPoint = environmentSettings.MountPath(sourceBasePath); + using RunnableProjectConfig templateConfig = new RunnableProjectConfig(environmentSettings, new RunnableProjectGenerator(), config, mountPoint.Root); + + var customMacros = templateConfig.GlobalOperationConfig.Macros.Where(m => m.Type == "fake"); + //verify + Assert.Equal(6, templateConfig.GlobalOperationConfig.Macros.Count); + Assert.Single(customMacros); + Assert.Equal("fake1", customMacros.First().VariableName); + // default symbols are different for OS + templateConfig.GlobalOperationConfig.SymbolNames.Count.Should().BeGreaterThanOrEqualTo(2); + templateConfig.GlobalOperationConfig.SymbolNames.Should().Contain(new string[] { "fake1" }); + } + + [Fact] + public void VerifyDerivedSymbolsParsedCorrectly() + { + // + // Template content preparation + // + + //fill test data here + TemplateConfigModel config = new TemplateConfigModel("test") + { + Name = "name", + ShortNameList = new[] { "shortName" }, + Symbols = new[] + { + (BaseSymbol)new ParameterSymbol("original", "whatever"), + new DerivedSymbol("derivedTest", valueTransform: "fakeForm", valueSource: "original", replaces: "something") + } + }; + + // + // Dependencies preparation and mounting + // + + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment( + additionalComponents: new[] + { + (typeof(IMacro), (IIdentifiedComponent)new FakeMacro()), + (typeof(IGeneratedSymbolMacro), new FakeMacro()) + }); + + string sourceBasePath = environmentSettings.GetTempVirtualizedPath(); + using IMountPoint mountPoint = environmentSettings.MountPath(sourceBasePath); + using RunnableProjectConfig templateConfig = new RunnableProjectConfig(environmentSettings, new RunnableProjectGenerator(), config, mountPoint.Root); + + //verify + Assert.Equal(7, templateConfig.GlobalOperationConfig.Macros.Count); + Assert.Equal(1, templateConfig.GlobalOperationConfig.Macros.Count(m => m.VariableName == "derivedTest{-VALUE-FORMS-}identity")); + + templateConfig.GlobalOperationConfig.SymbolNames.Should().Contain(new string[] { "original", "derivedTest" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/RunnableProjectGeneratorTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/RunnableProjectGeneratorTests.cs new file mode 100644 index 000000000000..5f2f2707222c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/RunnableProjectGeneratorTests.cs @@ -0,0 +1,830 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Abstractions.Parameters; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.Serialization; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests +{ + public class RunnableProjectGeneratorTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public RunnableProjectGeneratorTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Fact] + public async Task CreateAsyncTest_GuidsMacroProcessingCaseSensitivity() + { + // + // Template content preparation + // + + Guid inputTestGuid = new Guid("12aa8f4e-a4aa-4ac1-927c-94cb99485ef1"); + string contentFileNamePrefix = "content - "; + TemplateConfigModel config = new TemplateConfigModel("test") + { + Name = "test", + ShortNameList = new[] { "test" }, + Guids = new List() + { + inputTestGuid + } + }; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, config.ToJsonString() } + }; + + //content + foreach (string guidFormat in GuidMacroConfig.DefaultFormats.Select(c => c.ToString())) + { + templateSourceFiles.Add(contentFileNamePrefix + guidFormat, inputTestGuid.ToString(guidFormat)); + } + + // + // Dependencies preparation and mounting + // + + IEngineEnvironmentSettings environment = _environmentSettingsHelper.CreateEnvironment(); + string sourceBasePath = environment.GetTempVirtualizedPath(); + string targetDir = environment.GetTempVirtualizedPath(); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + + TestFileSystemUtils.WriteTemplateSource(environment, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = environment.MountPath(sourceBasePath); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(environment, rpg, config, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData(runnableConfig); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(environment, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Veryfying the outputs + // + + Guid expectedResultGuid = Guid.Empty; + foreach (string guidFormat in GuidMacroConfig.DefaultFormats.Select(c => c.ToString())) + { + string resultContent = environment.Host.FileSystem.ReadAllText(Path.Combine(targetDir, contentFileNamePrefix + guidFormat)); + Assert.True( + Guid.TryParseExact(resultContent, guidFormat, out Guid resultGuid), + $"Expected the result conent ({resultContent}) to be parseable by Guid format '{guidFormat}'"); + + if (expectedResultGuid == Guid.Empty) + { + expectedResultGuid = resultGuid; + } + else + { + Assert.Equal(expectedResultGuid, resultGuid); + } + } + Assert.NotEqual(inputTestGuid, expectedResultGuid); + } + + private const string TemplateConfigQuotelessLiteralsNotEnabled = /*lang=json*/ """ + { + "identity": "test.template", + "name": "test", + "shortName": "test", + "symbols": { + "ChoiceParam": { + "type": "parameter", + "description": "sample switch", + "datatype": "choice", + "choices": [ + { + "choice": "FirstChoice", + "description": "First Sample Choice" + }, + { + "choice": "SecondChoice", + "description": "Second Sample Choice" + }, + { + "choice": "ThirdChoice", + "description": "Third Sample Choice" + } + ], + "defaultValue": "ThirdChoice", + } + } + } + """; + + private const string TemplateConfigQuotelessLiteralsEnabled = /*lang=json*/ """ + + { + "identity": "test.template", + "name": "test", + "shortName": "test", + "symbols": { + "ChoiceParam": { + "type": "parameter", + "description": "sample switch", + "datatype": "choice", + "enableQuotelessLiterals": true, + "choices": [ + { + "choice": "FirstChoice", + "description": "First Sample Choice" + }, + { + "choice": "SecondChoice", + "description": "Second Sample Choice" + }, + { + "choice": "ThirdChoice", + "description": "Third Sample Choice" + } + ], + "defaultValue": "ThirdChoice", + } + } + } + + """; + + [Theory] + [InlineData(TemplateConfigQuotelessLiteralsNotEnabled, "UNKNOWN")] + [InlineData(TemplateConfigQuotelessLiteralsEnabled, "SECOND")] + public async Task CreateAsyncTest_ConditionWithUnquotedChoiceLiteral(string templateConfig, string expectedResult) + { + // + // Template content preparation + // + + string sourceSnippet = """ + //#if( ChoiceParam == FirstChoice ) + FIRST + //#elseif (ChoiceParam == SecondChoice ) + SECOND + //#elseif (ChoiceParam == ThirdChoice ) + THIRD + //#else + UNKNOWN + //#endif + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, templateConfig }, + //content + { "sourcFile", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + + IEngineEnvironmentSettings environment = _environmentSettingsHelper.CreateEnvironment(); + string sourceBasePath = environment.GetTempVirtualizedPath(); + string targetDir = environment.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(environment, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = environment.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromString(templateConfig); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(environment, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData( + runnableConfig, + new Dictionary() { { "ChoiceParam", "SecondChoice" } }); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(environment, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Veryfying the outputs + // + + string resultContent = environment.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourcFile")).Trim(); + Assert.Equal(expectedResult, resultContent); + } + + [Fact] + public async Task CreateAsyncTest_MultiChoiceParamReplacingAndCondition() + { + // + // Template content preparation + // + + string templateConfig = /*lang=json*/ """ + { + "identity": "test.template", + "name": "test", + "shortName": "test", + "symbols": { + "ChoiceParam": { + "type": "parameter", + "description": "sample switch", + "datatype": "choice", + "allowMultipleValues": true, + "enableQuotelessLiterals": true, + "choices": [ + { + "choice": "FirstChoice", + "description": "First Sample Choice" + }, + { + "choice": "SecondChoice", + "description": "Second Sample Choice" + }, + { + "choice": "ThirdChoice", + "description": "Third Sample Choice" + } + ], + "defaultValue": "ThirdChoice", + "replaces": "REPLACE_VALUE" + } + } + } + """; + + string sourceSnippet = """ + MultiChoiceValue: REPLACE_VALUE + //#if( ChoiceParam == FirstChoice ) + FIRST + //#endif + //#if (ChoiceParam == SecondChoice ) + SECOND + //#endif + //#if (ChoiceParam == ThirdChoice ) + THIRD + //#endif + """; + + string expectedSnippet = """ + MultiChoiceValue: SecondChoice|ThirdChoice + SECOND + THIRD + + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, templateConfig }, + //content + { "sourcFile", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + + IEngineEnvironmentSettings environment = _environmentSettingsHelper.CreateEnvironment(); + string sourceBasePath = environment.GetTempVirtualizedPath(); + string targetDir = environment.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(environment, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = environment.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromString(templateConfig); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(environment, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData( + runnableConfig, + new Dictionary() { { "ChoiceParam", new MultiValueParameter(new[] { "SecondChoice", "ThirdChoice" }) } }); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(environment, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Veryfying the outputs + // + + string resultContent = environment.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourcFile")); + Assert.Equal(expectedSnippet, resultContent); + } + + [Fact] + public async Task CreateAsyncTest_MultiChoiceParamAndConditionMacro() + { + // + // Template content preparation + // + + string templateConfig = /*lang=json,strict*/ """ + { + "$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 + { TestFileSystemUtils.DefaultConfigRelativePath, templateConfig }, + //content + { "sourcFile", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + + IEngineEnvironmentSettings environment = _environmentSettingsHelper.CreateEnvironment(); + string sourceBasePath = environment.GetTempVirtualizedPath(); + string targetDir = environment.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(environment, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = environment.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromString(templateConfig); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(environment, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData( + runnableConfig, + new Dictionary() { { "Platform", new MultiValueParameter(new[] { "android", "iOS" }) } }); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(environment, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Veryfying the outputs + // + + string resultContent = environment.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourcFile")); + Assert.Equal(expectedSnippet, resultContent); + } + + [Fact] + public async Task CreateAsyncTest_MultiChoiceParamJoining() + { + // + // Template content preparation + // + + string templateConfig = /*lang=json*/ """ + { + "identity": "test.template", + "name": "test", + "shortName": "test", + "symbols": { + "Platform": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "allowMultipleValues": 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" + }, + "joinedRename": { + "type": "generated", + "generator": "join", + "replaces": "SupportedPlatforms", + "parameters": { + "symbols": [ + { + "type": "ref", + "value": "Platform" + } + ], + "separator": ", ", + "removeEmptyValues": true, + } + } + } + } + """; + + string sourceSnippet = """ + // This file is generated for platfrom: SupportedPlatforms + """; + + string expectedSnippet = """ + // This file is generated for platfrom: MacOS, iOS + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, templateConfig }, + //content + { "sourcFile", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + + IEngineEnvironmentSettings environment = _environmentSettingsHelper.CreateEnvironment(); + string sourceBasePath = environment.GetTempVirtualizedPath(); + string targetDir = environment.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(environment, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = environment.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromString(templateConfig); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(environment, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData( + runnableConfig, + new Dictionary() { { "Platform", new MultiValueParameter(new[] { "MacOS", "iOS" }) } }); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(environment, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Veryfying the outputs + // + + string resultContent = environment.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourcFile")); + Assert.Equal(expectedSnippet, resultContent); + } + + [Fact] + public async Task Test_CoaleseWithInvalidSetup() + { + // + // Template content preparation + // + + var templateConfig = new + { + identity = "test.template", + name = "test", + shortName = "test", + symbols = new + { + safesourcename = new + { + type = "generated", + generator = "coalesce", + parameters = new + { + sourceVariableName = "safe_namespace", + fallbackVariableName = "safe_name" + }, + replaces = "%R1%" + }, + } + }; + + string sourceSnippet = """ + %R1% + """; + + string expectedSnippet = """ + %R1% + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, JsonSerializer.Serialize(templateConfig, new JsonSerializerOptions { WriteIndented = true }) }, + //content + { "sourceFile", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + IEngineEnvironmentSettings settings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: "TestHost", virtualize: true); + string sourceBasePath = settings.GetTempVirtualizedPath(); + string targetDir = settings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(settings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = settings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new RunnableProjectGenerator(); + TemplateConfigModel configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(templateConfig))!.AsObject()); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(settings, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parametersData = new ParameterSetData(runnableConfig); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + + await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parametersData, targetDir, CancellationToken.None); + + // + // Veryfying the outputs + // + + string resultContent = settings.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourceFile")); + Assert.Equal(expectedSnippet, resultContent); + } + +#pragma warning disable xUnit1004 // Test methods should not be skipped + [Fact(Skip = "https://github.com/dotnet/templating/issues/4988")] +#pragma warning restore xUnit1004 // Test methods should not be skipped + public async Task XMLConditionFailure() + { + // + // Template content preparation + // + + var templateConfig = new + { + identity = "test.template", + symbols = new + { + A = new + { + type = "parameter", + dataType = "bool", + defaultValue = "false" + }, + B = new + { + type = "parameter", + dataType = "bool", + defaultValue = "false" + }, + } + }; + + string sourceSnippet = """ + + + foo + + + This text should not be generated, just to make file content longer to prove the bug. + If the buffer is advanced when evaluating condition, the bug won't be reproduced. + This text ensures that buffer is long enough even considering very-very-long env variable names available on CI machine. + + """; + + string expectedSnippet = """ + + foo + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, JsonSerializer.Serialize(templateConfig, new JsonSerializerOptions { WriteIndented = true }) }, + + //content + { "sourceFile.md", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + IEngineEnvironmentSettings settings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + string sourceBasePath = settings.GetTempVirtualizedPath(); + string targetDir = settings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(settings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = settings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new(); + + TemplateConfigModel configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(templateConfig))!.AsObject()); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(settings, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parameters = new( + runnableConfig, + new Dictionary() { { "A", true } }); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parameters, targetDir, CancellationToken.None); + + // + // Veryfying the outputs + // + + string resultContent = settings.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourceFile.md")); + Assert.Equal(expectedSnippet, resultContent); + } + +#pragma warning disable xUnit1004 // Test methods should not be skipped + [Fact(Skip = "https://github.com/dotnet/templating/issues/4988")] +#pragma warning restore xUnit1004 // Test methods should not be skipped + public async Task HashConditionFailure() + { + // + // Template content preparation + // + + var templateConfig = new + { + identity = "test.template", + symbols = new + { + A = new + { + type = "parameter", + dataType = "bool", + defaultValue = "false" + }, + B = new + { + type = "parameter", + dataType = "bool", + defaultValue = "false" + }, + } + }; + + string sourceSnippet = """ + #if (A) + # comment foo + foo + #endif + ##if (B) + This text should not be generated, just to make file content longer to prove the bug. + If the buffer is advanced when evaluating condition, the bug won't be reproduced. + This text ensures that buffer is long enough even considering very-very-long env variable names available on CI machine. + #endif + """; + + string expectedSnippet = """ + # comment foo + foo + """; + + IDictionary templateSourceFiles = new Dictionary + { + // template.json + { TestFileSystemUtils.DefaultConfigRelativePath, JsonSerializer.Serialize(templateConfig, new JsonSerializerOptions { WriteIndented = true }) }, + + //content + { "sourceFile.yaml", sourceSnippet } + }; + + // + // Dependencies preparation and mounting + // + IEngineEnvironmentSettings settings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + string sourceBasePath = settings.GetTempVirtualizedPath(); + string targetDir = settings.GetTempVirtualizedPath(); + + TestFileSystemUtils.WriteTemplateSource(settings, sourceBasePath, templateSourceFiles); + using IMountPoint sourceMountPoint = settings.MountPath(sourceBasePath); + RunnableProjectGenerator rpg = new(); + TemplateConfigModel configModel = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(templateConfig))!.AsObject()); + using RunnableProjectConfig runnableConfig = new RunnableProjectConfig(settings, rpg, configModel, sourceMountPoint.Root); + ParameterSetData parameters = new( + runnableConfig, + new Dictionary() { { "A", true } }); + IDirectory sourceDir = sourceMountPoint!.DirectoryInfo("/")!; + + // + // Running the actual scenario: template files processing and generating output (including macros processing) + // + await RunnableProjectGenerator.CreateAsync(settings, runnableConfig, sourceDir, parameters, targetDir, CancellationToken.None); + + // + // Veryfying the outputs + // + + string resultContent = settings.Host.FileSystem.ReadAllText(Path.Combine(targetDir, "sourceFile.yaml")); + Assert.Equal(expectedSnippet, resultContent); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/ScanTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/ScanTests.cs new file mode 100644 index 000000000000..33028c8605dd --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/ScanTests.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests +{ + public class ScanTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public ScanTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Fact] + public async Task CanReadPostActions() + { + var jsonToBe = new + { + name = "TestTemplate", + identity = "id", + shortName = "test", + postActions = new[] + { + new + { + actionId = Guid.NewGuid(), + }, + new + { + actionId = Guid.NewGuid(), + }, + } + }; + IEngineEnvironmentSettings environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + string sourceBasePath = environmentSettings.GetTempVirtualizedPath(); + + string templateConfigDir = Path.Combine(sourceBasePath, RunnableProjectGenerator.TemplateConfigDirectoryName); + string filePath = Path.Combine(templateConfigDir, RunnableProjectGenerator.TemplateConfigFileName); + environmentSettings.Host.FileSystem.CreateDirectory(templateConfigDir); + environmentSettings.Host.FileSystem.WriteAllText(filePath, JsonSerializer.Serialize(jsonToBe)); + + using IMountPoint mountPoint = environmentSettings.MountPath(sourceBasePath); + RunnableProjectGenerator generator = new RunnableProjectGenerator(); + IReadOnlyList? templates = await (generator as IGenerator).GetTemplatesFromMountPointAsync(mountPoint, default); + + Assert.Single(templates); + var template = templates[0]; + Assert.Equal(new[] { jsonToBe.postActions[0].actionId, jsonToBe.postActions[1].actionId }, template.PostActions); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/BasicTest.json b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/BasicTest.json new file mode 100644 index 000000000000..2a10a3146fbc --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/BasicTest.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Mads Kristensen", + "classifications": [ "Web", "MVC" ], + "name": "ASP.NET Core MVC Basic", + "defaultName": "WebApplication1", + "identity": "madsk.mvc.basic", + "groupIdentity": "madsk.mvc.basic", + "shortName": "mvcbasic", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "Mvc.Basic", + "preferNameDirectory": true, + "primaryOutputs": [ { "path": "Mvc.Basic.csproj" } ], + "sources": [ + { + + } + ], + "symbols": { + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "displayName": "Target framework", + "choices": [ + { + "choice": "netcoreapp1.1", + "description": "Target netcoreapp1.1", + "displayName": ".NET Core 1.1" + } + ], + "defaultValue": "netcoreapp1.1" + } + }, + "guids": [ + "baf04077-a3c0-454b-ac6f-9fec00b8e170" + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/ConditionalParametersTest.json b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/ConditionalParametersTest.json new file mode 100644 index 000000000000..7664ed155b8b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/ConditionalParametersTest.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithConditionalParameters", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithConditionalParameters", + "precedence": "100", + "identity": "TestAssets.TemplateWithConditionalParameters", + "shortName": "TestAssets.TemplateWithConditionalParameters", + "tags": { + "language": "C#", + "type": "project" + }, + "symbols": { + "A": { + "type": "parameter", + "datatype": "string", + "isEnabled": "A_enabled", + "isRequired": "A_enabled", + "replaces": "paramA" + }, + "B": { + "type": "parameter", + "datatype": "string", + "isEnabled": "B_enabled", + "isRequired": "true", + "replaces": "paramB" + }, + "A_enabled": { + "type": "parameter", + "datatype": "bool", + "isRequired": true + }, + "B_enabled": { + "type": "parameter", + "datatype": "bool" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/ConstraintsTest.json b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/ConstraintsTest.json new file mode 100644 index 000000000000..ec9adc38235d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/ConstraintsTest.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test", + "classifications": [ "Web", "MVC" ], + "name": "Test", + "defaultName": "WebApplication1", + "identity": "Test", + "groupIdentity": "Test", + "shortName": "test", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "test.Basic", + "preferNameDirectory": true, + "primaryOutputs": [ { "path": "test.Basic.csproj" } ], + "sources": [ + { + + } + ], + "symbols": { + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "displayName": "Target framework", + "choices": [ + { + "choice": "netcoreapp1.1", + "description": "Target netcoreapp1.1", + "displayName": ".NET Core 1.1" + } + ], + "defaultValue": "netcoreapp1.1" + } + }, + "constraints": { + "one": { + "type": "t1", + "args": "string" + }, + "two": { + "type": "t2", + "args": [ + "one", + "two" + ] + }, + "three": { + "type": "t3", + "args": { + "a1": "val1", + "a2": { + "a3": "val2" + } + } + }, + "four": { + "type": "t4" + }, + "only-win": { + "type": "os", + "args": "Windows" + }, + "unix-and-osx": { + "type": "os", + "args": [ "OSX", "Windows" ] + }, + "VS": { + "type": "host", + "args": [ + { + "hostname": "vs" + } + ] + }, + "dotnet": { + "type": "host", + "args": [ + { + "hostname": "dotnetcli", + "version": "6.0.*" + } + ] + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/GeneratorTest.json b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/GeneratorTest.json new file mode 100644 index 000000000000..db3ef2464ce6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/GeneratorTest.json @@ -0,0 +1,230 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test", + "classifications": [ "Web", "MVC" ], + "name": "Test Generator", + "defaultName": "TestGenerator", + "identity": "Test.Generator", + "groupIdentity": "Test.Generator", + "shortName": "testgen", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "Test.Generator", + "preferNameDirectory": true, + "primaryOutputs": [ { "path": "Test.Generator" } ], + "sources": [ + { + + } + ], + "symbols": { + "createddate": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "5001" + }, + "replaces": "1234" + }, + "ownername": { + "type": "parameter", + "datatype": "text", + "replaces": "John Smith (a)", + "defaultValue": "John Doe" + }, + "nameUpper": { + "type": "generated", + "generator": "casing", + "parameters": { + "source": "ownername", + "toLower": false + }, + "replaces": "John Smith (U)" + }, + "nameLower": { + "type": "generated", + "generator": "casing", + "parameters": { + "source": "ownername", + "toLower": true + }, + "replaces": "John Smith (l)" + }, + "MessageYear": { + "type": "parameter", + "datatype": "int" + }, + "ThisYear": { + "type": "generated", + "generator": "now", + "parameters": { + "format": "yyyy" + } + }, + "YearReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "MessageYear", + "fallbackVariableName": "ThisYear" + }, + "replaces": "1234" + }, + "myconstant": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "5001" + }, + "replaces": "1234" + }, + "IndividualAuth": { + "type": "computed", + "value": "(auth == \"IndividualB2C\")" + }, + "KestrelPortGenerated": { + "type": "generated", + "generator": "port", + "parameters": { + "fallback": 5000 + } + }, + "id01": { + "type": "generated", + "generator": "guid", + "replaces": "myid01", + "parameters": { + "format": "N" + } + }, + "id02": { + "type": "generated", + "generator": "guid", + "replaces": "myid02", + "parameters": { + "format": "D" + } + }, + "id03": { + "type": "generated", + "generator": "guid", + "replaces": "myid03", + "parameters": { + "format": "B" + } + }, + "id04": { + "type": "generated", + "generator": "guid", + "replaces": "myid04", + "parameters": { + "format": "P" + } + }, + "id05": { + "type": "generated", + "generator": "guid", + "replaces": "myid05", + "parameters": { + "format": "X" + } + }, + "createddatenow": { + "type": "generated", + "generator": "now", + "parameters": { + "format": "MM/dd/yyyy" + }, + "replaces": "01/01/1999" + }, + "myRandomNumber": { + "type": "generated", + "generator": "random", + "parameters": { + "low": 0, + "high": 10000 + }, + "replaces": "4321" + }, + "test": { + "type": "parameter", + "datatype": "string" + }, + "example": { + "type": "generated", + "generator": "switch", + "replaces": "abc", + "parameters": { + "evaluator": "C++", + "datatype": "string", + "cases": [ + { + "condition": "(test == '123')", + "value": "456" + }, + { + "condition": "(test == '789')", + "value": "012" + } + ] + } + }, + "case3": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "replaces": "case3", + "fileRename" : "file_case3", + "parameters": { + "source": "case1", + "steps": [ + { + "regex": "\\.", + "replacement": "" + } + ] + } + }, + "joinedRename": { + "type": "generated", + "generator": "join", + "fileRename": "Api", + "parameters": { + "symbols": [ + { + "type": "const", + "value": "Source" + }, + { + "type": "const", + "value": "Api" + }, + { + "type": "ref", + "value": "company" + }, + { + "type": "ref", + "value": "product" + } + ], + "separator": "/" + } + }, + "isMatch": { + "type": "generated", + "generator": "regexMatch", + "dataType": "bool", + "replaces": "test.value1", + "parameters": { + "source": "name", + "pattern": "^hello$" + } + } + }, + "guids": [ + "baf04077-a3c0-454b-ac6f-9fec00b8e170" + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/JSONSchemaTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/JSONSchemaTests.cs new file mode 100644 index 000000000000..bae0bdb31a4f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/JSONSchemaTests.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Json.Schema; +using Microsoft.TemplateEngine.Tests; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.SchemaTests +{ + public class JSONSchemaTests : TestBase + { + [Theory(DisplayName = nameof(IsJSONSchemaValid))] + [InlineData(@"SchemaTests/BasicTest.json")] + [InlineData(@"SchemaTests/GeneratorTest.json")] + [InlineData(@"SchemaTests/StarterWebTest.json")] + [InlineData(@"SchemaTests/PostActionTest.json")] + [InlineData(@"SchemaTests/SymbolsTest.json")] + [InlineData(@"SchemaTests/MultiValueChoiceTest.json")] + [InlineData(@"SchemaTests/ConstraintsTest.json")] + [InlineData(@"SchemaTests/ConditionalParametersTest.json")] + public void IsJSONSchemaValid(string testFile) + { + string schemaContent = File.ReadAllText(@"SchemaTests/template.json"); + var schema = JsonSchema.FromText(schemaContent); + + string jsonContent = File.ReadAllText(testFile); + var jsonNode = JsonNode.Parse(jsonContent, null, new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); + + var result = schema.Evaluate(jsonNode, new EvaluationOptions { OutputFormat = OutputFormat.List }); + + var errors = result.Details? + .Where(d => !d.IsValid && d.Errors != null) + .SelectMany(d => d.Errors!.Values) + .ToList() ?? new List(); + + Assert.True( + result.IsValid, + "The JSON file is not valid against the schema" + + Environment.NewLine + + string.Join(Environment.NewLine, errors)); + } + + private static readonly string JsonLocation = Path.Combine(".template.config", "template.json"); + + public static IEnumerable GetAllTemplates() + { + //those templates are intentionally wrong + string[] exceptions = new[] { "MissingIdentity", "MissingMandatoryConfig" }; + + return Directory.EnumerateFiles(TestTemplatesLocation, "template.json", SearchOption.AllDirectories) + .Where(s => s.Contains(".template.config")) + .Where(s => !exceptions.Any(e => s.Contains(e))) + .Select(s => s.Remove(s.Length - JsonLocation.Length).Remove(0, TestTemplatesLocation.Length).Trim(Path.DirectorySeparatorChar)) + .Select(s => new object?[] { s }); + } + + [Theory] + [MemberData(nameof(GetAllTemplates))] + public void TestAllTestTemplatesHaveValidJson(string testTemplateName) + { + string testFile = Path.Combine(TestTemplatesLocation, testTemplateName, JsonLocation); + + IsJSONSchemaValid(testFile); + } + + public static IEnumerable GetAllTemplateSamples() + { + //those templates are intentionally wrong + //string[] exceptions = new[] { "MissingIdentity", "MissingMandatoryConfig" }; + + return Directory.EnumerateFiles(SampleTemplatesLocation, "template.json", SearchOption.AllDirectories) + .Where(s => s.Contains(".template.config")) + //.Where(s => !exceptions.Any(e => s.Contains(e))) + .Select(s => s.Remove(s.Length - JsonLocation.Length).Remove(0, SampleTemplatesLocation.Length).Trim(Path.DirectorySeparatorChar)) + .Select(s => new object?[] { s }); + } + + [Theory] + [MemberData(nameof(GetAllTemplateSamples))] + public void TestAllSampleTemplatesHaveValidJson(string testTemplateName) + { + string testFile = Path.Combine(SampleTemplatesLocation, testTemplateName, JsonLocation); + IsJSONSchemaValid(testFile); + } + } +} + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/MultiValueChoiceTest.json b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/MultiValueChoiceTest.json new file mode 100644 index 000000000000..6b9b11a6b230 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/MultiValueChoiceTest.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithMultiValueChoice", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithMultiValueChoice", + "precedence": "100", + "identity": "TestAssets.TemplateWithMultiValueChoice", + "shortName": "TestAssets.TemplateWithMultiValueChoice", + "tags": { + "language": "C#", + "type": "project" + }, + "symbols": { + "Platform": { + "type": "parameter", + "description": "The target framework 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" + }, + "joinedRename": { + "type": "generated", + "generator": "join", + "replaces": "SupportedPlatforms", + "parameters": { + "symbols": [ + { + "type": "ref", + "value": "Platform" + } + ], + "separator": ", ", + "removeEmptyValues": true + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/PostActionTest.json b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/PostActionTest.json new file mode 100644 index 000000000000..b299fa520f09 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/PostActionTest.json @@ -0,0 +1,145 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test", + "classifications": [ "Web", "MVC" ], + "name": "Test Post Actions", + "defaultName": "TestPostAction", + "identity": "Test.PostAction", + "groupIdentity": "Test.PostAction", + "shortName": "testpostactions", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "Test.Generator", + "preferNameDirectory": true, + "primaryOutputs": [ { "path": "Test.Generator" } ], + "sources": [ + { + + } + ], + "postActions": [ + { + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + }, + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true, + "args": { + "files": "singlefile" + } + }, + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true, + "args": { + "files": [ "array", "of", "files" ] + } + }, + { + "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", + "args": { + "executable": "setup.cmd", + "args": "", + "redirectStandardOutput": false, + "redirectStandardError": true + }, + "manualInstructions": [ + { + "text": "Run 'setup.cmd'" + } + ], + "continueOnError": false, + "description ": "setups the project by calling setup.cmd" + }, + { + "condition": "(HostIdentifier != \"dotnetcli\")", + "description": "Opens Class1.cs in the editor", + "manualInstructions": [], + "actionId": "84C0DA21-51C8-4541-9940-6CA19AF04EE6", + "args": { + "files": "1" + }, + "continueOnError": true + }, + { + "description": "Adding Reference to Microsoft.NET.Sdk.Functions Nuget package", + "actionId": "B17581D1-C5C9-4489-8F0A-004BE667B814", + "id": "add-reference-1", + "continueOnError": false, + "manualInstructions": [ + { + "text": "Manually add the reference to Microsoft.NET.Sdk.Functions to your project file", + "id": "instruction-1" + } + ], + "args": { + "referenceType": "package", + "reference": "Microsoft.NET.Sdk.Functions", + "version": "1.0.0", + "projectFileExtensions": ".csproj" + } + }, + { + "condition": "(postActionAddToSln)", + "description": "Add projects to solution", + "manualInstructions": [ { "text": "Run post action 'Add projects to a solution file'" } ], + "args": { + "primaryOutputIndexes": "0;2", + "solutionFolder": "src" + }, + "actionId": "D396686C-DE0E-4DE6-906D-291CD29FC5DE", + "continueOnError": true + }, + { + "condition": "(OS != \"Windows_NT\")", + "description": "Make scripts executable", + "manualInstructions": [ + { + "text": "Run 'chmod +x *.sh'" + } + ], + "actionId": "cb9a6cf3-4f5c-4860-b9d2-03a574959774", + "args": { + "+x": "*.sh" + }, + "continueOnError": true + }, + { + "description": "Add a property to an existing JSON file", + "manualInstructions": [ + { + "text": "Check if jsonFileName, newJsonPropertyName and newJsonPropertyValue are specified." + } + ], + "actionId": "695A3659-EB40-4FF5-A6A6-C9C4E629FCB0", + "args": { + "jsonFileName": "test.json", + "newJsonPropertyName": "newKey", + "newJsonPropertyValue": "newValue" + }, + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/StarterWebTest.json b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/StarterWebTest.json new file mode 100644 index 000000000000..b6b9e0718417 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/StarterWebTest.json @@ -0,0 +1,327 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Microsoft", + "classifications": [ "Web", "MVC" ], + "name": "ASP.NET Core Web App (Model-View-Controller)", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "Microsoft.Web.Mvc", + "precedence": "2000", + "identity": "Microsoft.Web.Mvc.CSharp.2.0", + "shortName": "mvc", + "thirdPartyNotices": "https://aka.ms/template-3pn", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "Company.WebApplication1", + "preferNameDirectory": true, + "guids": [ + "09732173-2cef-46b7-83db-1334bcb079d3", + "53bc9b9d-9d6a-45d4-8429-2a2761773502", + "ab1d2251-be0b-4457-abfe-4686ff9286c0", + "d2e0a81e-e08e-42ea-bbae-bec4c4ac6aed", + "c9c97e6d-e0fc-4f75-b7ca-d43515b68ee3", + "8f87a3e2-5ac9-4852-8cc9-35799e66f898" + ], + "sources": [ + { + "modifiers": [ + { + "condition": "(!IndividualAuth && !OrganizationalAuth)", + "exclude": [ + "Controllers/AccountController.cs", + "Views/Account/**", + "Views/Shared/_LoginPartial.cshtml", + "Extensions/AuthenticationServiceCollectionExtensions.cs" + ] + }, + { + "condition": "(!IndividualLocalAuth)", + "exclude": [ + "Areas/**", + "Extensions/IdentityService/**" + ] + }, + { + "condition": "(!IncludeLaunchSettings)", + "exclude": [ + "Properties/launchSettings.json" + ] + }, + { + "condition": "(!OrganizationalAuth)", + "exclude": [ + "Extensions/AzureAd/**" + ] + }, + { + "condition": "(!IndividualB2CAuth)", + "exclude": [ + "Extensions/AzureAdB2C/**" + ] + } + ] + } + ], + "symbols": { + "auth": { + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "None", + "description": "No authentication" + }, + { + "choice": "Individual", + "description": "Individual authentication" + }, + { + "choice": "IndividualB2C", + "description": "Individual authentication with Azure AD B2C" + }, + { + "choice": "SingleOrg", + "description": "Organizational authentication for a single tenant" + }, + { + "choice": "MultiOrg", + "description": "Organizational authentication for multiple tenants" + }, + { + "choice": "Windows", + "description": "Windows authentication" + } + ], + "defaultValue": "None", + "description": "The type of authentication to use" + }, + "AAdB2CInstance": { + "type": "parameter", + "datatype": "string", + "defaultValue": "https://login.microsoftonline.com/tfp/", + "replaces": "https:////login.microsoftonline.com/tfp/", + "description": "The Azure Active Directory B2C instance to connect to (use with IndividualB2C auth type)." + }, + "SignUpSignInPolicyId": { + "type": "parameter", + "datatype": "string", + "defaultValue": "", + "replaces": "MySignUpSignInPolicyId", + "description": "The sign-in and sign-up policy ID for this project (use with IndividualB2C auth type)." + }, + "ResetPasswordPolicyId": { + "type": "parameter", + "datatype": "string", + "defaultValue": "", + "replaces": "MyResetPasswordPolicyId", + "description": "The reset password policy ID for this project (use with IndividualB2C auth type)." + }, + "EditProfilePolicyId": { + "type": "parameter", + "datatype": "string", + "defaultValue": "", + "replaces": "MyEditProfilePolicyId", + "description": "The edit profile policy ID for this project (use with IndividualB2C auth type)." + }, + "AADInstance": { + "type": "parameter", + "datatype": "string", + "defaultValue": "https://login.microsoftonline.com/", + "replaces": "https:////login.microsoftonline.com/", + "description": "The Azure Active Directory instance to connect to (use with Single-org or Multi-org auth types)." + }, + "ClientId": { + "type": "parameter", + "datatype": "string", + "replaces": "11111111-1111-1111-11111111111111111", + "description": "The Client ID for this project (use with individual and organizational auth types)." + }, + "Domain": { + "type": "parameter", + "datatype": "string", + "replaces": "qualified.domain.name", + "description": "The domain for the directory tenant (use with Single-org or IndividualB2C auth types)." + }, + "TenantId": { + "type": "parameter", + "datatype": "string", + "replaces": "22222222-2222-2222-2222-222222222222", + "description": "The TenantId ID of the directory to connect to (use with Single-org auth types)." + }, + "CallbackPath": { + "type": "parameter", + "datatype": "string", + "replaces": "/signin-oidc", + "defaultValue": "/signin-oidc", + "description": "The request path within the application's base path of the redirect URI (use with Single-org or IndividualB2C auth types)." + }, + "OrgReadAccess": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether or not to allow this application read access to the directory (only applies to SingleOrg or MultiOrg auth types)." + }, + "UserSecretsId": { + "type": "parameter", + "datatype": "string", + "replaces": "aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502", + "defaultValue": "aspnet-Company.WebApplication1-53bc9b9d-9d6a-45d4-8429-2a2761773502", + "description": "The ID to use for secrets (use with OrgReadAccess or Individual auth)." + }, + "IncludeLaunchSettings": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether to include launchSettings.json in the generated template." + }, + "HttpsPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure SSL in launchSettings.json." + }, + "HttpsPortGenerated": { + "type": "generated", + "generator": "port" + }, + "HttpsPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "HttpsPort", + "fallbackVariableName": "HttpsPortGenerated" + }, + "replaces": "43434" + }, + "KestrelPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure Kestrel in launchSettings.json." + }, + "KestrelPortGenerated": { + "type": "generated", + "generator": "port" + }, + "KestrelPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "KestrelPort", + "fallbackVariableName": "KestrelPortGenerated" + }, + "replaces": "5000" + }, + "IISExpressPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use to configure IIS Express in launchSettings.json." + }, + "IISExpressPortGenerated": { + "type": "generated", + "generator": "port" + }, + "IISExpressPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "IISExpressPort", + "fallbackVariableName": "IISExpressPortGenerated" + }, + "replaces": "55555" + }, + "OrganizationalAuth": { + "type": "computed", + "value": "(auth == \"SingleOrg\" || auth == \"MultiOrg\")" + }, + "WindowsAuth": { + "type": "computed", + "value": "(auth == \"Windows\")" + }, + "MultiOrgAuth": { + "type": "computed", + "value": "(auth == \"MultiOrg\")" + }, + "SingleOrgAuth": { + "type": "computed", + "value": "(auth == \"SingleOrg\")" + }, + "IndividualLocalAuth": { + "type": "computed", + "value": "(auth == \"Individual\")" + }, + "IndividualAuth": { + "type": "computed", + "value": "(auth == \"Individual\" || auth == \"IndividualB2C\")" + }, + "IndividualB2CAuth": { + "type": "computed", + "value": "(auth == \"IndividualB2C\")" + }, + "NoAuth": { + "type": "computed", + "value": "(!(IndividualAuth || OrganizationalAuth || WindowsAuth))" + }, + "RequiresHttps": { + "type": "computed", + "value": "(OrganizationalAuth || IndividualAuth)" + }, + "UseLocalDB": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Whether or not to use LocalDB instead of SQLite" + }, + "TargetFrameworkOverride": { + "type": "parameter", + "description": "Overrides the target framework", + "replaces": "TargetFrameworkOverride", + "datatype": "string", + "defaultValue": "" + }, + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "netcoreapp2.0", + "description": "Target netcoreapp2.0" + } + ], + "replaces": "netcoreapp2.0", + "defaultValue": "netcoreapp2.0" + }, + "copyrightYear": { + "type": "generated", + "generator": "now", + "replaces": "1975", + "parameters": { + "format": "yyyy" + } + }, + "NoTools": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, + "skipRestore": { + "type": "parameter", + "datatype": "bool", + "description": "If specified, skips the automatic restore of the project on create.", + "defaultValue": "false" + } + }, + "primaryOutputs": [ { "path": "Company.WebApplication1.csproj" } ], + "defaultName": "WebApplication1", + "postActions": [ + { + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { "text": "Run 'dotnet restore'" } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/SymbolsTest.json b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/SymbolsTest.json new file mode 100644 index 000000000000..15235ae97393 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SchemaTests/SymbolsTest.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test", + "classifications": [ "Test" ], + "name": "Test Symbols", + "defaultName": "SymbolsTest", + "identity": "Symbols.Test", + "groupIdentity": "Symbols.Test", + "shortName": "symbols-test", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "SymbolsTest", + "preferNameDirectory": true, + "symbols": { + "TestParameter": { + "type": "parameter", + "description": "Test Choice Parameter", + "datatype": "choice", + "choices": [ + { + "choice": "ch1", + "description": "Choice 1" + }, + { + "choice": "ch2", + "description": "Choice 2" + } + ], + "defaultValue": "ch1", + "replaces": "Test1", + "fileRename": "Test1", + "isRequired": true, + "onlyIf": [ + { + "after": "T" + }, + { + "before": "A" + } + ] + }, + "TestDerived": { + "type": "derived", + "datatype": "string", + "replaces": "Test2", + "fileRename": "Test2", + "valueSource": "name", + "valueTransform": "form1" + }, + "TestComputed": { + "type": "computed", + "datatype": "bool", + "value": "(TestParameter == \"ch1\")" + }, + "TestGenerated": { + "type": "generated", + "generator": "casing", + "datatype": "string", + "parameters": { + "source": "name", + "toLower": true + }, + "replaces": "Test4", + "fileRename": "Test4" + }, + "TestBinding": { + "type": "bind", + "binding": "HostIdentifier", + "datatype": "string" + } + }, + "guids": [ + "baf04077-a3c0-454b-ac6f-9fec00b8e170" + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Serialization/Extensions.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Serialization/Extensions.cs new file mode 100644 index 000000000000..bc018d0a122d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Serialization/Extensions.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.Serialization +{ + internal static class Extensions + { + /// + /// Serializes model to JSON string. + /// Note that not all members of are supported. + /// For details, see . + /// + /// when attempting to serialize model which contains unsupported member. + internal static string ToJsonString(this TemplateConfigModel templateConfigModel) + => JsonSerializer.Serialize(templateConfigModel, new JsonSerializerOptions { WriteIndented = true, Converters = { TemplateConfigModelJsonConverter.Instance, ParameterSymbolJsonConverter.Instance } }); + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Serialization/ParameterSymbolJsonConverter.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Serialization/ParameterSymbolJsonConverter.cs new file mode 100644 index 000000000000..7c9170f4602b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Serialization/ParameterSymbolJsonConverter.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.Serialization +{ + internal class ParameterSymbolJsonConverter : JsonConverter + { + //falls back to default de-serializer if not implemented + public override ParameterSymbol Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + + public override void Write(Utf8JsonWriter writer, ParameterSymbol value, JsonSerializerOptions options) + { + if (value == null) + { + return; + } + + writer.WritePropertyName(value.Name); + writer.WriteStartObject(); + + writer.WritePropertyName("type"); + writer.WriteStringValue(value.Type); + + if (!string.IsNullOrEmpty(value.FileRename)) + { + writer.WritePropertyName("fileRename"); + writer.WriteStringValue(value.FileRename); + } + + if (!string.IsNullOrEmpty(value.Replaces)) + { + writer.WritePropertyName("replaces"); + writer.WriteStringValue(value.Replaces); + } + + if (value.ReplacementContexts.Any()) + { + throw new NotSupportedException("Serializing replacement context is not supported."); + } + + if (value.Forms != SymbolValueFormsModel.Default) + { + throw new NotSupportedException("Serializing forms is not supported."); + } + + if (!string.IsNullOrEmpty(value.DefaultValue)) + { + writer.WritePropertyName("defaultValue"); + writer.WriteStringValue(value.DefaultValue); + } + + if (!string.IsNullOrEmpty(value.DataType)) + { + writer.WritePropertyName("datatype"); + writer.WriteStringValue(value.DataType); + } + + if (!string.IsNullOrEmpty(value.DisplayName)) + { + writer.WritePropertyName("displayName"); + writer.WriteStringValue(value.DisplayName); + } + + if (!string.IsNullOrEmpty(value.Description)) + { + writer.WritePropertyName("description"); + writer.WriteStringValue(value.Description); + } + + if (!string.IsNullOrEmpty(value.DefaultIfOptionWithoutValue) && value.DataType != "bool") + { + writer.WritePropertyName("defaultIfOptionWithoutValue"); + writer.WriteStringValue(value.DefaultIfOptionWithoutValue); + } + + if (value.AllowMultipleValues) + { + writer.WritePropertyName("allowMultipleValues"); + writer.WriteBooleanValue(value.AllowMultipleValues); + } + + if (value.EnableQuotelessLiterals) + { + writer.WritePropertyName("enableQuotelessLiterals"); + writer.WriteBooleanValue(value.EnableQuotelessLiterals); + } + + if (value.IsRequired) + { + writer.WritePropertyName("isRequired"); + writer.WriteBooleanValue(value.IsRequired); + } + else if (!string.IsNullOrEmpty(value.IsRequiredCondition)) + { + writer.WritePropertyName("isRequired"); + writer.WriteStringValue(value.IsRequiredCondition); + } + + if (value.Precedence.PrecedenceDefinition == PrecedenceDefinition.Disabled) + { + writer.WritePropertyName("isEnabled"); + writer.WriteBooleanValue(false); + } + else if (!string.IsNullOrEmpty(value.IsEnabledCondition)) + { + writer.WritePropertyName("isEnabled"); + writer.WriteStringValue(value.IsEnabledCondition); + } + + if (value.Choices != null) + { + writer.WritePropertyName("choices"); + writer.WriteStartArray(); + foreach (KeyValuePair choice in value.Choices) + { + writer.WriteStartObject(); + writer.WritePropertyName("choice"); + writer.WriteStringValue(choice.Key); + if (!string.IsNullOrEmpty(choice.Value.DisplayName)) + { + writer.WritePropertyName("displayName"); + writer.WriteStringValue(choice.Value.DisplayName); + } + + if (!string.IsNullOrEmpty(choice.Value.Description)) + { + writer.WritePropertyName("description"); + writer.WriteStringValue(choice.Value.Description); + } + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + + writer.WriteEndObject(); + } + + internal static ParameterSymbolJsonConverter Instance { get; } = new ParameterSymbolJsonConverter(); + } + +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Serialization/TemplateConfigModelJsonConverter.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Serialization/TemplateConfigModelJsonConverter.cs new file mode 100644 index 000000000000..cc12f1dadafb --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/Serialization/TemplateConfigModelJsonConverter.cs @@ -0,0 +1,423 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.Serialization +{ + internal class TemplateConfigModelJsonConverter : JsonConverter + { + //falls back to default de-serializer if not implemented + public override TemplateConfigModel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => throw new NotImplementedException(); + + public override void Write(Utf8JsonWriter writer, TemplateConfigModel value, JsonSerializerOptions options) + { + if (value == null) + { + return; + } + writer.WriteStartObject(); + writer.WritePropertyName("identity"); + writer.WriteStringValue(value.Identity); + writer.WritePropertyName("name"); + writer.WriteStringValue(value.Name); + writer.WritePropertyName("shortName"); + + if (value.ShortNameList.Count > 1) + { + writer.WriteStartArray(); + foreach (string shortName in value.ShortNameList) + { + if (!string.IsNullOrEmpty(shortName)) + { + writer.WriteStringValue(shortName); + } + } + writer.WriteEndArray(); + } + else if (value.ShortNameList.Count == 1) + { + writer.WriteStringValue(value.ShortNameList[0]); + } + else + { + writer.WriteStringValue(string.Empty); + } + + if (!string.IsNullOrEmpty(value.GroupIdentity)) + { + writer.WritePropertyName("groupIdentity"); + writer.WriteStringValue(value.GroupIdentity); + } + if (value.Precedence != 0) + { + writer.WritePropertyName("precedence"); + writer.WriteNumberValue(value.Precedence); + } + if (!string.IsNullOrEmpty(value.Author)) + { + writer.WritePropertyName("author"); + writer.WriteStringValue(value.Author); + } + if (!string.IsNullOrEmpty(value.Description)) + { + writer.WritePropertyName("description"); + writer.WriteStringValue(value.Description); + } + if (!string.IsNullOrEmpty(value.ThirdPartyNotices)) + { + writer.WritePropertyName("thirdPartyNotices"); + writer.WriteStringValue(value.ThirdPartyNotices); + } + if (!string.IsNullOrEmpty(value.DefaultName)) + { + writer.WritePropertyName("defaultName"); + writer.WriteStringValue(value.DefaultName); + } + if (!string.IsNullOrEmpty(value.SourceName)) + { + writer.WritePropertyName("sourceName"); + writer.WriteStringValue(value.SourceName); + } + if (!string.IsNullOrEmpty(value.PlaceholderFilename)) + { + writer.WritePropertyName("placeholderFilename"); + writer.WriteStringValue(value.PlaceholderFilename); + } + if (!string.IsNullOrEmpty(value.GeneratorVersions)) + { + writer.WritePropertyName("generatorVersions"); + writer.WriteStringValue(value.GeneratorVersions); + } + if (value.PreferNameDirectory) + { + writer.WritePropertyName("preferNameDirectory"); + writer.WriteBooleanValue(value.PreferNameDirectory); + } + if (value.PreferDefaultName) + { + writer.WritePropertyName("preferDefaultName"); + writer.WriteBooleanValue(value.PreferDefaultName); + } + + if (value.Classifications.Any()) + { + writer.WritePropertyName("classifications"); + writer.WriteStartArray(); + foreach (string classification in value.Classifications) + { + if (!string.IsNullOrEmpty(classification)) + { + writer.WriteStringValue(classification); + } + } + writer.WriteEndArray(); + } + + if (value.Guids.Any()) + { + writer.WritePropertyName("guids"); + writer.WriteStartArray(); + foreach (Guid guid in value.Guids) + { + writer.WriteStringValue(guid.ToString()); + } + writer.WriteEndArray(); + } + + if (value.Tags.Any()) + { + writer.WritePropertyName("tags"); + writer.WriteStartObject(); + foreach (KeyValuePair tag in value.Tags) + { + writer.WritePropertyName(tag.Key); + writer.WriteStringValue(tag.Value); + } + writer.WriteEndObject(); + } + if (value.Sources.Any()) + { + writer.WritePropertyName("sources"); + writer.WriteStartArray(); + foreach (ExtendedFileSource source in value.Sources) + { + writer.WriteStartObject(); + if (!string.IsNullOrEmpty(source.Source)) + { + writer.WritePropertyName("source"); + writer.WriteStringValue(source.Source); + } + if (!string.IsNullOrEmpty(source.Target)) + { + writer.WritePropertyName("target"); + writer.WriteStringValue(source.Target); + } + if (!string.IsNullOrEmpty(source.Condition)) + { + writer.WritePropertyName("condition"); + writer.WriteStringValue(source.Condition); + } + if (source.Include.Any()) + { + writer.WritePropertyName("include"); + if (source.Include.Count == 1) + { + writer.WriteStringValue(source.Include[0]); + } + else + { + writer.WriteStartArray(); + foreach (string el in source.Include) + { + writer.WriteStringValue(el); + } + writer.WriteEndArray(); + } + } + if (source.Exclude.Any()) + { + writer.WritePropertyName("exclude"); + if (source.Exclude.Count == 1) + { + writer.WriteStringValue(source.Exclude[0]); + } + else + { + writer.WriteStartArray(); + foreach (string el in source.Exclude) + { + writer.WriteStringValue(el); + } + writer.WriteEndArray(); + } + } + if (source.CopyOnly.Any()) + { + writer.WritePropertyName("copyOnly"); + if (source.CopyOnly.Count == 1) + { + writer.WriteStringValue(source.CopyOnly[0]); + } + else + { + writer.WriteStartArray(); + foreach (string el in source.CopyOnly) + { + writer.WriteStringValue(el); + } + writer.WriteEndArray(); + } + } + if (source.Rename.Any()) + { + writer.WritePropertyName("rename"); + foreach (KeyValuePair el in source.Rename) + { + writer.WriteStartObject(); + writer.WritePropertyName(el.Key); + writer.WriteStringValue(el.Value); + writer.WriteEndObject(); + } + } + if (source.Modifiers.Any()) + { + writer.WritePropertyName("modifiers"); + writer.WriteStartArray(); + foreach (SourceModifier mod in source.Modifiers) + { + writer.WriteStartObject(); + if (!string.IsNullOrEmpty(mod.Condition)) + { + writer.WritePropertyName("condition"); + writer.WriteStringValue(mod.Condition); + } + if (mod.Include.Any()) + { + writer.WritePropertyName("include"); + if (mod.Include.Count == 1) + { + writer.WriteStringValue(mod.Include[0]); + } + else + { + writer.WriteStartArray(); + foreach (string el in mod.Include) + { + writer.WriteStringValue(el); + } + writer.WriteEndArray(); + } + } + if (mod.Exclude.Any()) + { + writer.WritePropertyName("exclude"); + if (mod.Exclude.Count == 1) + { + writer.WriteStringValue(mod.Exclude[0]); + } + else + { + writer.WriteStartArray(); + foreach (string el in mod.Exclude) + { + writer.WriteStringValue(el); + } + writer.WriteEndArray(); + } + } + if (mod.CopyOnly.Any()) + { + writer.WritePropertyName("copyOnly"); + if (mod.CopyOnly.Count == 1) + { + writer.WriteStringValue(mod.CopyOnly[0]); + } + else + { + writer.WriteStartArray(); + foreach (string el in mod.CopyOnly) + { + writer.WriteStringValue(el); + } + writer.WriteEndArray(); + } + } + if (mod.Rename.Any()) + { + writer.WritePropertyName("rename"); + foreach (KeyValuePair el in mod.Rename) + { + writer.WriteStartObject(); + writer.WritePropertyName(el.Key); + writer.WriteStringValue(el.Value); + writer.WriteEndObject(); + } + } + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + + if (value.PostActionModels.Any()) + { + writer.WritePropertyName("postActions"); + writer.WriteStartArray(); + foreach (PostActionModel model in value.PostActionModels) + { + writer.WriteStartObject(); + if (!string.IsNullOrEmpty(model.Id)) + { + writer.WritePropertyName("id"); + writer.WriteStringValue(model.Id); + } + writer.WritePropertyName("actionId"); + writer.WriteStringValue(model.ActionId); + if (!string.IsNullOrEmpty(model.Description)) + { + writer.WritePropertyName("description"); + writer.WriteStringValue(model.Description); + } + writer.WritePropertyName("continueOnError"); + writer.WriteBooleanValue(model.ContinueOnError); + + if (model.Args.Any()) + { + writer.WritePropertyName("args"); + writer.WriteStartObject(); + foreach (KeyValuePair arg in model.Args) + { + writer.WritePropertyName(arg.Key); + writer.WriteStringValue(arg.Value); + } + writer.WriteEndObject(); + } + + if (model.ManualInstructionInfo.Any()) + { + writer.WritePropertyName("manualInstructions"); + writer.WriteStartArray(); + foreach (ManualInstructionModel mi in model.ManualInstructionInfo) + { + writer.WriteStartObject(); + writer.WritePropertyName("text"); + writer.WriteStringValue(mi.Text); + if (!string.IsNullOrEmpty(mi.Condition)) + { + writer.WritePropertyName("condition"); + writer.WriteStringValue(mi.Condition); + } + if (!string.IsNullOrEmpty(mi.Id)) + { + writer.WritePropertyName("id"); + writer.WriteStringValue(mi.Id); + } + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + + //not implemented + if (value.Forms.Values.Any(f => !f.IsDefault)) + { + throw new NotSupportedException("Forms are not supported for serialization to JSON."); + } + if (value.BaselineInfo.Any()) + { + throw new NotSupportedException("Baselines are not supported for serialization to JSON."); + } + if (value.Symbols.Any(s => !s.IsImplicit)) + { + writer.WritePropertyName("symbols"); + writer.WriteStartObject(); + foreach (ParameterSymbol p in value.Symbols.OfType()) + { + if (p.IsImplicit) + { + continue; + } + JsonSerializer.Serialize(writer, p, options); + //writer.WriteRaw(JsonConvert.SerializeObject(p, ParameterSymbolJsonConverter.Instance)); + } + writer.WriteEndObject(); + + if (value.Symbols.Any(s => s is not ParameterSymbol && !s.IsImplicit)) + { + throw new NotSupportedException("Symbols are not supported for serialization to JSON."); + } + } + if (value.PrimaryOutputs.Any()) + { + throw new NotSupportedException("Primary outputs are not supported for serialization to JSON."); + } + if (value.GlobalCustomOperations != null) + { + throw new NotSupportedException("Global custom operations are not supported for serialization to JSON."); + } + if (value.SpecialCustomOperations.Any()) + { + throw new NotSupportedException("Special custom operations are not supported for serialization to JSON."); + } + if (value.Constraints.Any()) + { + throw new NotSupportedException("Constraints are not supported for serialization to JSON."); + } + + writer.WriteEndObject(); + } + + internal static TemplateConfigModelJsonConverter Instance { get; } = new TemplateConfigModelJsonConverter(); + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SnapshotTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SnapshotTests.cs new file mode 100644 index 000000000000..002150d8ab5a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/SnapshotTests.cs @@ -0,0 +1,309 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET + +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Authoring.TemplateApiVerifier; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Xunit.Abstractions; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests +{ + [Collection("Verify Tests")] + public class SnapshotTests : TestBase + { + private readonly ILogger _log; + + public SnapshotTests(ITestOutputHelper log) + { + _log = new XunitLoggerProvider(log).CreateLogger("TestRun"); + } + + [Fact] + public Task TestGeneratedSymbolWithRefToDerivedSymbol() + { + string templateLocation = GetTestTemplateLocation("TemplateWithGeneratedSymbolWithRefToDerivedSymbol"); + var templateParams = new Dictionary() + { + { "NugetToolName", "nuget" } + }; + string workingDir = TestUtils.CreateTemporaryFolder(); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateGenSymWithRefToDerivedSym") + { + TemplatePath = templateLocation, + OutputDirectory = workingDir, + DoNotAppendTemplateArgsToScenarioName = true, + DoNotPrependTemplateNameToScenarioName = true, + SnapshotsDirectory = "Approvals" + } + .WithInstantiationThroughTemplateCreatorApi(templateParams); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TestGeneratedSymbolWithRefToDerivedSymbol_DifferentOrder() + { + string templateLocation = GetTestTemplateLocation("TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder"); + var templateParams = new Dictionary() + { + { "NugetToolName", "nuget" } + }; + string workingDir = TestUtils.CreateTemporaryFolder(); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder") + { + TemplatePath = templateLocation, + OutputDirectory = workingDir, + DoNotAppendTemplateArgsToScenarioName = true, + DoNotPrependTemplateNameToScenarioName = true, + SnapshotsDirectory = "Approvals" + } + .WithInstantiationThroughTemplateCreatorApi(templateParams); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TestCoalesce_EmptyStringForMultiChoices() + { + string templateLocation = GetTestTemplateLocation("TemplateWithMultipleChoicesAndCoalesce"); + var templateParams = new Dictionary() + { + { "tests", string.Empty } + }; + string workingDir = TestUtils.CreateTemporaryFolder(); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithMultipleChoicesAndCoalesce") + { + TemplatePath = templateLocation, + OutputDirectory = workingDir, + DoNotAppendTemplateArgsToScenarioName = true, + DoNotPrependTemplateNameToScenarioName = true, + SnapshotsDirectory = "Approvals" + } + .WithInstantiationThroughTemplateCreatorApi(templateParams); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TestSingleSelectionForMultiChoices() + { + string templateLocation = GetTestTemplateLocation("TemplateWithMultipleChoicesAndCoalesce"); + var templateParams = new Dictionary() + { + { "tests", "unit" } + }; + string workingDir = TestUtils.CreateTemporaryFolder(); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithMultipleChoicesAndCoalesce") + { + TemplatePath = templateLocation, + OutputDirectory = workingDir, + DoNotAppendTemplateArgsToScenarioName = true, + DoNotPrependTemplateNameToScenarioName = true, + SnapshotsDirectory = "Approvals" + } + .WithInstantiationThroughTemplateCreatorApi(templateParams); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TestTemplateWithBrokenGeneratedInComputed() + { + string templateLocation = GetTestTemplateLocation("TemplateWithBrokenGeneratedInComputed"); + var templateParams = new Dictionary() + { + { "navigation", "regions" } + }; + string workingDir = TestUtils.CreateTemporaryFolder(); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithBrokenGeneratedInComputed") + { + TemplatePath = templateLocation, + OutputDirectory = workingDir, + DoNotAppendTemplateArgsToScenarioName = true, + DoNotPrependTemplateNameToScenarioName = true, + VerifyCommandOutput = true, + SnapshotsDirectory = "Approvals" + } + .WithInstantiationThroughTemplateCreatorApi(templateParams); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TestTemplateWithComputedInGenerated() + { + string templateLocation = GetTestTemplateLocation("TemplateWithComputedInGenerated"); + string workingDir = TestUtils.CreateTemporaryFolder(); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithComputedInGenerated") + { + TemplatePath = templateLocation, + OutputDirectory = workingDir, + DoNotAppendTemplateArgsToScenarioName = true, + DoNotPrependTemplateNameToScenarioName = true, + VerifyCommandOutput = true, + SnapshotsDirectory = "Approvals" + } + .WithInstantiationThroughTemplateCreatorApi( + new Dictionary() + { + { "preset", "recommended" } + }); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TestTemplateWithComputedInDerivedThroughGenerated() + { + string templateLocation = GetTestTemplateLocation("TemplateWithComputedInDerivedThroughGenerated"); + string workingDir = TestUtils.CreateTemporaryFolder(); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithComputedInDerivedThroughGenerated") + { + TemplatePath = templateLocation, + OutputDirectory = workingDir, + DoNotAppendTemplateArgsToScenarioName = true, + DoNotPrependTemplateNameToScenarioName = true, + VerifyCommandOutput = true, + SnapshotsDirectory = "Approvals" + } + .WithInstantiationThroughTemplateCreatorApi(new Dictionary()); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TestTemplateWithGeneratedInComputed() + { + string templateLocation = GetTestTemplateLocation("TemplateWithGeneratedInComputed"); + string workingDir = TestUtils.CreateTemporaryFolder(); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithGeneratedInComputed") + { + TemplatePath = templateLocation, + OutputDirectory = workingDir, + DoNotAppendTemplateArgsToScenarioName = true, + DoNotPrependTemplateNameToScenarioName = true, + VerifyCommandOutput = true, + SnapshotsDirectory = "Approvals" + } + .WithInstantiationThroughTemplateCreatorApi( + new Dictionary() + { + { "navigation", "regions" }, + { "dependencyInjection", "true" } + }); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TestTemplateWithGeneratedSwitchInComputed() + { + string templateLocation = GetTestTemplateLocation("TemplateWithGeneratedSwitchInComputed"); + string workingDir = TestUtils.CreateTemporaryFolder(); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithGeneratedSwitchInComputed") + { + TemplatePath = templateLocation, + OutputDirectory = workingDir, + DoNotAppendTemplateArgsToScenarioName = true, + DoNotPrependTemplateNameToScenarioName = true, + VerifyCommandOutput = true, + SnapshotsDirectory = "Approvals" + } + .WithInstantiationThroughTemplateCreatorApi( + new Dictionary() + { + { "navigation", "regions" }, + { "dependencyInjection", "true" } + }); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TestTemplateWithCircleDependencyInMacros() + { + string templateLocation = GetTestTemplateLocation("TemplateWithCircleDependencyInMacros"); + + string workingDir = TestUtils.CreateTemporaryFolder(); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithCircleDependencyInMacros") + { + TemplatePath = templateLocation, + OutputDirectory = workingDir, + DoNotAppendTemplateArgsToScenarioName = true, + DoNotPrependTemplateNameToScenarioName = true, + VerifyCommandOutput = true, + IsCommandExpectedToFail = true, + SnapshotsDirectory = "Approvals" + } + .WithInstantiationThroughTemplateCreatorApi(new Dictionary()); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + + [Fact] + public Task TestSelectionForMultiChoicesWhenThereAreMultiplePartialMatchesAndOnePreciseMatch() + { + string templateLocation = GetTestTemplateLocation("TemplateWithMultipleChoicesAndPartialMatches"); + var templateParams = new Dictionary() + { + // There are multiple choices for the parameter that overlap: "aab", "aac", "aa". + // We want to ensure that "aa" can be selected, because it is a precise match, + // even if there are more than one choices that start with "aa", and even if "aa", + // is not listed first in the list of choices. + { "tests", "aa" } + }; + string workingDir = TestUtils.CreateTemporaryFolder(); + + TemplateVerifierOptions options = + new TemplateVerifierOptions(templateName: "TestAssets.TemplateWithMultipleChoicesAndPartialMatches") + { + TemplatePath = templateLocation, + OutputDirectory = workingDir, + DoNotAppendTemplateArgsToScenarioName = true, + DoNotPrependTemplateNameToScenarioName = true, + SnapshotsDirectory = "Approvals" + } + .WithInstantiationThroughTemplateCreatorApi(templateParams); + + VerificationEngine engine = new VerificationEngine(_log); + return engine.Execute(options); + } + } +} + +#endif + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/TemplateConfigTests/ConstraintsTest.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/TemplateConfigTests/ConstraintsTest.cs new file mode 100644 index 000000000000..955d1f9b01fc --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/TemplateConfigTests/ConstraintsTest.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel; +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.TemplateConfigTests +{ + public class ConstraintsTest + { + [Fact] + public void CanReadConstraintDefinition() + { + var json = new + { + identity = "test", + constraints = new + { + one = new + { + type = "con1", + args = "arg" + }, + two = new + { + type = "con2", + args = new[] + { + "one", "two", "three" + } + }, + three = new + { + type = "con3", + args = new + { + one = "one", + two = "two", + } + }, + four = new + { + type = "con4" + }, + } + }; + + var model = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(json))!.AsObject()); + + Assert.Equal(4, model.Constraints.Count); + Assert.Equal("con1", model.Constraints[0].Type); + Assert.Equal("con2", model.Constraints[1].Type); + Assert.Equal("con3", model.Constraints[2].Type); + Assert.Equal("con4", model.Constraints[3].Type); + + Assert.Equal("\"arg\"", model.Constraints[0].Args); + Assert.Equal("""["one","two","three"]""", model.Constraints[1].Args); + Assert.Equal(/*lang=json,strict*/ """{"one":"one","two":"two"}""", model.Constraints[2].Args); + Assert.Null(model.Constraints[3].Args); + } + + [Fact] + public void CannotReadConstraint_WhenTypeIsNotSet() + { + var json = new + { + identity = "test", + constraints = new + { + one = new + { + args = "arg" + } + } + }; + + List<(LogLevel, string)> loggedMessages = new List<(LogLevel, string)>(); + InMemoryLoggerProvider loggerProvider = new InMemoryLoggerProvider(loggedMessages); + var model = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(json))!.AsObject(), loggerProvider.CreateLogger("test")); + Assert.Empty(model.Constraints); + Assert.Single(loggedMessages); + Assert.Equal($"Constraint definition '{JsonNode.Parse(JsonSerializer.Serialize(new { args = "arg" }))!.ToJsonString()}' does not contain mandatory property 'type'.", loggedMessages.Single().Item2); + } + + [Fact] + public void CannotReadConstraint_WhenArrayIsDenfined() + { + var json = new + { + identity = "test", + constraints = new + { + one = new[] { "one", "two" } + } + }; + + List<(LogLevel, string)> loggedMessages = new List<(LogLevel, string)>(); + InMemoryLoggerProvider loggerProvider = new InMemoryLoggerProvider(loggedMessages); + var model = TemplateConfigModel.FromJObject(JsonNode.Parse(JsonSerializer.Serialize(json))!.AsObject(), loggerProvider.CreateLogger("test")); + Assert.Empty(model.Constraints); + Assert.Single(loggedMessages); + Assert.Equal("'constraints' should contain objects.", loggedMessages.Single().Item2); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/TemplateConfigTests/CustomConditionalTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/TemplateConfigTests/CustomConditionalTests.cs new file mode 100644 index 000000000000..7aca9a3a587f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/TemplateConfigTests/CustomConditionalTests.cs @@ -0,0 +1,217 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using FakeItEasy; +using Microsoft.TemplateEngine.Abstractions.Mount; +using Microsoft.TemplateEngine.Core.Contracts; +using Microsoft.TemplateEngine.Core.Operations; +using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.OperationConfig; + +namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests.TemplateConfigTests +{ + public class CustomConditionalTests + { + // defines a template configuration with a custom conditional configuration. + // The style is omitted, which implies custon + private static JsonObject CustomConditionalSetupNoStyleSpecification + { + get + { + string configString = /*lang=json*/ """ + { + "actionableIf": [ " + + $(NetMinimum);$(NetFrameworkMinimum) + true + false + true + true + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/Microsoft.TemplateEngine.TestHelper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/Microsoft.TemplateEngine.TestHelper.csproj new file mode 100644 index 000000000000..ee0310b7d8b4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/Microsoft.TemplateEngine.TestHelper.csproj @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/MonitoredFileSystem.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/MonitoredFileSystem.cs new file mode 100644 index 000000000000..c9628a1dc57f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/MonitoredFileSystem.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; + +namespace Microsoft.TemplateEngine.TestHelper +{ + public class MonitoredFileSystem : IPhysicalFileSystem + { + private readonly IPhysicalFileSystem _baseFileSystem; + private ConcurrentBag _directoriesScanned = new(); + private ConcurrentBag _filesOpened = new(); + private readonly ConcurrentBag _filesWatched = new(); + + public MonitoredFileSystem(IPhysicalFileSystem baseFileSystem) + { + _baseFileSystem = baseFileSystem; + } + + public IReadOnlyList DirectoriesScanned => _directoriesScanned.ToArray(); + + public IEnumerable FilesOpened => _filesOpened; + + public IEnumerable FilesWatched => _filesWatched; + + public void CreateDirectory(string path) => _baseFileSystem.CreateDirectory(path); + + public Stream CreateFile(string path) => _baseFileSystem.CreateFile(path); + + public void DirectoryDelete(string path, bool recursive) => _baseFileSystem.DirectoryDelete(path, recursive); + + public bool DirectoryExists(string directory) => _baseFileSystem.DirectoryExists(directory); + + public IEnumerable EnumerateDirectories(string path, string pattern, SearchOption searchOption) => _baseFileSystem.EnumerateDirectories(path, pattern, searchOption); + + public IEnumerable EnumerateFiles(string path, string pattern, SearchOption searchOption) => _baseFileSystem.EnumerateFiles(path, pattern, searchOption); + + public IEnumerable EnumerateFileSystemEntries(string directoryName, string pattern, SearchOption searchOption) + { + RecordDirectoryScan(directoryName, pattern, searchOption); + return _baseFileSystem.EnumerateFileSystemEntries(directoryName, pattern, searchOption); + } + + public void Reset() + { + _directoriesScanned = new(); + _filesOpened = new(); + } + + public void FileCopy(string sourcePath, string targetPath, bool overwrite) => _baseFileSystem.FileCopy(sourcePath, targetPath, overwrite); + + public void FileDelete(string path) => _baseFileSystem.FileDelete(path); + + public bool FileExists(string file) => _baseFileSystem.FileExists(file); + + public string GetCurrentDirectory() => _baseFileSystem.GetCurrentDirectory(); + + public FileAttributes GetFileAttributes(string file) => _baseFileSystem.GetFileAttributes(file); + + public Stream OpenRead(string path) + { + _filesOpened.Add(path); + return _baseFileSystem.OpenRead(path); + } + + public string ReadAllText(string path) => _baseFileSystem.ReadAllText(path); + + public byte[] ReadAllBytes(string path) => _baseFileSystem.ReadAllBytes(path); + + public void SetFileAttributes(string file, FileAttributes attributes) => _baseFileSystem.SetFileAttributes(file, attributes); + + public void WriteAllText(string path, string value) => _baseFileSystem.WriteAllText(path, value); + + public IDisposable WatchFileChanges(string filepath, FileSystemEventHandler fileChanged) + { + _filesWatched.Add(filepath); + return _baseFileSystem.WatchFileChanges(filepath, fileChanged); + } + + public DateTime GetLastWriteTimeUtc(string file) => _baseFileSystem.GetLastWriteTimeUtc(file); + + public void SetLastWriteTimeUtc(string file, DateTime lastWriteTimeUtc) => _baseFileSystem.SetLastWriteTimeUtc(file, lastWriteTimeUtc); + + public string PathRelativeTo(string target, string relativeTo) => _baseFileSystem.PathRelativeTo(target, relativeTo); + + private void RecordDirectoryScan(string directoryName, string pattern, SearchOption searchOption) + { + _directoriesScanned.Add(new DirectoryScanParameters(directoryName, pattern, searchOption)); + } + + public class DirectoryScanParameters + { + public DirectoryScanParameters(string directoryName, string pattern, SearchOption searchOption) + { + DirectoryName = directoryName; + Pattern = pattern; + SearchOption = searchOption; + } + + public string DirectoryName { get; } + + public string Pattern { get; } + + public SearchOption SearchOption { get; } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/PackageManager.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/PackageManager.cs new file mode 100644 index 000000000000..59aa359a301f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/PackageManager.cs @@ -0,0 +1,439 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Diagnostics; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace Microsoft.TemplateEngine.TestHelper +{ + public class PackageManager : IDisposable + { + private const string NuGetOrgFeed = "https://api.nuget.org/v3/index.json"; + private static readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1); + private readonly string _packageLocation = TestUtils.CreateTemporaryFolder("packages"); + private readonly ConcurrentDictionary _installedPackages = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private static readonly Mutex PackMutex = new Mutex(false, "TemplateEngineTestPackMutex"); + + public async Task GetNuGetPackage(string templatePackName, string? version = null, NuGetVersion? minimumVersion = null, ILogger? logger = null, string? downloadDirectory = null) + { + logger ??= NullLogger.Instance; + string downloadDir = downloadDirectory ?? _packageLocation; + NuGetHelper nuGetHelper = new NuGetHelper(downloadDir, logger); + try + { + logger.LogDebug($"[NuGet Package Manager] Trying to get semaphore."); + await Semaphore.WaitAsync(); + logger.LogDebug($"[NuGet Package Manager] Semaphore acquired."); + if (_installedPackages.TryGetValue(templatePackName, out string? packagePath)) + { + return packagePath; + } + + for (int retry = 0; retry < 5; retry++) + { + try + { + logger.LogDebug($"[NuGet Package Manager][attempt: {retry + 1}] Downloading package {templatePackName}, minimum version: {minimumVersion?.ToNormalizedString()}"); + string downloadedPackage = await nuGetHelper.DownloadPackageAsync( + templatePackName, + version: version, + additionalSources: new[] { NuGetOrgFeed }, + minimumVersion: minimumVersion); + _installedPackages[templatePackName] = downloadedPackage; + logger.LogDebug($"[NuGet Package Manager][attempt: {retry + 1}] The package {templatePackName} was successfully downloaded."); + return downloadedPackage; + } + catch (Exception ex) + { + logger.LogError($"[NuGet Package Manager] Download failed: package {templatePackName}, details: {ex}"); + //retry failed download + } + logger.LogDebug($"[NuGet Package Manager][attempt: {retry + 1}] Will wait for 1 sec before re-attempt."); + await Task.Delay(1000); + logger.LogDebug($"[NuGet Package Manager][attempt: {retry + 1}] Waiting over."); + } + logger.LogError($"[NuGet Package Manager] Failed to download {templatePackName} after 5 retries."); + throw new Exception($"Failed to download {templatePackName} after 5 retries"); + } + finally + { + logger.LogDebug($"[NuGet Package Manager] Releasing semaphore."); + Semaphore.Release(); + logger.LogDebug($"[NuGet Package Manager] Semaphore released."); + } + } + + public string PackNuGetPackage(string projectPath, ILogger? logger = null) + { + if (string.IsNullOrWhiteSpace(projectPath)) + { + throw new ArgumentException("projectPath cannot be null", nameof(projectPath)); + } + string absolutePath = Path.GetFullPath(projectPath); + if (!File.Exists(projectPath)) + { + throw new ArgumentException($"{projectPath} doesn't exist", nameof(projectPath)); + } + + string? packagePath = null; + logger ??= NullLogger.Instance; + try + { + try + { + PackMutex.WaitOne(); + } + catch (AbandonedMutexException ex) + { + logger.LogDebug($"AbandonedMutexException on return from WaitOne. Message: {ex.Message}"); + } + + var isFound = _installedPackages.TryGetValue(absolutePath, out packagePath); + if (!isFound) + { + var info = new ProcessStartInfo("dotnet", $"pack {absolutePath} -o {_packageLocation}") + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + Process p = Process.Start(info) ?? throw new Exception("Failed to start dotnet process."); + p.WaitForExit(); + if (p.ExitCode != 0) + { + string? stdOut = null; + string? stdErr = null; + try + { + stdOut = p.StandardOutput.ReadToEnd(); + stdErr = p.StandardError.ReadToEnd(); + } + catch + { + //do nothing in case streams cannot be read + } + + throw new Exception($"Failed to pack the project {projectPath}: " + + $"{Environment.NewLine}StdOut: {stdOut}." + + $"{Environment.NewLine}StdErr: {stdErr}." + + $"{Environment.NewLine}Exit Code: {p.ExitCode}."); + } + + packagePath = Directory.GetFiles(_packageLocation).Aggregate( + (latest, current) => (latest == null) ? current : File.GetCreationTimeUtc(current) > File.GetCreationTimeUtc(latest) ? current : latest); + _installedPackages[absolutePath] = packagePath; + } + } + finally + { + PackMutex.ReleaseMutex(); + } + return packagePath!; + } + + public void Dispose() => Directory.Delete(_packageLocation, true); + + private class NuGetHelper + { + private readonly string _packageLocation; + private readonly ILogger _nugetLogger; + private readonly SourceCacheContext _cacheSettings = new SourceCacheContext() + { + NoCache = true, + DirectDownload = true + }; + + internal NuGetHelper(string packageLocation, ILogger? logger = null) + { + _packageLocation = packageLocation; + _nugetLogger = logger ?? NullLogger.Instance; + } + + internal async Task DownloadPackageAsync( + string identifier, + string? version = null, + IEnumerable? additionalSources = null, + NuGetVersion? minimumVersion = null, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(identifier)) + { + throw new ArgumentException($"{nameof(identifier)} cannot be null or empty", nameof(identifier)); + } + + IEnumerable packagesSources = LoadNuGetSources(additionalSources?.ToArray() ?? []); + + NuGetVersion packageVersion; + PackageSource source; + IPackageSearchMetadata packageMetadata; + + if (string.IsNullOrWhiteSpace(version)) + { + _nugetLogger.LogDebug($"[NuGet Package Manager] Getting latest version for the package {identifier}."); + (source, packageMetadata) = await GetLatestVersionInternalAsync(identifier, packagesSources, cancellationToken); + if (minimumVersion != null && packageMetadata.Identity.Version < minimumVersion) + { + _nugetLogger.LogError($"[NuGet Package Manager] Failed to find the package with version {minimumVersion} or later."); + throw new Exception($"Failed to find the package with version {minimumVersion} or later"); + } + } + else + { + _nugetLogger.LogDebug($"[NuGet Package Manager] Getting package metadata {identifier}@{version}."); + packageVersion = new NuGetVersion(version!); + (source, packageMetadata) = await GetPackageMetadataAsync(identifier, packageVersion, packagesSources, cancellationToken); + } + + _nugetLogger.LogDebug($"[NuGet Package Manager] Getting repository for source {source.Source}."); + FindPackageByIdResource resource; + SourceRepository repository = Repository.Factory.GetCoreV3(source); + try + { + resource = await repository.GetResourceAsync(cancellationToken); + } + catch (Exception e) + { + _nugetLogger.LogError($"[NuGet Package Manager] Failed to load NuGet source {source.Source}, details: {e}."); + throw new Exception($"Failed to load NuGet source {source.Source}", e); + } + _nugetLogger.LogDebug($"[NuGet Package Manager] Repository for source {source.Source} is loaded."); + + string filePath = Path.Combine(_packageLocation, packageMetadata.Identity.Id + "." + packageMetadata.Identity.Version + ".nupkg"); + if (File.Exists(filePath)) + { + _nugetLogger.LogError($"[NuGet Package Manager] {filePath} already exists."); + throw new Exception($"{filePath} already exists"); + } + try + { + _nugetLogger.LogDebug($"[NuGet Package Manager] Started download to {filePath}."); + using Stream packageStream = File.Create(filePath); + if (await resource.CopyNupkgToStreamAsync( + packageMetadata.Identity.Id, + packageMetadata.Identity.Version, + packageStream, + _cacheSettings, + _nugetLogger, + cancellationToken)) + { + _nugetLogger.LogDebug($"[NuGet Package Manager] Download finished successfully."); + return filePath; + } + else + { + _nugetLogger.LogError($"[NuGet Package Manager] Failed to download {packageMetadata.Identity.Id}, version: {packageMetadata.Identity.Version} from {source.Source}"); + throw new Exception($"Failed to download {packageMetadata.Identity.Id}, version: {packageMetadata.Identity.Version} from {source.Source}"); + } + } + catch (Exception e) + { + _nugetLogger.LogError($"[NuGet Package Manager] Failed to download {packageMetadata.Identity.Id}, version: {packageMetadata.Identity.Version} from {source.Source}, details: {e}."); + throw new Exception($"Failed to download {packageMetadata.Identity.Id}, version: {packageMetadata.Identity.Version} from {source.Source}", e); + } + } + + private async Task<(PackageSource, IPackageSearchMetadata)> GetLatestVersionInternalAsync( + string packageIdentifier, + IEnumerable packageSources, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(packageIdentifier)) + { + throw new ArgumentException($"{nameof(packageIdentifier)} cannot be null or empty", nameof(packageIdentifier)); + } + _ = packageSources ?? throw new ArgumentNullException(nameof(packageSources)); + + _nugetLogger.LogDebug($"[NuGet Package Manager] Checking package metadata in all sources, package: {packageIdentifier}."); + + (PackageSource Source, IEnumerable? FoundPackages)[] foundPackagesBySource = + await Task.WhenAll( + packageSources.Select(source => + Task.Run(() => GetPackageMetadataAsync(source, packageIdentifier, includePrerelease: true, cancellationToken)))); + + if (!foundPackagesBySource.Any(result => result.FoundPackages != null)) + { + _nugetLogger.LogError($"[NuGet Package Manager] Failed to load NuGet sources {string.Join(";", packageSources.Select(source => source.Source))}."); + throw new Exception($"Failed to load NuGet sources {string.Join(";", packageSources.Select(source => source.Source))}"); + } + + var accumulativeSearchResults = foundPackagesBySource + .Where(r => r.FoundPackages != null) + .SelectMany(result => result.FoundPackages!.Select(package => (result.Source, package))); + + _nugetLogger.LogDebug($"[NuGet Package Manager] Found {accumulativeSearchResults.Count()} matches."); + + if (!accumulativeSearchResults.Any()) + { + _nugetLogger.LogError($"[NuGet Package Manager] {packageIdentifier} is not found in {string.Join(";", packageSources.Select(source => source.Source))}."); + throw new Exception($"{packageIdentifier} is not found in {string.Join(";", packageSources.Select(source => source.Source))}"); + } + + (PackageSource, IPackageSearchMetadata) latestVersion = accumulativeSearchResults.Aggregate( + (max, current) => + { + if (max == default) + { + return current; + } + + return current.package.Identity.Version > max.package.Identity.Version ? current : max; + }); + _nugetLogger.LogDebug($"[NuGet Package Manager] Latest version is {latestVersion.Item2.Identity.Id}@{latestVersion.Item2.Identity.Version}, source: {latestVersion.Item1.Source}."); + return latestVersion; + } + + private async Task<(PackageSource, IPackageSearchMetadata)> GetPackageMetadataAsync( + string packageIdentifier, + NuGetVersion packageVersion, + IEnumerable sources, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(packageIdentifier)) + { + throw new ArgumentException($"{nameof(packageIdentifier)} cannot be null or empty", nameof(packageIdentifier)); + } + _ = packageVersion ?? throw new ArgumentNullException(nameof(packageVersion)); + _ = sources ?? throw new ArgumentNullException(nameof(sources)); + + bool atLeastOneSourceValid = false; + _nugetLogger.LogDebug($"[NuGet Package Manager] Loading package {packageIdentifier} metadata from all sources."); + using CancellationTokenSource linkedCts = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var tasks = sources.Select(source => + Task.Run(() => GetPackageMetadataAsync(source, packageIdentifier, includePrerelease: true, linkedCts.Token))).ToList(); + while (tasks.Any()) + { + var finishedTask = await Task.WhenAny(tasks); + tasks.Remove(finishedTask); + (PackageSource foundSource, IEnumerable? foundPackages) = await finishedTask; + _nugetLogger.LogDebug($"[NuGet Package Manager] Processed source {foundSource.Source}, found {foundPackages?.Count()} packages."); + if (foundPackages == null) + { + continue; + } + atLeastOneSourceValid = true; + IPackageSearchMetadata? matchedVersion = foundPackages.FirstOrDefault(package => package.Identity.Version == packageVersion); + if (matchedVersion != null) + { + _nugetLogger.LogDebug($"[NuGet Package Manager] Processed source {foundSource.Source}, found {matchedVersion.Identity.Id}@{matchedVersion.Identity.Version} package, cancelling other tasks."); + linkedCts.Cancel(); + return (foundSource, matchedVersion); + } + _nugetLogger.LogDebug($"[NuGet Package Manager] Processed source {foundSource.Source}, no package with version {packageVersion} found."); + } + if (!atLeastOneSourceValid) + { + _nugetLogger.LogError($"[NuGet Package Manager] Failed to load NuGet sources {string.Join(";", sources.Select(source => source.Source))}."); + throw new Exception($"Failed to load NuGet sources {string.Join(";", sources.Select(source => source.Source))}"); + } + + _nugetLogger.LogError($"[NuGet Package Manager] {packageIdentifier}, version: {packageVersion} is not found in {string.Join(";", sources.Select(source => source.Source))}."); + throw new Exception($"{packageIdentifier}, version: {packageVersion} is not found in {string.Join(";", sources.Select(source => source.Source))}"); + } + + private async Task<(PackageSource Source, IEnumerable? FoundPackages)> GetPackageMetadataAsync( + PackageSource source, + string packageIdentifier, + bool includePrerelease = false, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(packageIdentifier)) + { + throw new ArgumentException($"{nameof(packageIdentifier)} cannot be null or empty", nameof(packageIdentifier)); + } + _ = source ?? throw new ArgumentNullException(nameof(source)); + + try + { + _nugetLogger.LogDebug($"[NuGet Package Manager] Getting metadata for package {packageIdentifier} from source {source.Source}."); + SourceRepository repository = Repository.Factory.GetCoreV3(source); + PackageMetadataResource resource = await repository.GetResourceAsync(cancellationToken); + IEnumerable foundPackages = await resource.GetMetadataAsync( + packageIdentifier, + includePrerelease: includePrerelease, + includeUnlisted: false, + _cacheSettings, + _nugetLogger, + cancellationToken); + _nugetLogger.LogDebug($"[NuGet Package Manager] Found {foundPackages.Count()} packages in source {source.Source}."); + return (source, foundPackages); + } + catch (Exception ex) + { + //ignore errors + _nugetLogger.LogWarning($"Retrieving info from {source.Source} failed, details: {ex}."); + return (source, null); + } + } + + private IEnumerable LoadNuGetSources(params string[] additionalSources) + { + IEnumerable defaultSources; + string currentDirectory = string.Empty; + try + { + _nugetLogger.LogDebug($"[NuGet Package Manager] loading default sources"); + currentDirectory = Directory.GetCurrentDirectory(); + ISettings settings = global::NuGet.Configuration.Settings.LoadDefaultSettings(currentDirectory); + PackageSourceProvider packageSourceProvider = new PackageSourceProvider(settings); + defaultSources = packageSourceProvider.LoadPackageSources().Where(source => source.IsEnabled); + foreach (var source in defaultSources) + { + _nugetLogger.LogDebug($"[NuGet Package Manager] Loaded source: {source.Source}"); + } + } + catch (Exception ex) + { + _nugetLogger.LogError($"[NuGet Package Manager] Failed to load NuGet sources configured for the folder {currentDirectory}, details: {ex}."); + throw new Exception($"Failed to load NuGet sources configured for the folder {currentDirectory}", ex); + } + + if (!additionalSources.Any()) + { + if (!defaultSources.Any()) + { + _nugetLogger.LogError($"[NuGet Package Manager] No NuGet sources are defined or enabled."); + throw new Exception("No NuGet sources are defined or enabled"); + } + return defaultSources; + } + + List customSources = new List(); + _nugetLogger.LogDebug($"[NuGet Package Manager] loading additional sources"); + foreach (string source in additionalSources) + { + _nugetLogger.LogDebug($"[NuGet Package Manager] loading source {source}."); + if (string.IsNullOrWhiteSpace(source)) + { + continue; + } + if (defaultSources.Any(s => s.Source.Equals(source, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + PackageSource packageSource = new PackageSource(source); + if (packageSource.TrySourceAsUri == null) + { + _nugetLogger.LogDebug($"[NuGet Package Manager] {source} is not a valid source."); + continue; + } + customSources.Add(packageSource); + _nugetLogger.LogDebug($"[NuGet Package Manager] Loaded source: {packageSource.Source}"); + } + + IEnumerable retrievedSources = customSources.Concat(defaultSources); + if (!retrievedSources.Any()) + { + throw new Exception("No NuGet sources are defined or enabled"); + } + return retrievedSources; + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/PublicAPI.Shipped.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..91b0e1a43b98 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/PublicAPI.Unshipped.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..3d8fd672fb5f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/PublicAPI.Unshipped.txt @@ -0,0 +1,103 @@ +Microsoft.TemplateEngine.TestHelper.AssemblyComponentCatalog +Microsoft.TemplateEngine.TestHelper.AssemblyComponentCatalog.AssemblyComponentCatalog(System.Collections.Generic.IReadOnlyList! assemblies) -> void +Microsoft.TemplateEngine.TestHelper.AssemblyComponentCatalog.Count.get -> int +Microsoft.TemplateEngine.TestHelper.AssemblyComponentCatalog.GetEnumerator() -> System.Collections.Generic.IEnumerator<(System.Type!, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent!)>! +Microsoft.TemplateEngine.TestHelper.AssemblyComponentCatalog.this[int index].get -> (System.Type!, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent!) +Microsoft.TemplateEngine.TestHelper.BuiltInTemplatePackagesProviderFactory +Microsoft.TemplateEngine.TestHelper.BuiltInTemplatePackagesProviderFactory.BuiltInTemplatePackagesProviderFactory(params string![]! pathsToProbe) -> void +Microsoft.TemplateEngine.TestHelper.BuiltInTemplatePackagesProviderFactory.CreateProvider(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! settings) -> Microsoft.TemplateEngine.Abstractions.TemplatePackage.ITemplatePackageProvider! +Microsoft.TemplateEngine.TestHelper.BuiltInTemplatePackagesProviderFactory.DisplayName.get -> string! +Microsoft.TemplateEngine.TestHelper.BuiltInTemplatePackagesProviderFactory.Id.get -> System.Guid +Microsoft.TemplateEngine.TestHelper.EnvironmentSettingsHelper +Microsoft.TemplateEngine.TestHelper.EnvironmentSettingsHelper.CreateEnvironment(string? locale = null, bool virtualize = false, string! hostIdentifier = "", bool loadDefaultGenerator = true, Microsoft.TemplateEngine.Abstractions.IEnvironment? environment = null, System.Collections.Generic.IReadOnlyList<(System.Type!, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent!)>? additionalComponents = null, System.Collections.Generic.IEnumerable? addLoggerProviders = null) -> Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! +Microsoft.TemplateEngine.TestHelper.EnvironmentSettingsHelper.CreateTemporaryFolder(string! name = "") -> string! +Microsoft.TemplateEngine.TestHelper.EnvironmentSettingsHelper.Dispose() -> void +Microsoft.TemplateEngine.TestHelper.EnvironmentSettingsHelper.EnvironmentSettingsHelper(Xunit.Abstractions.IMessageSink! messageSink) -> void +Microsoft.TemplateEngine.TestHelper.InMemoryLoggerProvider +Microsoft.TemplateEngine.TestHelper.InMemoryLoggerProvider.CreateLogger(string! categoryName) -> Microsoft.Extensions.Logging.ILogger! +Microsoft.TemplateEngine.TestHelper.InMemoryLoggerProvider.Dispose() -> void +Microsoft.TemplateEngine.TestHelper.InMemoryLoggerProvider.InMemoryLoggerProvider(System.Collections.Generic.List<(Microsoft.Extensions.Logging.LogLevel, string!)>! messagesCollection) -> void +Microsoft.TemplateEngine.TestHelper.LongRunningConstraintFactory +Microsoft.TemplateEngine.TestHelper.LongRunningConstraintFactory.CreateTemplateConstraintAsync(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.TestHelper.LongRunningConstraintFactory.Id.get -> System.Guid +Microsoft.TemplateEngine.TestHelper.LongRunningConstraintFactory.LongRunningConstraintFactory(string! type, int msDelay) -> void +Microsoft.TemplateEngine.TestHelper.LongRunningConstraintFactory.Type.get -> string! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.CreateDirectory(string! path) -> void +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.CreateFile(string! path) -> System.IO.Stream! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.DirectoriesScanned.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.DirectoryDelete(string! path, bool recursive) -> void +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.DirectoryExists(string! directory) -> bool +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.DirectoryScanParameters +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.DirectoryScanParameters.DirectoryName.get -> string! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.DirectoryScanParameters.DirectoryScanParameters(string! directoryName, string! pattern, System.IO.SearchOption searchOption) -> void +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.DirectoryScanParameters.Pattern.get -> string! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.DirectoryScanParameters.SearchOption.get -> System.IO.SearchOption +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.EnumerateDirectories(string! path, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.EnumerateFiles(string! path, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.EnumerateFileSystemEntries(string! directoryName, string! pattern, System.IO.SearchOption searchOption) -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.FileCopy(string! sourcePath, string! targetPath, bool overwrite) -> void +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.FileDelete(string! path) -> void +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.FileExists(string! file) -> bool +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.FilesOpened.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.FilesWatched.get -> System.Collections.Generic.IEnumerable! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.GetCurrentDirectory() -> string! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.GetFileAttributes(string! file) -> System.IO.FileAttributes +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.GetLastWriteTimeUtc(string! file) -> System.DateTime +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.MonitoredFileSystem(Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem.IPhysicalFileSystem! baseFileSystem) -> void +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.OpenRead(string! path) -> System.IO.Stream! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.PathRelativeTo(string! target, string! relativeTo) -> string! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.ReadAllBytes(string! path) -> byte[]! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.ReadAllText(string! path) -> string! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.Reset() -> void +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.SetFileAttributes(string! file, System.IO.FileAttributes attributes) -> void +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.SetLastWriteTimeUtc(string! file, System.DateTime lastWriteTimeUtc) -> void +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.WatchFileChanges(string! filepath, System.IO.FileSystemEventHandler! fileChanged) -> System.IDisposable! +Microsoft.TemplateEngine.TestHelper.MonitoredFileSystem.WriteAllText(string! path, string! value) -> void +Microsoft.TemplateEngine.TestHelper.PackageManager +Microsoft.TemplateEngine.TestHelper.PackageManager.Dispose() -> void +Microsoft.TemplateEngine.TestHelper.PackageManager.GetNuGetPackage(string! templatePackName, string? version = null, NuGet.Versioning.NuGetVersion? minimumVersion = null, NuGet.Common.ILogger? logger = null, string? downloadDirectory = null) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.TestHelper.PackageManager.PackageManager() -> void +Microsoft.TemplateEngine.TestHelper.PackageManager.PackNuGetPackage(string! projectPath, NuGet.Common.ILogger? logger = null) -> string! +Microsoft.TemplateEngine.TestHelper.SharedTestOutputHelper +Microsoft.TemplateEngine.TestHelper.SharedTestOutputHelper.SharedTestOutputHelper(Xunit.Abstractions.IMessageSink! sink) -> void +Microsoft.TemplateEngine.TestHelper.SharedTestOutputHelper.WriteLine(string! format, params object![]! args) -> void +Microsoft.TemplateEngine.TestHelper.SharedTestOutputHelper.WriteLine(string! message) -> void +Microsoft.TemplateEngine.TestHelper.StringExtensions +Microsoft.TemplateEngine.TestHelper.TestConstraintFactory +Microsoft.TemplateEngine.TestHelper.TestConstraintFactory.CreateTemplateConstraintAsync(Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.TemplateEngine.TestHelper.TestConstraintFactory.Id.get -> System.Guid +Microsoft.TemplateEngine.TestHelper.TestConstraintFactory.TestConstraintFactory(string! type) -> void +Microsoft.TemplateEngine.TestHelper.TestConstraintFactory.Type.get -> string! +Microsoft.TemplateEngine.TestHelper.TestFileSystemUtils +Microsoft.TemplateEngine.TestHelper.TestHost +Microsoft.TemplateEngine.TestHelper.TestHost.Dispose() -> void +Microsoft.TemplateEngine.TestHelper.TestHost.HostParamDefaults.get -> System.Collections.Generic.Dictionary! +Microsoft.TemplateEngine.TestHelper.TestHost.HostParamDefaults.set -> void +Microsoft.TemplateEngine.TestHelper.TestLoggerFactory +Microsoft.TemplateEngine.TestHelper.TestLoggerFactory.AddProvider(Microsoft.Extensions.Logging.ILoggerProvider! provider) -> void +Microsoft.TemplateEngine.TestHelper.TestLoggerFactory.CreateLogger() -> Microsoft.Extensions.Logging.ILogger! +Microsoft.TemplateEngine.TestHelper.TestLoggerFactory.CreateLogger(string! categoryName) -> Microsoft.Extensions.Logging.ILogger! +Microsoft.TemplateEngine.TestHelper.TestLoggerFactory.Dispose() -> void +Microsoft.TemplateEngine.TestHelper.TestLoggerFactory.TestLoggerFactory(Xunit.Abstractions.IMessageSink? messageSink = null) -> void +Microsoft.TemplateEngine.TestHelper.TestUtils +Microsoft.TemplateEngine.TestHelper.XunitLoggerProvider +Microsoft.TemplateEngine.TestHelper.XunitLoggerProvider.CreateLogger(string! categoryName) -> Microsoft.Extensions.Logging.ILogger! +Microsoft.TemplateEngine.TestHelper.XunitLoggerProvider.Dispose() -> void +Microsoft.TemplateEngine.TestHelper.XunitLoggerProvider.XunitLoggerProvider(Xunit.Abstractions.ITestOutputHelper! output) -> void +Microsoft.TemplateEngine.TestHelper.XunitLoggerProvider.XunitLoggerProvider(Xunit.Abstractions.ITestOutputHelper! output, Microsoft.Extensions.Logging.LogLevel minLevel) -> void +Microsoft.TemplateEngine.TestHelper.XunitLoggerProvider.XunitLoggerProvider(Xunit.Abstractions.ITestOutputHelper! output, Microsoft.Extensions.Logging.LogLevel minLevel, System.DateTimeOffset? logStart) -> void +static Microsoft.TemplateEngine.TestHelper.BuiltInTemplatePackagesProviderFactory.GetComponents(params string![]! pathsToProbe) -> System.Collections.Generic.List<(System.Type!, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent!)>! +static Microsoft.TemplateEngine.TestHelper.StringExtensions.UnixifyLineBreaks(this string! input) -> string! +static Microsoft.TemplateEngine.TestHelper.TestFileSystemUtils.GetTempVirtualizedPath(this Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings) -> string! +static Microsoft.TemplateEngine.TestHelper.TestFileSystemUtils.MountPath(this Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, string! sourceBasePath) -> Microsoft.TemplateEngine.Abstractions.Mount.IMountPoint! +static Microsoft.TemplateEngine.TestHelper.TestFileSystemUtils.WriteFile(this Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, string! filePath, string? fileContent) -> void +static Microsoft.TemplateEngine.TestHelper.TestFileSystemUtils.WriteTemplateSource(this Microsoft.TemplateEngine.Abstractions.IEngineEnvironmentSettings! environmentSettings, string! sourceBasePath, System.Collections.Generic.IDictionary! templateSourceFileNamesWithContent) -> void +static Microsoft.TemplateEngine.TestHelper.TestHost.GetVirtualHost(string! hostIdentifier = "", Microsoft.TemplateEngine.Abstractions.IEnvironment? environment = null, System.Collections.Generic.IReadOnlyList<(System.Type!, Microsoft.TemplateEngine.Abstractions.IIdentifiedComponent!)>? additionalComponents = null, System.Collections.Generic.IReadOnlyDictionary? defaultParameters = null) -> Microsoft.TemplateEngine.Abstractions.ITemplateEngineHost! +static Microsoft.TemplateEngine.TestHelper.TestUtils.AttemptSearch(int count, System.TimeSpan interval, System.Func!>! execute) -> System.Threading.Tasks.Task! +static Microsoft.TemplateEngine.TestHelper.TestUtils.CompareFiles(string! file1, string! file2) -> bool +static Microsoft.TemplateEngine.TestHelper.TestUtils.CreateTemporaryFolder(string! name = "") -> string! +static Microsoft.TemplateEngine.TestHelper.TestUtils.DirectoryCopy(string! sourceDirName, string! destDirName, bool copySubDirs) -> void +static Microsoft.TemplateEngine.TestHelper.TestUtils.SetupNuGetConfigForPackagesLocation(string! projectDirectory, string! packagesLocation) -> void +static readonly Microsoft.TemplateEngine.TestHelper.BuiltInTemplatePackagesProviderFactory.FactoryId -> System.Guid +static readonly Microsoft.TemplateEngine.TestHelper.TestFileSystemUtils.DefaultConfigRelativePath -> string! \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/SharedTestOutputHelper.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/SharedTestOutputHelper.cs new file mode 100644 index 000000000000..52347e1d906e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/SharedTestOutputHelper.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if XUNIT_V3 +using System.Text; +using Xunit; +using Xunit.Sdk; +using Xunit.v3; +#else +using Xunit.Abstractions; +using Xunit.Sdk; +#endif + +namespace Microsoft.TemplateEngine.TestHelper +{ + /// + /// This is so we can pass ITestOutputHelper to TestCommand constructor + /// when calling from SharedHomeDirectory. + /// + public class SharedTestOutputHelper : ITestOutputHelper + { + private readonly IMessageSink _sink; +#if XUNIT_V3 + private readonly StringBuilder _output = new(); +#endif + + public SharedTestOutputHelper(IMessageSink sink) + { + this._sink = sink; + } + +#if XUNIT_V3 + public string Output => _output.ToString(); + + public void Write(string message) + { + _output.Append(message); + _sink.OnMessage(new DiagnosticMessage(message)); + } + + public void Write(string format, params object[] args) + { + string message = string.Format(format, args); + _output.Append(message); + _sink.OnMessage(new DiagnosticMessage(message)); + } + + public void WriteLine(string message) + { + _output.AppendLine(message); + _sink.OnMessage(new DiagnosticMessage(message)); + } + + public void WriteLine(string format, params object[] args) + { + string message = string.Format(format, args); + _output.AppendLine(message); + _sink.OnMessage(new DiagnosticMessage(message)); + } +#else + public void WriteLine(string message) + { + _sink.OnMessage(new DiagnosticMessage(message)); + } + + public void WriteLine(string format, params object[] args) + { + _sink.OnMessage(new DiagnosticMessage(format, args)); + } +#endif + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/StringExtensions.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/StringExtensions.cs new file mode 100644 index 000000000000..3111049d14d9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/StringExtensions.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.TestHelper +{ + public static class StringExtensions + { + public static string UnixifyLineBreaks(this string input) + { + return input.Replace("\r\n", "\n"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestConstraintFactory.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestConstraintFactory.cs new file mode 100644 index 000000000000..bedb4bbefa72 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestConstraintFactory.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Constraints; + +namespace Microsoft.TemplateEngine.TestHelper +{ + /// + /// Test cosnstraint factory. Creates a constraint wtih given type. + /// If args == yes, the constraint returns , + /// if args == no, the constraint returns , otherwise . + /// + public class TestConstraintFactory : ITemplateConstraintFactory + { + public TestConstraintFactory(string type) + { + Type = type; + Id = Guid.NewGuid(); + } + + public string Type { get; } + + public Guid Id { get; } + + public Task CreateTemplateConstraintAsync(IEngineEnvironmentSettings environmentSettings, CancellationToken cancellationToken) + { + return Task.FromResult((ITemplateConstraint)new TestConstraint(this)); + } + + private class TestConstraint : ITemplateConstraint + { + public TestConstraint(ITemplateConstraintFactory factory) + { + Type = factory.Type; + } + + public string Type { get; } + + public string DisplayName => "Test Constraint"; + + public TemplateConstraintResult Evaluate(string? args) + { + if (args == "yes") + { + return TemplateConstraintResult.CreateAllowed(this); + } + else if (args == "no") + { + return TemplateConstraintResult.CreateRestricted(this, "cannot run", "do smth"); + } + return TemplateConstraintResult.CreateEvaluationFailure(this, "bad params"); + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestFileSystemUtils.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestFileSystemUtils.cs new file mode 100644 index 000000000000..106cc5a29043 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestFileSystemUtils.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.Mount; + +namespace Microsoft.TemplateEngine.TestHelper +{ + public static class TestFileSystemUtils + { + public static readonly string DefaultConfigRelativePath = ".template.config/template.json"; + + /// + /// Returns the temp directory path that has been virtualized. + /// + public static string GetTempVirtualizedPath(this IEngineEnvironmentSettings environmentSettings) + { + string basePath = Path.Combine(Directory.GetCurrentDirectory(), "sandbox", Guid.NewGuid().ToString()) + Path.DirectorySeparatorChar; + environmentSettings.Host.VirtualizeDirectory(basePath); + + return basePath; + } + + /// + /// Writes the file with content . + /// + public static void WriteFile(this IEngineEnvironmentSettings environmentSettings, string filePath, string? fileContent) + { + string fullPathDir = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Failed to get directory name.", nameof(filePath)); + + environmentSettings.Host.FileSystem.CreateDirectory(fullPathDir); + environmentSettings.Host.FileSystem.WriteAllText(filePath, fileContent ?? string.Empty); + } + + /// + /// Writes the to . + /// + public static void WriteTemplateSource( + this IEngineEnvironmentSettings environmentSettings, + string sourceBasePath, + IDictionary templateSourceFileNamesWithContent) + { + foreach (KeyValuePair fileInfo in templateSourceFileNamesWithContent) + { + string filePath = Path.Combine(sourceBasePath, fileInfo.Key); + WriteFile(environmentSettings, filePath, fileInfo.Value); + } + } + + /// + /// Mounts the and returns the mount point. + /// Note that is disposable and needs to be disposed after use. + /// + /// when mount succeeded but returned is . + /// when the path failed to be mounted. + public static IMountPoint MountPath(this IEngineEnvironmentSettings environmentSettings, string sourceBasePath) + { + foreach (IMountPointFactory factory in environmentSettings.Components.OfType()) + { + if (factory.TryMount(environmentSettings, null, sourceBasePath, out IMountPoint? sourceMountPoint)) + { + if (sourceMountPoint is null) + { + throw new InvalidOperationException($"{nameof(sourceMountPoint)} cannot be null when {nameof(factory.TryMount)} is 'true'."); + } + return sourceMountPoint; + } + } + throw new Exception($"Failed to mount path {sourceBasePath}."); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestHost.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestHost.cs new file mode 100644 index 000000000000..4f0c804b1af9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestHost.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; +using Microsoft.TemplateEngine.Edge; +using Microsoft.TemplateEngine.Utils; + +namespace Microsoft.TemplateEngine.TestHelper +{ + public partial class TestHost : ITemplateEngineHost + { + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + private readonly string _hostIdentifier; + private readonly string _version; + private IPhysicalFileSystem _fileSystem; + private readonly IReadOnlyList<(Type, IIdentifiedComponent)> _builtIns; + private readonly IReadOnlyList _fallbackNames; + + internal TestHost( + [CallerMemberName] string hostIdentifier = "", + string version = "1.0.0", + bool loadDefaultGenerator = true, + IReadOnlyList<(Type, IIdentifiedComponent)>? additionalComponents = null, + IPhysicalFileSystem? fileSystem = null, + IReadOnlyList? fallbackNames = null, + IEnumerable? addLoggerProviders = null) + { + _hostIdentifier = string.IsNullOrWhiteSpace(hostIdentifier) ? "TestRunner" : hostIdentifier; + _version = string.IsNullOrWhiteSpace(version) ? "1.0.0" : version; + + var builtIns = new List<(Type, IIdentifiedComponent)>(); + if (additionalComponents != null) + { + builtIns.AddRange(additionalComponents); + } + builtIns.AddRange(Edge.Components.AllComponents); + if (loadDefaultGenerator) + { + builtIns.AddRange(Orchestrator.RunnableProjects.Components.AllComponents); + } + + _builtIns = builtIns; + HostParamDefaults = new Dictionary(); + _fileSystem = fileSystem ?? new PhysicalFileSystem(); + + _loggerFactory = new TestLoggerFactory(); + addLoggerProviders?.ToList().ForEach(_loggerFactory.AddProvider); + _logger = _loggerFactory.CreateLogger(hostIdentifier); + _fallbackNames = fallbackNames ?? new[] { "dotnetcli" }; + } + + public Dictionary HostParamDefaults { get; set; } = new Dictionary(); + + IPhysicalFileSystem ITemplateEngineHost.FileSystem => _fileSystem; + + string ITemplateEngineHost.HostIdentifier => _hostIdentifier; + + IReadOnlyList ITemplateEngineHost.FallbackHostTemplateConfigNames => _fallbackNames; + + string ITemplateEngineHost.Version => _version; + + IReadOnlyList<(Type, IIdentifiedComponent)> ITemplateEngineHost.BuiltInComponents => _builtIns; + + ILogger ITemplateEngineHost.Logger => _logger; + + ILoggerFactory ITemplateEngineHost.LoggerFactory => _loggerFactory; + + public static ITemplateEngineHost GetVirtualHost( + [CallerMemberName] string hostIdentifier = "", + IEnvironment? environment = null, + IReadOnlyList<(Type, IIdentifiedComponent)>? additionalComponents = null, + IReadOnlyDictionary? defaultParameters = null) + { + TestHost host = new TestHost(hostIdentifier: hostIdentifier, additionalComponents: additionalComponents); + environment ??= new DefaultEnvironment(); + + if (defaultParameters != null) + { + foreach (var parameter in defaultParameters) + { + host.HostParamDefaults[parameter.Key] = parameter.Value; + } + } + ((ITemplateEngineHost)host).VirtualizeDirectory(new DefaultPathInfo(environment, host).GlobalSettingsDir); + return host; + } + + bool ITemplateEngineHost.TryGetHostParamDefault(string paramName, out string? value) + { + return HostParamDefaults.TryGetValue(paramName, out value); + } + + void ITemplateEngineHost.VirtualizeDirectory(string path) + { + _fileSystem = new InMemoryFileSystem(path, _fileSystem); + } + + public void Dispose() + { + _loggerFactory?.Dispose(); + } + + [Obsolete] + bool ITemplateEngineHost.OnPotentiallyDestructiveChangesDetected(IReadOnlyList changes, IReadOnlyList destructiveChanges) + { + //do nothing + return false; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestLoggerFactory.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestLoggerFactory.cs new file mode 100644 index 000000000000..c344f39eebc1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestLoggerFactory.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +#if XUNIT_V3 +using Xunit.Sdk; +#else +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.TestHelper +{ + public class TestLoggerFactory : ILoggerFactory + { + private readonly List _loggerProviders = new List(); + + private readonly List _factories = new List(); + + public TestLoggerFactory(IMessageSink? messageSink = null) + { + if (messageSink != null) + { + SharedTestOutputHelper testOutputHelper = new SharedTestOutputHelper(messageSink); + _loggerProviders = + new List() { new XunitLoggerProvider(testOutputHelper) }; + } + } + + public void Dispose() + { + while (_factories.Count > 0) + { + var factory = _factories[0]; + _factories.RemoveAt(0); + + factory?.Dispose(); + } + } + + public ILogger CreateLogger(string categoryName) + { + var loggerFactory = LoggerFactory.Create(builder => + { + builder + .SetMinimumLevel(LogLevel.Trace); + + if (_loggerProviders?.Any() ?? false) + { + foreach (ILoggerProvider loggerProvider in _loggerProviders) + { + builder.AddProvider(loggerProvider); + } + } + builder.AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "[yyyy-MM-dd HH:mm:ss.fff] "; + options.IncludeScopes = true; + }); + }); + _factories.Add(loggerFactory); + return loggerFactory.CreateLogger(categoryName); + } + + public ILogger CreateLogger() => CreateLogger("Test Host"); + + public void AddProvider(ILoggerProvider provider) + { + _loggerProviders.Add(provider); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestUtils.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestUtils.cs new file mode 100644 index 000000000000..2cac468dd2cf --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/TestUtils.cs @@ -0,0 +1,124 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.TemplateEngine.TestHelper +{ + public static class TestUtils + { + public static string CreateTemporaryFolder(string name = "") + { + string workingDir = Path.Combine(Path.GetTempPath(), "TemplateEngine.Tests", Guid.NewGuid().ToString(), name); + Directory.CreateDirectory(workingDir); + return workingDir; + } + + public static void SetupNuGetConfigForPackagesLocation(string projectDirectory, string packagesLocation) + { + string nugetConfigShim = +$@" + + + + + + + + +"; + + File.WriteAllText(Path.Combine(projectDirectory, "nuget.config"), nugetConfigShim); + } + + public static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs) + { + // Get the subdirectories for the specified directory. + DirectoryInfo dir = new DirectoryInfo(sourceDirName); + + if (!dir.Exists) + { + throw new DirectoryNotFoundException( + "Source directory does not exist or could not be found: " + + dir.FullName); + } + + DirectoryInfo[] dirs = dir.GetDirectories(); + + // If the destination directory doesn't exist, create it. + Directory.CreateDirectory(destDirName); + + // Get the files in the directory and copy them to the new location. + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) + { + string tempPath = Path.Combine(destDirName, file.Name); + file.CopyTo(tempPath, false); + } + + // If copying subdirectories, copy them and their contents to new location. + if (copySubDirs) + { + foreach (DirectoryInfo subdir in dirs) + { + string tempPath = Path.Combine(destDirName, subdir.Name); + DirectoryCopy(subdir.FullName, tempPath, copySubDirs); + } + } + } + + public static bool CompareFiles(string file1, string file2) + { + using var fs1 = new FileStream(file1, FileMode.Open, FileAccess.Read); + using var fs2 = new FileStream(file2, FileMode.Open, FileAccess.Read); + if (fs1.Length != fs2.Length) + { + return false; + } + + int file1byte; + int file2byte; + do + { + file1byte = fs1.ReadByte(); + file2byte = fs2.ReadByte(); + } + while ((file1byte == file2byte) && (file1byte != -1)); + return (file1byte - file2byte) == 0; + } + + public static async Task AttemptSearch(int count, TimeSpan interval, Func> execute) where TEx : Exception + { + T? result = default; + int attempt = 0; + while (attempt < count) + { + try + { + result = await execute(); + break; + } + catch (Exception ex) + { + if (attempt + 1 == count) + { + throw; + } + + if (ex is AggregateException agEx) + { + if (!agEx.InnerExceptions.Any(e => e is TEx)) + { + throw; + } + } + else if (ex is not TEx) + { + throw; + } + } + await Task.Delay(interval); + attempt++; + } + return result!; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/XunitLoggerProvider.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/XunitLoggerProvider.cs new file mode 100644 index 000000000000..9f1130d0360a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestHelper/XunitLoggerProvider.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Extensions.Logging; +#if XUNIT_V3 +using Xunit; +#else +using Xunit.Abstractions; +#endif + +namespace Microsoft.TemplateEngine.TestHelper +{ + /// + /// Microsoft.Extensions.Logging which logs to XUnit test output. + /// + /// + /// See https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Logging/tests/DI.Common/Common/src/XunitLoggerProvider.cs for more details. + /// + public class XunitLoggerProvider : ILoggerProvider + { + private readonly ITestOutputHelper _output; + private readonly LogLevel _minLevel; + private readonly DateTimeOffset? _logStart; + + public XunitLoggerProvider(ITestOutputHelper output) + : this(output, LogLevel.Trace) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel) + : this(output, minLevel, null) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) + { + _output = output; + _minLevel = minLevel; + _logStart = logStart; + } + + public ILogger CreateLogger(string categoryName) + { + return new XunitLogger(_output, categoryName, _minLevel, _logStart); + } + + public void Dispose() + { + } + + private class XunitLogger : ILogger + { + private static readonly string[] NewLineChars = new[] { Environment.NewLine }; + private readonly string _category; + private readonly LogLevel _minLogLevel; + private readonly ITestOutputHelper _output; + private readonly DateTimeOffset? _logStart; + + public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel, DateTimeOffset? logStart) + { + _minLogLevel = minLogLevel; + _category = category; + _output = output; + _logStart = logStart; + } + + public void Log( + LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + // Buffer the message into a single string in order to avoid shearing the message when running across multiple threads. + var messageBuilder = new StringBuilder(); + + var timestamp = _logStart.HasValue ? $"{(DateTimeOffset.UtcNow - _logStart.Value).TotalSeconds:N3}s" : DateTimeOffset.UtcNow.ToString("s"); + + var firstLinePrefix = $"| [{timestamp}] {_category} {logLevel}: "; + var lines = formatter(state, exception).Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries); + messageBuilder.AppendLine(firstLinePrefix + (lines.FirstOrDefault() ?? string.Empty)); + + var additionalLinePrefix = "|" + new string(' ', firstLinePrefix.Length - 1); + foreach (var line in lines.Skip(1)) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + + if (exception != null) + { + lines = exception.ToString().Split(NewLineChars, StringSplitOptions.RemoveEmptyEntries); + additionalLinePrefix = "| "; + foreach (var line in lines) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + } + + // Remove the last line-break, because ITestOutputHelper only has WriteLine. + var message = messageBuilder.ToString(); + if (message.EndsWith(Environment.NewLine)) + { + message = message.Substring(0, message.Length - Environment.NewLine.Length); + } + + try + { + _output.WriteLine(message); + } + catch (Exception) + { + // We could fail because we're on a background thread and our captured ITestOutputHelper is + // busted (if the test "completed" before the background thread fired). + // So, ignore this. There isn't really anything we can do but hope the + // caller has additional loggers registered + } + } + + public bool IsEnabled(LogLevel logLevel) + => logLevel >= _minLogLevel; + + public IDisposable BeginScope(TState state) where TState : notnull + => new NullScope(); + + private class NullScope : IDisposable + { + public void Dispose() + { + } + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/Microsoft.TemplateEngine.TestTemplates.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/Microsoft.TemplateEngine.TestTemplates.csproj new file mode 100644 index 000000000000..6505f27299d8 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/Microsoft.TemplateEngine.TestTemplates.csproj @@ -0,0 +1,27 @@ + + + + $(NetCurrent) + Template + Microsoft.TemplateEngine.TestTemplates + Microsoft + Test Templates + true + false + content + $(NoWarn);NU5128;NU5110;NU5111 + true + true + true + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/nupkg_templates/TestNupkgInstallTemplate.0.0.1.nupkg b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/nupkg_templates/TestNupkgInstallTemplate.0.0.1.nupkg new file mode 100644 index 000000000000..6bc982486f16 Binary files /dev/null and b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/nupkg_templates/TestNupkgInstallTemplate.0.0.1.nupkg differ diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/nupkg_templates/TestNupkgInstallTemplateV2.0.0.2.nupkg b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/nupkg_templates/TestNupkgInstallTemplateV2.0.0.2.nupkg new file mode 100644 index 000000000000..4fc4fa718e13 Binary files /dev/null and b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/nupkg_templates/TestNupkgInstallTemplateV2.0.0.2.nupkg differ diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/.template.config/template.json new file mode 100644 index 000000000000..8aa270efc3a6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/.template.config/template.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "ConfigurationKitchenSink", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.ConfigurationKitchenSink", + "precedence": "100", + "identity": "TestAssets.ConfigurationKitchenSink", + "shortName": "TestAssets.ConfigurationKitchenSink", + "sources": [ + { + "modifiers": [ + { + "rename": { + "RenameBattery/C.txt": "RenameBattery/D.txt" + } + } + ], + "rename": { + "RenameBattery/A.txt": "RenameBattery/B.txt" + } + } + ], + "symbols": { + "defaultFalse": { + "type": "parameter", + "dataType": "boolean", + "defaultValue": "false" + }, + "defaultTrue": { + "type": "parameter", + "dataType": "boolean", + "defaultValue": "true" + }, + "replaceThings": { + "type": "parameter", + "dataType": "string", + "replaces": "Things", + "defaultValue": "Things" + }, + "replaceThere": { + "type": "parameter", + "dataType": "string", + "replaces": "There", + "onlyIf": [ + { + "after": "Hello ", + "before": "!" + } + ], + "defaultValue": "Things" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Company.ClassLibrary1.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Company.ClassLibrary1.csproj new file mode 100644 index 000000000000..93e6ec640733 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Company.ClassLibrary1.csproj @@ -0,0 +1,37 @@ + + + + + + + netstandard1.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/RenameBattery/A.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/RenameBattery/A.txt new file mode 100644 index 000000000000..e02abfc9b0e1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/RenameBattery/A.txt @@ -0,0 +1 @@ + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/RenameBattery/C.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/RenameBattery/C.txt new file mode 100644 index 000000000000..e02abfc9b0e1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/RenameBattery/C.txt @@ -0,0 +1 @@ + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Test.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Test.cs new file mode 100644 index 000000000000..6e905f4d7718 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Test.cs @@ -0,0 +1,29 @@ +using MyApp.Test; + +//Things +namespace MyApp +{ +#if defaultTrue + public class DefaultTrueIncluded { } +#else + public class DefaultTrueExcluded { } +#endif + +#if defaultFalse + public class DefaultFalseExcluded { } +#else + public class DefaultFalseIncluded { } +#endif + +//-:cnd:noEmit +#if DEBUG1 + public class InsideUnknownDirectiveNoEmit { } +#endif +//+:cnd:noEmit + +//-:cnd +#if DEBUG2 + public class InsideUnknownDirectiveEmit { } +#endif +//+:cnd +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Test.css b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Test.css new file mode 100644 index 000000000000..bf48c788f226 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Test.css @@ -0,0 +1,23 @@ +/*Things*/ +a { + /*#if (defaultTrue) */ + DefaultTrueIncluded: 0; + /*#else*/ + DefaultTrueExcluded: 0; + /*#endif*/ + /*#if (defaultFalse) */ + DefaultFalseExcluded: 0; + /*#else*/ + DefaultFalseIncluded: 0; + /*#endif*/ +/*-:cnd:noEmit*/ + /*#if (DEBUG1) */ + InsideUnknownDirectiveNoEmit: 0; + /*#endif*/ +/*+:cnd:noEmit*/ +/*-:cnd*/ + /*#if (DEBUG2) */ + InsideUnknownDirectiveEmit: 0; + /*#endif*/ +/*+:cnd*/ +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Test.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Test.json new file mode 100644 index 000000000000..46a80261d65c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ConfigurationKitchenSink/Test.json @@ -0,0 +1,23 @@ +//Things +{ + //#if defaultTrue + "DefaultTrueIncluded": 0, + //#else + "DefaultTrueExcluded": 0, + //#endif + //#if defaultFalse + "DefaultFalseExcluded": 0, + //#else + "DefaultFalseIncluded": 0, + //#endif +//-:cnd:noEmit + //#if DEBUG1 + "InsideUnknownDirectiveNoEmit": 0, + //#endif +//+:cnd:noEmit +//-:cnd + //#if DEBUG2 + "InsideUnknownDirectiveEmit": 0 + //#endif +//+:cnd +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Constraints/RestrictedTemplate/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Constraints/RestrictedTemplate/.template.config/template.json new file mode 100644 index 000000000000..d8031e387232 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Constraints/RestrictedTemplate/.template.config/template.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "Constraints - Restricted Template", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "Constraints.RestrictedTemplate", + "precedence": "100", + "identity": "Constraints.RestrictedTemplate", + "shortName": "Constraints.RestrictedTemplate", + "sourceName": "bar", + "constraints": { + "host-restriction": { + "type": "host", + "args": [ + { + "hostname": "do-not-exist", + "version": "(1.0, )" + } + ] + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Constraints/RestrictedTemplate/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Constraints/RestrictedTemplate/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Constraints/RestrictedTemplate/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/.template.config/template.json new file mode 100644 index 000000000000..43f4ef111fcf --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/.template.config/template.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "DefaultIfOptionWithoutValue", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.DefaultIfOptionWithoutValue", + "precedence": "100", + "identity": "TestAssets.DefaultIfOptionWithoutValue", + "shortName": "TestAssets.DefaultIfOptionWithoutValue", + "sourceName": "Company.Web.Application1", + "sources": [ + { + "modifiers": [ + { + "exclude": "StringOtherFile.cs", + "condition": "(MyString != 'NoValueDefaultString')" + }, + { + "exclude": "ChoiceOtherFile.cs", + "condition": "(MyChoice != 'NoValueDefaultChoice')" + } + ] + } + ], + "symbols": { + "MyString": { + "type": "parameter", + "datatype": "string", + "description": "Test string", + "defaultValue": "RegularDefaultString", + "replaces": "OriginalString", + "DefaultIfOptionWithoutValue": "NoValueDefaultString" + }, + "MyChoice": { + "type": "parameter", + "datatype": "choice", + "description": "Test choice", + "defaultValue": "RegularDefaultChoice", + "DefaultIfOptionWithoutValue": "NoValueDefaultChoice", + "choices": [ + { + "choice": "RegularDefaultChoice", + "description": "Choice 1" + }, + { + "choice": "NoValueDefaultChoice", + "description": "Choice 2" + }, + { + "choice": "OtherChoice", + "description": "Choice 3" + } + ], + "replaces": "OriginalChoice" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/ChoiceOtherFile.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/ChoiceOtherFile.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/ChoiceOtherFile.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/Program.cs new file mode 100644 index 000000000000..1f28c56be1dd --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/Program.cs @@ -0,0 +1,13 @@ +using System; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.DefaultIfOptionWithoutValue +{ + public class Program + { + // User specified comment: OriginalString + public void Main() + { + Console.WriteLine("Template created with MyChoice = OriginalChoice"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/StringOtherFile.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/StringOtherFile.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/DefaultIfOptionWithoutValue/StringOtherFile.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/InvalidHostData/.template.config/dotnetcli.host.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/InvalidHostData/.template.config/dotnetcli.host.json new file mode 100644 index 000000000000..a293aa4f284a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/InvalidHostData/.template.config/dotnetcli.host.json @@ -0,0 +1,15 @@ +//bad json +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "SdkVersion": { + "longName": "sdk-version", + "shortName": "" + }, + "RollForward": { + "longName": "roll-forward", + "shortName": "" + }, + "dotnet-cli-version": { + "isHidden": "true" + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/InvalidHostData/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/InvalidHostData/.template.config/template.json new file mode 100644 index 000000000000..52999f7bcbd8 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/InvalidHostData/.template.config/template.json @@ -0,0 +1,108 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ + "Config" + ], + "name": "TestAssets.Invalid.InvalidHostData", + "generatorVersions": "[1.0.0.0-*)", + "tags": { + "type": "item" + }, + "groupIdentity": "TestAssets.Invalid.InvalidHostData", + "precedence": "100", + "identity": "TestAssets.Invalid.InvalidHostData", + "shortName": "TestAssets.Invalid.InvalidHostData", + "sourceName": "unused", + "primaryOutputs": [ + { + "path": "global.json" + } + ], + "defaultName": "global.json", + "symbols": { + "HostIdentifier": { + "type": "bind", + "binding": "HostIdentifier" + }, + "SdkVersion": { + "type": "parameter", + "datatype": "string", + "description": "The version of the .NET SDK to use.", + "displayName": "SDK version" + }, + "dotnet-cli-version": { + "type": "parameter", + "datatype": "string", + "displayName": "dotnet CLI version" + }, + "CombinedVersion": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "SdkVersion", + "fallbackVariableName": "dotnet-cli-version" + }, + "replaces": "SDK_VERSION" + }, + "RollForward": { + "type": "parameter", + "description": "The roll-forward policy to use when selecting an SDK version.", + "displayName": "Roll Forward", + "replaces": "ROLL_FORWARD_VALUE", + "defaultValue": "", + "datatype": "choice", + "choices": [ + { + "choice": "patch", + "description": "Uses the specified version.\nIf not found, rolls forward to the latest patch level.\nIf not found, fails.\nThis value is the legacy behavior from the earlier versions of the SDK." + }, + { + "choice": "feature", + "description": "Uses the latest patch level for the specified major, minor, and feature band.\nIf not found, rolls forward to the next higher feature band within the same major/minor and uses the latest patch level for that feature band.\nIf not found, fails." + }, + { + "choice": "minor", + "description": "Uses the latest patch level for the specified major, minor, and feature band.\nIf not found, rolls forward to the next higher feature band within the same major/minor version and uses the latest patch level for that feature band.\nIf not found, rolls forward to the next higher minor and feature band within the same major and uses the latest patch level for that feature band.\nIf not found, fails." + }, + { + "choice": "major", + "description": "Uses the latest patch level for the specified major, minor, and feature band.\nIf not found, rolls forward to the next higher feature band within the same major/minor version and uses the latest patch level for that feature band.\nIf not found, rolls forward to the next higher minor and feature band within the same major and uses the latest patch level for that feature band.\nIf not found, rolls forward to the next higher major, minor, and feature band and uses the latest patch level for that feature band.\nIf not found, fails." + }, + { + "choice": "latestPatch", + "description": "Uses the latest installed patch level that matches the requested major, minor, and feature band with a patch level and that is greater or equal than the specified value.\nIf not found, fails." + }, + { + "choice": "latestFeature", + "description": "Uses the highest installed feature band and patch level that matches the requested major and minor with a feature band and patch level that is greater or equal than the specified value.\nIf not found, fails." + }, + { + "choice": "latestMinor", + "description": "Uses the highest installed minor, feature band, and patch level that matches the requested major with a minor, feature band, and patch level that is greater or equal than the specified value.\nIf not found, fails." + }, + { + "choice": "latestMajor", + "description": "Uses the highest installed .NET SDK with a version that is greater or equal than the specified value.\nIf not found, fail." + }, + { + "choice": "disable", + "description": "Doesn't roll forward. Exact match required." + } + ] + } + }, + "postActions": [ + { + "id": "open-file", + "condition": "(HostIdentifier != \"dotnetcli\" && HostIdentifier != \"dotnetcli-preview\")", + "description": "Opens global.json in the editor", + "manualInstructions": [], + "actionId": "84C0DA21-51C8-4541-9940-6CA19AF04EE6", + "args": { + "files": "0" + }, + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/InvalidHostData/global.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/InvalidHostData/global.json new file mode 100644 index 000000000000..217558135c95 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/InvalidHostData/global.json @@ -0,0 +1,8 @@ +{ + "sdk": { + //#if (RollForward!="") + "rollForward": "ROLL_FORWARD_VALUE", + //#endif + "version": "SDK_VERSION" + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/InvalidFormat/.template.config/localize/templatestrings.de-DE.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/InvalidFormat/.template.config/localize/templatestrings.de-DE.json new file mode 100644 index 000000000000..7f8c66c66442 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/InvalidFormat/.template.config/localize/templatestrings.de-DE.json @@ -0,0 +1 @@ +this is not JSON diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/InvalidFormat/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/InvalidFormat/.template.config/template.json new file mode 100644 index 000000000000..fe8a1e19664d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/InvalidFormat/.template.config/template.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "name in base configuration", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.Invalid.Localization.InvalidFormat", + "precedence": "100", + "identity": "TestAssets.Invalid.Localization.InvalidFormat", + "shortName": "TestAssets.Invalid.Localization.InvalidFormat", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "bar", + "description": "desc" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/InvalidFormat/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/InvalidFormat/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/InvalidFormat/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/.template.config/localize/templatestrings.de-DE.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/.template.config/localize/templatestrings.de-DE.json new file mode 100644 index 000000000000..5d54fc6aa702 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/.template.config/localize/templatestrings.de-DE.json @@ -0,0 +1,20 @@ +{ + "name": "name_de-DE:äÄßöÖüÜ", + "description": "desc_de-DE:äÄßöÖüÜ", + "symbols/someSymbol/displayName": "sym0_displayName_de-DE:äÄßöÖüÜ", + "symbols/someSymbol/description": "sym0_desc_de-DE:äÄßöÖüÜ", + "symbols/someChoice/description": "sym1_desc_de-DE:äÄßöÖüÜ", + "symbols/someChoice/choices/choice0/displayName": "sym1_choice0_de-DE_dn:äÄßöÖüÜ", + "symbols/someChoice/choices/choice0/description": "sym1_choice0_de-DE_d:äÄßöÖüÜ", + "symbols/someChoice/choices/choice1/displayName": "sym1_choice1_de-DE_dn:äÄßöÖüÜ", + "symbols/someChoice/choices/choice1/description": "sym1_choice1_de-DE_d:äÄßöÖüÜ", + "symbols/someOtherChoice/choices/choice0/displayName": "sym1_choice0_de-DE_dn:äÄßöÖüÜ", + "symbols/someOtherChoice/choices/choice1/description": "sym1_choice1_de-DE_d:äÄßöÖüÜ", + "postActions/pa0/description": "pa0_desc_de-DE:äÄßöÖüÜ", + "postActions/pa0/manualInstructions/first_instruction/text": "pa0_manualInstructions_de-DE:äÄßöÖüÜ", + "postActions/pa1/description": "pa1_desc_de-DE:äÄßöÖüÜ", + "postActions/pa1/manualInstructions/third_instruction/text": "pa1_manualInstructions1_de-DE:äÄßöÖüÜ", + "postActions/pa1/manualInstructions/do-not-exist/text": "pa1_manualInstructions2_de-DE:äÄßöÖüÜ", + "postActions/pa2/description": "pa2_desc_de-DE:äÄßöÖüÜ", + "postActions/pa2/manualInstructions/default/text": "pa2_manualInstructions_de-DE:äÄßöÖüÜ" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/.template.config/localize/templatestrings.tr.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/.template.config/localize/templatestrings.tr.json new file mode 100644 index 000000000000..5206e0369275 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/.template.config/localize/templatestrings.tr.json @@ -0,0 +1,14 @@ +{ + "name": "name_tr-TR:çÇğĞıIİöÖşŞüÜ", + "description": "desc_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someSymbol/displayName": "sym0_displayName_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someSymbol/description": "sym0_desc_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someChoice/description": "sym1_desc_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someChoice/displayName": "sym1_displayName_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someChoice/choices/choice0/description": "sym1_choice0_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someChoice/choices/choice2/description": "sym1_choice2_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someOtherChoice/choices/choice0/description": "sym1_choice0_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someOtherChoice/choices/choice2/description": "sym1_choice2_tr-TR:çÇğĞıIİöÖşŞüÜ", + "postActions/pa6/description": "pa0_desc_tr-TR:çÇğĞıIİöÖşŞüÜ", + "postActions/pa6/manualInstructions/first_instruction/text": "pa0_manualInstructions_tr-TR:çÇğĞıIİöÖşŞüÜ" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/.template.config/template.json new file mode 100644 index 000000000000..a94663532f95 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/.template.config/template.json @@ -0,0 +1,102 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "name", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.Invalid.Localization.ValidationFailure", + "precedence": "100", + "identity": "TestAssets.Invalid.Localization.ValidationFailure", + "shortName": "TestAssets.Invalid.Localization.ValidationFailure", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "bar", + "description": "desc", + "symbols": { + "someSymbol": { + "displayName": "sym0_displayName", + "description": "sym0_desc", + "type": "parameter", + "datatype": "string", + "defaultValue": "RegularDefaultString", + "replaces": "OriginalString", + "DefaultIfOptionWithoutValue": "NoValueDefaultString" + }, + "someChoice": { + "type": "parameter", + "displayName": "sym1_displayName", + "description": "sym1_desc", + "datatype": "choice", + "choices": [ + { + "choice": "choice0", + "displayName": "sym1_choice0_displayName", + "description": "sym1_choice0" + }, + { + "choice": "choice1", + "displayName": "sym1_choice1_displayName", + "description": "sym1_choice1" + }, + { + "choice": "choice2", + "displayName": "sym1_choice2_displayName", + "description": "sym1_choice2" + } + ], + "replaces": "choice1", + "defaultValue": "choice1" + } + }, + "postActions": [ + { + "description": "pa0_desc", + "manualInstructions": [ + { + "id": "first_instruction", + "text": "pa0_manualInstructions" + } + ], + "actionId": "58E1433C-303B-4713-8A47-26E7B03BD8A5" + }, + { + "id": "pa1", + "description": "pa1_desc", + "manualInstructions": [ + { + "id": "first_instruction", + "text": "pa1_manualInstructions0" + }, + { + "id": "second_instruction", + "text": "pa1_manualInstructions1" + }, + { + "id": "third_instruction", + "text": "pa1_manualInstructions2" + } + ], + "actionId": "2B5CDBB7-1FAC-41EC-ADCC-08A73021BC40" + }, + { + "id": "pa2", + "manualInstructions": [ + { + "text": "pa2_manualInstructions" + } + ], + "actionId": "58E1433C-303B-4713-8A47-26E7B03BD8A5" + }, + { + "id": "pa2", + "manualInstructions": [ + { + "text": "pa2_manualInstructions" + } + ], + "actionId": "58E1433C-303B-4713-8A47-26E7B03BD8A5" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/Localization/ValidationFailure/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/MissingIdentity/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/MissingIdentity/.template.config/template.json new file mode 100644 index 000000000000..e0b61b12e355 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/MissingIdentity/.template.config/template.json @@ -0,0 +1,8 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "group", + "precedence": "100", + "sourceName": "Basic" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/MissingMandatoryConfig/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/MissingMandatoryConfig/.template.config/template.json new file mode 100644 index 000000000000..03c33c9312db --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/MissingMandatoryConfig/.template.config/template.json @@ -0,0 +1,3 @@ +{ + "identity": "MissingConfigTest", +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateA/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateA/.template.config/template.json new file mode 100644 index 000000000000..be590eb2f7da --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateA/.template.config/template.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "Invalid.SameShortName.TemplateA", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "Invalid.SameShortName.TemplateA", + "precedence": "100", + "identity": "Invalid.SameShortName.TemplateA", + "shortName": "sameshortname", + "sourceName": "bar" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateA/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateA/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateA/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateB/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateB/.template.config/template.json new file mode 100644 index 000000000000..e7f01b999a31 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateB/.template.config/template.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "Invalid.SameShortName.TemplateB", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "Invalid.SameShortName.TemplateB", + "precedence": "100", + "identity": "Invalid.SameShortName.TemplateB", + "shortName": "sameshortname", + "sourceName": "bar" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateB/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateB/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/Invalid/SameShortName/TemplateB/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/Basic/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/Basic/.template.config/template.json new file mode 100644 index 000000000000..33cb40b9809e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/Basic/.template.config/template.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.AddPackageReference.Basic", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.AddPackageReference.Basic", + "precedence": "100", + "identity": "TestAssets.PostActions.AddPackageReference.Basic", + "shortName": "TestAssets.PostActions.AddPackageReference.Basic", + "sourceName": "Basic", + "postActions": [ + { + "description": "Adding Reference to Newtonsoft.Json NuGet package", + "actionId": "B17581D1-C5C9-4489-8F0A-004BE667B814", + "continueOnError": false, + "manualInstructions": [ + { + "text": "Manually add the reference to Newtonsoft.Json to your project file" + } + ], + "args": { + "referenceType": "package", + "reference": "Newtonsoft.Json", + "version": "13.0.3", + "projectFileExtensions": ".csproj" + } + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/Basic/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/Basic/Basic.csproj new file mode 100644 index 000000000000..120e38c31501 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/Basic/Basic.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/Basic/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/Basic/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/Basic/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/BasicWithFiles/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/BasicWithFiles/.template.config/template.json new file mode 100644 index 000000000000..ce3cfeb11cdd --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/BasicWithFiles/.template.config/template.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.AddPackageReference.BasicWithFiles", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.AddPackageReference.BasicWithFiles", + "precedence": "100", + "identity": "TestAssets.PostActions.AddPackageReference.BasicWithFiles", + "shortName": "TestAssets.PostActions.AddPackageReference.BasicWithFiles", + "sourceName": "Basic", + "postActions": [ + { + "description": "Adding Reference to Newtonsoft.Json NuGet package", + "actionId": "B17581D1-C5C9-4489-8F0A-004BE667B814", + "ContinueOnError": false, + "manualInstructions": [ + { + "text": "Manually add the reference to Newtonsoft.Json to your project file" + } + ], + "args": { + "files": [ "Basic.csproj" ], + "referenceType": "package", + "reference": "Newtonsoft.Json", + "version": "13.0.3" + } + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/BasicWithFiles/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/BasicWithFiles/Basic.csproj new file mode 100644 index 000000000000..120e38c31501 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/BasicWithFiles/Basic.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/BasicWithFiles/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/BasicWithFiles/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddPackageReference/BasicWithFiles/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/.template.config/template.json new file mode 100644 index 000000000000..f989b578dafd --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/.template.config/template.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.AddProjectReference.Basic", + "tags": { "type": "project" }, + "groupIdentity": "TestAssets.PostActions.AddProjectReference.Basic", + "precedence": "100", + "identity": "TestAssets.PostActions.AddProjectReference.Basic", + "shortName": "TestAssets.PostActions.AddProjectReference.Basic", + "primaryOutputs": [ + { + "path": "Project1/Project1.csproj" + }, + { + "path": "Project2/Project2.csproj" + } + ], + "postActions": [ + { + "description": "Adding Reference to Newtonsoft.Json NuGet package", + "actionId": "B17581D1-C5C9-4489-8F0A-004BE667B814", + "continueOnError": false, + "manualInstructions": [ + { + "text": "Manually add the reference to Project2 in Project1" + } + ], + "args": { + "targetFiles": [ + "./Project1/Project1.csproj" + ], + "referenceType": "project", + "reference": "./Project2/Project2.csproj", + "projectFileExtensions": ".csproj" + } + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project1/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project1/Program.cs new file mode 100644 index 000000000000..c650074c95a7 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project1/Program.cs @@ -0,0 +1,14 @@ +using System; +using Project2; + +namespace Project1 +{ + public class Program + { + public static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + MyClass.SayHello(); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project1/Project1.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project1/Project1.csproj new file mode 100644 index 000000000000..120e38c31501 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project1/Project1.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project2/MyClass.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project2/MyClass.cs new file mode 100644 index 000000000000..4c812a5f853e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project2/MyClass.cs @@ -0,0 +1,12 @@ +using System; + +namespace Project2 +{ + public class MyClass + { + public static void SayHello() + { + Console.WriteLine("Hello World from MyClass"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project2/Project2.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project2/Project2.csproj new file mode 100644 index 000000000000..8268829b64bf --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectReference/Basic/Project2/Project2.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/Basic/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/Basic/.template.config/template.json new file mode 100644 index 000000000000..a03b130a00f5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/Basic/.template.config/template.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.AddProjectToSolution.Basic", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.AddProjectToSolution.Basic", + "precedence": "100", + "identity": "TestAssets.PostActions.AddProjectToSolution.Basic", + "shortName": "TestAssets.PostActions.AddProjectToSolution.Basic", + "sourceName": "Basic", + "primaryOutputs": [ + { + "path": "Basic.csproj" + } + ], + "postActions": [ + { + "description": "Add projects to solution", + "manualInstructions": [ { "text": "Add generated project to solution manually." } ], + "args": { + "solutionFolder": "src" + }, + "actionId": "D396686C-DE0E-4DE6-906D-291CD29FC5DE", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/Basic/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/Basic/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/Basic/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/Basic/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/Basic/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/Basic/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithFiles/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithFiles/.template.config/template.json new file mode 100644 index 000000000000..2482b9294308 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithFiles/.template.config/template.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.AddProjectToSolution.BasicWithFiles", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.AddProjectToSolution.BasicWithFiles", + "precedence": "100", + "identity": "TestAssets.PostActions.AddProjectToSolution.BasicWithFiles", + "shortName": "TestAssets.PostActions.AddProjectToSolution.BasicWithFiles", + "sourceName": "Basic", + "primaryOutputs": [ + { + "path": "Basic.csproj" + } + ], + "postActions": [ + { + "description": "Add projects to solution", + "manualInstructions": [ { "text": "Add generated project to solution manually." } ], + "args": { + "solutionFolder": "src", + "projectFiles": [ "Basic.csproj" ] + }, + "actionId": "D396686C-DE0E-4DE6-906D-291CD29FC5DE", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithFiles/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithFiles/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithFiles/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithFiles/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithFiles/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithFiles/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/.template.config/template.json new file mode 100644 index 000000000000..679fef1fda1f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/.template.config/template.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.AddProjectToSolution.BasicWithIndexes", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.AddProjectToSolution.BasicWithIndexes", + "precedence": "100", + "identity": "TestAssets.PostActions.AddProjectToSolution.BasicWithIndexes", + "shortName": "TestAssets.PostActions.AddProjectToSolution.BasicWithIndexes", + "sourceName": "Basic", + "primaryOutputs": [ + { + "path": "./Client/Client.csproj" + }, + { + "path": "./Server/Server.csproj" + } + ], + "postActions": [ + { + "description": "Add projects to solution", + "manualInstructions": [ { "text": "Add generated Server project to solution manually to folder 'Server'." } ], + "args": { + "solutionFolder": "Server", + "primaryOutputIndexes": "1" + }, + "actionId": "D396686C-DE0E-4DE6-906D-291CD29FC5DE", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Client/Client.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Client/Client.csproj new file mode 100644 index 000000000000..120e38c31501 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Client/Client.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Client/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Client/Program.cs new file mode 100644 index 000000000000..9b2d5e531b1c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Client/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Server/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Server/Program.cs new file mode 100644 index 000000000000..9b2d5e531b1c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Server/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Server/Server.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Server/Server.csproj new file mode 100644 index 000000000000..120e38c31501 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/AddProjectToSolution/BasicWithIndexes/Server/Server.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/Instructions/Basic/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/Instructions/Basic/.template.config/template.json new file mode 100644 index 000000000000..ec8a5495ce56 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/Instructions/Basic/.template.config/template.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.Instructions.Basic", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.Instructions.Basic", + "precedence": "100", + "identity": "TestAssets.PostActions.Instructions.Basic", + "shortName": "TestAssets.PostActions.Instructions.Basic", + "sourceName": "Basic", + "postActions": [ + { + "description": "Manual actions needed", + "manualInstructions": [ + { + "text": "Run the following command:" + } + ], + "actionId": "AC1156F7-BB77-4DB8-B28F-24EEBCCA1E5C", + "args": { + "executable": "setup.cmd", + "args": "" + }, + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/Instructions/Basic/Class1.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/Instructions/Basic/Class1.cs new file mode 100644 index 000000000000..a082e956b411 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/Instructions/Basic/Class1.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.TemplateEngine.TestTemplates.test_templates.PostActions.RunScript.Basic +{ + class Class1 + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Basic/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Basic/.template.config/template.json new file mode 100644 index 000000000000..91f38784185d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Basic/.template.config/template.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.Basic", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.Basic", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.Basic", + "shortName": "TestAssets.PostActions.RestoreNuGet.Basic", + "sourceName": "Basic", + "primaryOutputs": [ + { + "path": "Basic.csproj" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Basic/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Basic/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Basic/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Basic/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Basic/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Basic/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/BasicWithFiles/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/BasicWithFiles/.template.config/template.json new file mode 100644 index 000000000000..15f2031bd2f0 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/BasicWithFiles/.template.config/template.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.BasicWithFiles", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.BasicWithFiles", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.BasicWithFiles", + "shortName": "TestAssets.PostActions.RestoreNuGet.BasicWithFiles", + "sourceName": "Basic", + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true, + "args": { + "files": [ "Basic.csproj" ] + } + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/BasicWithFiles/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/BasicWithFiles/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/BasicWithFiles/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/BasicWithFiles/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/BasicWithFiles/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/BasicWithFiles/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePath/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePath/.template.config/template.json new file mode 100644 index 000000000000..3d23af2c5b46 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePath/.template.config/template.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.CustomSourcePath", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.CustomSourcePath", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.CustomSourcePath", + "shortName": "TestAssets.PostActions.RestoreNuGet.CustomSourcePath", + "sourceName": "Basic", + "sources": [ + { + "source": "./Custom/Path/" + } + ], + "primaryOutputs": [ + { + "path": "Basic.csproj" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePath/Custom/Path/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePath/Custom/Path/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePath/Custom/Path/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePath/Custom/Path/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePath/Custom/Path/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePath/Custom/Path/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePathFiles/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePathFiles/.template.config/template.json new file mode 100644 index 000000000000..c221dad9065d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePathFiles/.template.config/template.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.CustomSourcePathFiles", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.CustomSourcePathFiles", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.CustomSourcePathFiles", + "shortName": "TestAssets.PostActions.RestoreNuGet.CustomSourcePathFiles", + "sourceName": "Basic", + "sources": [ + { + "source": "./Custom/Path/" + } + ], + "primaryOutputs": [ + { + "path": "Basic.csproj" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "args": { + "files": [ "./Custom/Path/Basic.csproj" ] + }, + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePathFiles/Custom/Path/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePathFiles/Custom/Path/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePathFiles/Custom/Path/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePathFiles/Custom/Path/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePathFiles/Custom/Path/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourcePathFiles/Custom/Path/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPath/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPath/.template.config/template.json new file mode 100644 index 000000000000..4035a9c84a64 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPath/.template.config/template.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.CustomSourceTargetPath", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.CustomSourceTargetPath", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.CustomSourceTargetPath", + "shortName": "TestAssets.PostActions.RestoreNuGet.CustomSourceTargetPath", + "sourceName": "Basic", + "sources": [ + { + "source": "./Src/Custom/Path/", + "target": "./Target/Output/" + } + ], + "primaryOutputs": [ + { + "path": "Target/Output/Basic.csproj" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPath/Src/Custom/Path/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPath/Src/Custom/Path/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPath/Src/Custom/Path/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPath/Src/Custom/Path/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPath/Src/Custom/Path/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPath/Src/Custom/Path/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPathFiles/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPathFiles/.template.config/template.json new file mode 100644 index 000000000000..dd8c678d1198 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPathFiles/.template.config/template.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.CustomSourceTargetPathFiles", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.CustomSourceTargetPathFiles", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.CustomSourceTargetPathFiles", + "shortName": "TestAssets.PostActions.RestoreNuGet.CustomSourceTargetPathFiles", + "sourceName": "Basic", + "sources": [ + { + "source": "./Src/Custom/Path/", + "target": "./Target/Output/" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "args": { + "files": [ "./Src/Custom/Path/Basic.csproj" ] + }, + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPathFiles/Src/Custom/Path/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPathFiles/Src/Custom/Path/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPathFiles/Src/Custom/Path/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPathFiles/Src/Custom/Path/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPathFiles/Src/Custom/Path/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomSourceTargetPathFiles/Src/Custom/Path/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPath/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPath/.template.config/template.json new file mode 100644 index 000000000000..009f2e9ba118 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPath/.template.config/template.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.CustomTargetPath", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.CustomTargetPath", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.CustomTargetPath", + "shortName": "TestAssets.PostActions.RestoreNuGet.CustomTargetPath", + "sourceName": "Basic", + "sources": [ + { + "target": "./Custom/Path/" + } + ], + "primaryOutputs": [ + { + "path": "./Custom/Path/Basic.csproj" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPath/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPath/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPath/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPath/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPath/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPath/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPathFiles/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPathFiles/.template.config/template.json new file mode 100644 index 000000000000..8c7cf6f5000a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPathFiles/.template.config/template.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.CustomTargetPathFiles", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.CustomTargetPathFiles", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.CustomTargetPathFiles", + "shortName": "TestAssets.PostActions.RestoreNuGet.CustomTargetPathFiles", + "sourceName": "Basic", + "sources": [ + { + "target": "./Custom/Path/" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true, + "args": { + "files": [ "Basic.csproj" ] + } + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPathFiles/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPathFiles/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPathFiles/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPathFiles/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPathFiles/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/CustomTargetPathFiles/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid/.template.config/template.json new file mode 100644 index 000000000000..caa34bd329da --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid/.template.config/template.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.Invalid", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.Invalid", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.Invalid", + "shortName": "TestAssets.PostActions.RestoreNuGet.Invalid", + "sourceName": "invalid", + "primaryOutputs": [ + { + "path": "Invalid.csproj" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": false + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid/Invalid.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid/Invalid.csproj new file mode 100644 index 000000000000..f7168ba58543 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid/Invalid.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid/Program.cs new file mode 100644 index 000000000000..d91f206bcaa9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Invalid +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid_ContinueOnError/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid_ContinueOnError/.template.config/template.json new file mode 100644 index 000000000000..836af7709e74 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid_ContinueOnError/.template.config/template.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.Invalid.ContinueOnError", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.Invalid.ContinueOnError", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.Invalid.ContinueOnError", + "shortName": "TestAssets.PostActions.RestoreNuGet.Invalid.ContinueOnError", + "sourceName": "invalid", + "primaryOutputs": [ + { + "path": "Invalid.csproj" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid_ContinueOnError/Invalid.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid_ContinueOnError/Invalid.csproj new file mode 100644 index 000000000000..f7168ba58543 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid_ContinueOnError/Invalid.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid_ContinueOnError/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid_ContinueOnError/Program.cs new file mode 100644 index 000000000000..d91f206bcaa9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/Invalid_ContinueOnError/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Invalid +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRename/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRename/.template.config/template.json new file mode 100644 index 000000000000..c0363d133172 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRename/.template.config/template.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.SourceRename", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.SourceRename", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.SourceRename", + "shortName": "TestAssets.PostActions.RestoreNuGet.SourceRename", + "sourceName": "Basic", + "sources": [ + { + "rename": { + "Basic.csproj": "MyFirstTestProject.csproj" + } + } + ], + "symbols": { + "firstRename": { + "type": "parameter", + "dataType": "string", + "fileRename": "First" + } + }, + "primaryOutputs": [ + { + "path": "MyFirstTestProject.csproj" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRename/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRename/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRename/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRename/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRename/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRename/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRenameFiles/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRenameFiles/.template.config/template.json new file mode 100644 index 000000000000..2b6b077ef541 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRenameFiles/.template.config/template.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.SourceRenameFiles", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.SourceRenameFiles", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.SourceRenameFiles", + "shortName": "TestAssets.PostActions.RestoreNuGet.SourceRenameFiles", + "sourceName": "Basic", + "sources": [ + { + "rename": { + "Basic.csproj": "MyFirstTestProject.csproj" + } + } + ], + "symbols": { + "firstRename": { + "type": "parameter", + "dataType": "string", + "fileRename": "First" + } + }, + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "args": { + "files": [ "Basic.csproj" ] + }, + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRenameFiles/Basic.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRenameFiles/Basic.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRenameFiles/Basic.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRenameFiles/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRenameFiles/Program.cs new file mode 100644 index 000000000000..38a7692a4297 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/SourceRenameFiles/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace Basic +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/.template.config/template.json new file mode 100644 index 000000000000..2910c0154b52 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/.template.config/template.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.TwoProjectsFiles", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.TwoProjectsFiles", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.TwoProjectsFiles", + "shortName": "TestAssets.PostActions.RestoreNuGet.TwoProjectsFiles", + "sourceName": "MyTestProject", + "sources": [ + { + "source": "./Custom/MyTestProject/", + "target": "./src/MyTestProject/" + }, + { + "source": "./Custom/MyTestProject.Tests/", + "target": "./test/MyTestProject.Tests/" + } + ], + "primaryOutputs": [ + { + "path": "./src/MyTestProject/MyTestProject.csproj" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { "text": "Run 'dotnet restore'" } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject.Tests/MyTestProject.Tests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject.Tests/MyTestProject.Tests.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject.Tests/MyTestProject.Tests.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject.Tests/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject.Tests/Program.cs new file mode 100644 index 000000000000..3165089b2e5d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject.Tests/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace MyTestProject.Tests +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject/MyTestProject.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject/MyTestProject.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject/MyTestProject.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject/Program.cs new file mode 100644 index 000000000000..e24f78b7e6f3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsFiles/Custom/MyTestProject/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace MyTestProject +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/.template.config/template.json new file mode 100644 index 000000000000..66dd83a78c02 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/.template.config/template.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.TwoProjectsPrimaryOutputs", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.TwoProjectsPrimaryOutputs", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.TwoProjectsPrimaryOutputs", + "shortName": "TestAssets.PostActions.RestoreNuGet.TwoProjectsPrimaryOutputs", + "sourceName": "MyTestProject", + "sources": [ + { + "source": "./Custom/MyTestProject/", + "target": "./src/MyTestProject/" + }, + { + "source": "./Custom/MyTestProject.Tests/", + "target": "./test/MyTestProject.Tests/" + } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { "text": "Run 'dotnet restore'" } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true, + "args": { + "files": [ "./Custom/MyTestProject/MyTestProject.csproj" ] //here the source location should be used as the post action supports this notation + } + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject.Tests/MyTestProject.Tests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject.Tests/MyTestProject.Tests.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject.Tests/MyTestProject.Tests.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject.Tests/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject.Tests/Program.cs new file mode 100644 index 000000000000..3165089b2e5d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject.Tests/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace MyTestProject.Tests +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject/MyTestProject.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject/MyTestProject.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject/MyTestProject.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject/Program.cs new file mode 100644 index 000000000000..e24f78b7e6f3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsPrimaryOutputs/Custom/MyTestProject/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace MyTestProject +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/.template.config/template.json new file mode 100644 index 000000000000..18a79ae11826 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/.template.config/template.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.TwoProjectsWithSourceRenames", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.TwoProjectsWithSourceRenames", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.TwoProjectsWithSourceRenames", + "shortName": "TestAssets.PostActions.RestoreNuGet.TwoProjectsWithSourceRenames", + "sourceName": "TemplateProject", + "sources": [ + { + "include": [ "TemplateProject1/*" ], + "rename": { + "TemplateProject1": "TemplateProject.UI" + } + }, + { + "include": [ "TemplateProject2/*" ], + "rename": { + "TemplateProject2": "TemplateProject.Tests" + } + } + ], + "primaryOutputs": [ + { "path": "TemplateProject.UI/TemplateProject.UI.csproj" }, + { "path": "TemplateProject.Tests/TemplateProject.Tests.csproj" } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject1/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject1/Program.cs new file mode 100644 index 000000000000..d38dd97a2e5c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject1/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace TemplateProject1 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject1/TemplateProject1.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject1/TemplateProject1.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject1/TemplateProject1.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject2/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject2/Program.cs new file mode 100644 index 000000000000..d15a7fd4cae9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject2/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace TemplateProject2 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject2/TemplateProject2.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject2/TemplateProject2.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames/TemplateProject2/TemplateProject2.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/.template.config/template.json new file mode 100644 index 000000000000..8e76914dcf36 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/.template.config/template.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RestoreNuGet.TwoProjectsWithSourceRenames2", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RestoreNuGet.TwoProjectsWithSourceRenames2", + "precedence": "100", + "identity": "TestAssets.PostActions.RestoreNuGet.TwoProjectsWithSourceRenames2", + "shortName": "TestAssets.PostActions.RestoreNuGet.TwoProjectsWithSourceRenames2", + "sourceName": "TemplateProject", + "sources": [ + { + "include": [ "TemplateProject1/*", "TemplateProject2/*" ], + "rename": { + "TemplateProject1": "TemplateProject.UI", + "TemplateProject2": "TemplateProject.Tests" + } + } + ], + "primaryOutputs": [ + { "path": "TemplateProject.UI/TemplateProject.UI.csproj" }, + { "path": "TemplateProject.Tests/TemplateProject.Tests.csproj" } + ], + "postActions": [ + { + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject1/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject1/Program.cs new file mode 100644 index 000000000000..d38dd97a2e5c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject1/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace TemplateProject1 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject1/TemplateProject1.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject1/TemplateProject1.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject1/TemplateProject1.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject2/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject2/Program.cs new file mode 100644 index 000000000000..d15a7fd4cae9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject2/Program.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace TemplateProject2 +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + string result = JsonConvert.SerializeObject(new { a = "test" }); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject2/TemplateProject2.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject2/TemplateProject2.csproj new file mode 100644 index 000000000000..1e626621ef77 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RestoreNuGet/TwoProjectsWithSourceRenames2/TemplateProject2/TemplateProject2.csproj @@ -0,0 +1,12 @@ + + + + Exe + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Basic/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Basic/.template.config/template.json new file mode 100644 index 000000000000..62a45644595c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Basic/.template.config/template.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RunScript.Basic", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RunScript.Basic", + "precedence": "100", + "identity": "TestAssets.PostActions.RunScript.Basic", + "shortName": "TestAssets.PostActions.RunScript.Basic", + "sourceName": "Basic", + "postActions": [ + { + "condition": "(OS != \"Windows_NT\")", + "description": "Make scripts executable", + "manualInstructions": [ + { + "text": "Run 'chmod +x *.sh'" + } + ], + "actionId": "cb9a6cf3-4f5c-4860-b9d2-03a574959774", + "args": { + "+x": "*.sh" + }, + "continueOnError": false + }, + { + "condition": "(OS != \"Windows_NT\")", + "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", + "args": { + "executable": "setup.sh", + "redirectStandardOutput": false + }, + "manualInstructions": [ + { + "text": "Run 'setup.sh'" + } + ], + "continueOnError": false, + "description ": "setups the project by calling setup.sh" + }, + { + "condition": "(OS == \"Windows_NT\")", + "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", + "args": { + "executable": "setup.cmd", + "redirectStandardOutput": false + }, + "manualInstructions": [ + { + "text": "Run 'setup.cmd'" + } + ], + "continueOnError": false, + "description ": "setups the project by calling setup.cmd" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Basic/setup.cmd b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Basic/setup.cmd new file mode 100644 index 000000000000..d0b3ddfa9a7d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Basic/setup.cmd @@ -0,0 +1,2 @@ +@ECHO OFF +echo Hello Windows diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Basic/setup.sh b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Basic/setup.sh new file mode 100644 index 000000000000..80dd6a6ee09d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Basic/setup.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo Hello Unix diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/DoNotRedirect/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/DoNotRedirect/.template.config/template.json new file mode 100644 index 000000000000..7acacb30c5c3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/DoNotRedirect/.template.config/template.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RunScript.DoNotRedirect", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RunScript.DoNotRedirect", + "precedence": "100", + "identity": "TestAssets.PostActions.RunScript.DoNotRedirect", + "shortName": "TestAssets.PostActions.RunScript.DoNotRedirect", + "sourceName": "DoNotRedirect", + "postActions": [ + { + "condition": "(OS != \"Windows_NT\")", + "description": "Make scripts executable", + "manualInstructions": [ + { + "text": "Run 'chmod +x *.sh'" + } + ], + "actionId": "cb9a6cf3-4f5c-4860-b9d2-03a574959774", + "args": { + "+x": "*.sh" + }, + "continueOnError": false + }, + { + "condition": "(OS != \"Windows_NT\")", + "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", + "args": { + "executable": "setup.sh", + "redirectStandardOutput": false, + "redirectStandardError": false + }, + "manualInstructions": [ + { + "text": "Run 'setup.sh'" + } + ], + "continueOnError": false, + "description ": "setups the project by calling setup.sh" + }, + { + "condition": "(OS == \"Windows_NT\")", + "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", + "args": { + "executable": "setup.cmd", + "redirectStandardOutput": false, + "redirectStandardError": false + }, + "manualInstructions": [ + { + "text": "Run 'setup.cmd'" + } + ], + "continueOnError": false, + "description ": "setups the project by calling setup.cmd" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/DoNotRedirect/setup.cmd b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/DoNotRedirect/setup.cmd new file mode 100644 index 000000000000..e07c4c312854 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/DoNotRedirect/setup.cmd @@ -0,0 +1,3 @@ +@ECHO OFF +echo "This line goes to stdout" +echo "This line goes to stderr" 1>&2 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/DoNotRedirect/setup.sh b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/DoNotRedirect/setup.sh new file mode 100644 index 000000000000..1bbd8ac6cecd --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/DoNotRedirect/setup.sh @@ -0,0 +1,3 @@ +#!/bin/sh +echo "This line goes to stdout" +>&2 echo "This line goes to stderr" diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Redirect/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Redirect/.template.config/template.json new file mode 100644 index 000000000000..4f7f86abb130 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Redirect/.template.config/template.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RunScript.Redirect", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RunScript.Redirect", + "precedence": "100", + "identity": "TestAssets.PostActions.RunScript.Redirect", + "shortName": "TestAssets.PostActions.RunScript.Redirect", + "sourceName": "DoNotRedirect", + "postActions": [ + { + "condition": "(OS != \"Windows_NT\")", + "description": "Make scripts executable", + "manualInstructions": [ + { + "text": "Run 'chmod +x *.sh'" + } + ], + "actionId": "cb9a6cf3-4f5c-4860-b9d2-03a574959774", + "args": { + "+x": "*.sh" + }, + "continueOnError": false + }, + { + "condition": "(OS != \"Windows_NT\")", + "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", + "args": { + "executable": "setup.sh" + }, + "manualInstructions": [ + { + "text": "Run 'setup.sh'" + } + ], + "continueOnError": false, + "description ": "setups the project by calling setup.sh" + }, + { + "condition": "(OS == \"Windows_NT\")", + "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", + "args": { + "executable": "setup.cmd" + }, + "manualInstructions": [ + { + "text": "Run 'setup.cmd'" + } + ], + "continueOnError": false, + "description ": "setups the project by calling setup.cmd" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Redirect/setup.cmd b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Redirect/setup.cmd new file mode 100644 index 000000000000..e07c4c312854 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Redirect/setup.cmd @@ -0,0 +1,3 @@ +@ECHO OFF +echo "This line goes to stdout" +echo "This line goes to stderr" 1>&2 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Redirect/setup.sh b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Redirect/setup.sh new file mode 100644 index 000000000000..1bbd8ac6cecd --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/Redirect/setup.sh @@ -0,0 +1,3 @@ +#!/bin/sh +echo "This line goes to stdout" +>&2 echo "This line goes to stderr" diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/RedirectOnError/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/RedirectOnError/.template.config/template.json new file mode 100644 index 000000000000..1b0230b3caea --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/RedirectOnError/.template.config/template.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.RunScript.RedirectOnError", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.RunScript.RedirectOnError", + "precedence": "100", + "identity": "TestAssets.PostActions.RunScript.RedirectOnError", + "shortName": "TestAssets.PostActions.RunScript.RedirectOnError", + "sourceName": "DoNotRedirect", + "postActions": [ + { + "condition": "(OS != \"Windows_NT\")", + "description": "Make scripts executable", + "manualInstructions": [ + { + "text": "Run 'chmod +x *.sh'" + } + ], + "actionId": "cb9a6cf3-4f5c-4860-b9d2-03a574959774", + "args": { + "+x": "*.sh" + }, + "continueOnError": false + }, + { + "condition": "(OS != \"Windows_NT\")", + "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", + "args": { + "executable": "setup.sh" + }, + "manualInstructions": [ + { + "text": "Run 'setup.sh'" + } + ], + "continueOnError": false, + "description ": "setups the project by calling setup.sh" + }, + { + "condition": "(OS == \"Windows_NT\")", + "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", + "args": { + "executable": "setup.cmd" + }, + "manualInstructions": [ + { + "text": "Run 'setup.cmd'" + } + ], + "continueOnError": false, + "description ": "setups the project by calling setup.cmd" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/RedirectOnError/setup.cmd b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/RedirectOnError/setup.cmd new file mode 100644 index 000000000000..e549e92e05ca --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/RedirectOnError/setup.cmd @@ -0,0 +1,4 @@ +@ECHO OFF +echo "This line goes to stdout" +echo "This line goes to stderr" 1>&2 +exit 1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/RedirectOnError/setup.sh b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/RedirectOnError/setup.sh new file mode 100644 index 000000000000..e8827d776f23 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/RunScript/RedirectOnError/setup.sh @@ -0,0 +1,4 @@ +#!/bin/sh +echo "This line goes to stdout" +>&2 echo "This line goes to stderr" +exit 1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/UnknownPostAction/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/UnknownPostAction/.template.config/template.json new file mode 100644 index 000000000000..2bf1e36ac4f4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/UnknownPostAction/.template.config/template.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.UnknownPostAction", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.UnknownPostAction", + "precedence": "100", + "identity": "TestAssets.PostActions.UnknownPostAction", + "shortName": "TestAssets.PostActions.UnknownPostAction", + "sourceName": "UnknownPostAction", + "primaryOutputs": [ + { + "path": "UnknownPostAction.csproj" + } + ], + "postActions": [ + { + "description": "This is not defined post action.", + "manualInstructions": [ + { + "text": "Run setup.cmd script manually." + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9027", + "continueOnError": false + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/UnknownPostAction/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/UnknownPostAction/Program.cs new file mode 100644 index 000000000000..0daa4cdd7ced --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/UnknownPostAction/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace UnknownPostAction +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/UnknownPostAction/UnknownPostAction.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/UnknownPostAction/UnknownPostAction.csproj new file mode 100644 index 000000000000..120e38c31501 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/UnknownPostAction/UnknownPostAction.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/WithFileRename/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/WithFileRename/.template.config/template.json new file mode 100644 index 000000000000..b9bf30f80195 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/WithFileRename/.template.config/template.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.PostActions.AddJsonProperty.WithSourceNameChangeInJson", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.PostActions.AddJsonProperty.WithSourceNameChangeInJson", + "precedence": "100", + "identity": "TestAssets.PostActions.AddJsonProperty.WithSourceNameChangeInJson", + "shortName": "TestAssets.PostActions.AddJsonProperty.WithSourceNameChangeInJson", + "tags": { "type": "project" }, + "sourceName": "MyTestProject", + "primaryOutputs": [ + { + "path": "MyTestProject.csproj" + } + ], + "postActions": [ + { + "description": "Modify JSON file", + "manualInstructions": [ { "text": "Add MyTestProject property to testfile.json manually." } ], + "args": { + "jsonFileName": "testfile.json", + "parentPropertyPath": "moduleConfiguration:edgeAgent:properties.desired:modules", + "newJsonPropertyName": "MyTestProject", + "newJsonPropertyValue": "${MODULEDIR<../MyTestProject>}" + }, + "applyFileRenamesToArgs": [ "newJsonPropertyName", "newJsonPropertyValue" ], + "applyFileRenamesToManualInstructions": true, + "actionId": "695A3659-EB40-4FF5-A6A6-C9C4E629FCB0", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/WithFileRename/MyTestProject.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/WithFileRename/MyTestProject.csproj new file mode 100644 index 000000000000..120e38c31501 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/WithFileRename/MyTestProject.csproj @@ -0,0 +1,8 @@ + + + + Exe + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/WithFileRename/testfile.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/WithFileRename/testfile.json new file mode 100644 index 000000000000..980dc39c20d2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/PostActions/WithFileRename/testfile.json @@ -0,0 +1,11 @@ +{ + "moduleConfiguration": { + "edgeAgent": { + "properties.desired": { + "modules": { + "module1": "someValue" + } + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceNameForms/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceNameForms/.template.config/template.json new file mode 100644 index 000000000000..3a398493af00 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceNameForms/.template.config/template.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "SourceNameForms", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.SourceNameForms", + "precedence": "100", + "identity": "TestAssets.SourceNameForms", + "shortName": "TestAssets.SourceNameForms", + "sourceName": "Good.Source.Name.1", + "symbols": { + "sourceNameLc": { + "type": "derived", + "valueSource": "name", + "valueTransform": "lc", + "replaces": "replace_name_lc", + "fileRename": "fileRename-name-lc" + } + }, + "forms": { + "lc": { + "identifier": "lowerCaseInvariant" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceNameForms/Good.Source.Name.1.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceNameForms/Good.Source.Name.1.cs new file mode 100644 index 000000000000..1d086aafd53c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceNameForms/Good.Source.Name.1.cs @@ -0,0 +1,6 @@ +Identity: Good.Source.Name.1 +Namespace: Good.Source.Name._1 +Class name: Good_Source_Name__1 +Namespace (lc): good.source.name._1 +Class name (lc): good_source_name__1 +Lowercase: replace_name_lc diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceNameForms/fileRename-name-lc2.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceNameForms/fileRename-name-lc2.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceNameForms/fileRename-name-lc2.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/With/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/With/.template.config/template.json new file mode 100644 index 000000000000..d7b3504f76d5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/With/.template.config/template.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "name": "Basic Template With Exclude", + "identity": "TestAssets.BasicTemplateWithExclude", + "shortName": "withexclude", + "sources": [ + { + "exclude": [ + "**/[Bb]in/**", + "**/[Oo]bj/**", + "**/.template.config/**", + "**/*.filelist", + "**/*.user" + ] + } + ], + "author": "", + "classifications": [], + "tags": { + "type": "project" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/With/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/With/bar.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/With/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/With/foo.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/With/foo.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/With/packages.lock.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/With/packages.lock.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/Without/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/Without/.template.config/template.json new file mode 100644 index 000000000000..46ff60238e78 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/Without/.template.config/template.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "name": "Basic Template Without Exclude", + "identity": "TestAssets.BasicTemplateWithoutExclude", + "shortName": "withoutexclude", + "author": "", + "classifications": [], + "tags": { + "type": "project" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/Without/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/Without/bar.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/Without/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/Without/foo.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/Without/foo.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/Without/packages.lock.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/SourceWithExcludeAndWithout/Without/packages.lock.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/.template.config/template.json new file mode 100644 index 000000000000..0d8dc0a3952a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/.template.config/template.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateConditionalProcessing", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateConditionalProcessing", + "precedence": "100", + "identity": "TestAssets.TemplateConditionalProcessing", + "shortName": "TestAssets.TemplateConditionalProcessing", + "symbols": { + "defaultFalse": { + "type": "parameter", + "dataType": "boolean", + "defaultValue": "false" + }, + "defaultTrue": { + "type": "parameter", + "dataType": "boolean", + "defaultValue": "true" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cmd b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cmd new file mode 100644 index 000000000000..816dadafb06a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cmd @@ -0,0 +1,11 @@ +rem #if defaultTrue +set type = "DefaultTrueIncluded" +rem #else +set type = "DefaultTrueExcluded" +rem #endif + +rem #if (defaultFalse) +set type = "DefaultFalseExcluded" +rem #else +set type = "DefaultFalseIncluded" +rem #endif \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cpp b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cpp new file mode 100644 index 000000000000..11b67a654207 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cpp @@ -0,0 +1,29 @@ +#include "stdafx.h" +#include +#include + +#if defaultTrue +class DefaultTrueIncluded { } +#else +class DefaultTrueExcluded { } +#endif + +//-:cnd:noEmit +#if defaultTrue +class InsideUnknownDirectiveNoEmit { } +#endif +//+:cnd:noEmit + +#if (defaultFalse) +class DefaultFalseExcluded { } +#else +class DefaultFalseIncluded { } +#endif + +// Without noEmit the following line will be emitted +//-:cnd +#if defaultFalse +class InsideUnknownDirectiveEmit { } +#endif +//+:cnd + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cs new file mode 100644 index 000000000000..b06682780d26 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cs @@ -0,0 +1,29 @@ +using MyApp.Test; + +namespace MyApp +{ +#if defaultTrue + public class DefaultTrueIncluded { } +#else + public class DefaultTrueExcluded { } +#endif + +//-:cnd:noEmit +#if defaultTrue + public class InsideUnknownDirectiveNoEmit { } +#endif +//+:cnd:noEmit + +#if (defaultFalse) + public class DefaultFalseExcluded { } +#else + public class DefaultFalseIncluded { } +#endif + +// Without noEmit the following line will be emitted +//-:cnd +#if defaultFalse + public class InsideUnknownDirectiveEmit { } +#endif +//+:cnd +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cshtml b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cshtml new file mode 100644 index 000000000000..e93b3067dddc --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.cshtml @@ -0,0 +1,11 @@ +@*#if defaultTrue +

DefaultTrueIncluded

+#else +

DefaultTrueExcluded

+#endif*@ + +@*#if defaultFalse +

DefaultFalseExcluded

+#else +

DefaultFalseIncluded

+#endif*@ \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.csproj new file mode 100644 index 000000000000..1f4e3296259a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.csproj @@ -0,0 +1,40 @@ + + + + netstandard1.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.css b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.css new file mode 100644 index 000000000000..0c7f02bd2db2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.css @@ -0,0 +1,23 @@ +a { + /*#if (defaultTrue) */ + DefaultTrueIncluded: 0; + /*#else*/ + DefaultTrueExcluded: 0; + /*#endif*/ +/*-:cnd:noEmit*/ + /*#if (defaultTrue) */ + InsideUnknownDirectiveNoEmit: 0; + /*#endif*/ +/*+:cnd:noEmit*/ + /*#if (defaultFalse) */ + DefaultFalseExcluded: 0; + /*#else*/ + DefaultFalseIncluded: 0; + /*#endif*/ +/*Without noEmit the following line will be emitted*/ +/*-:cnd*/ + /*#if (defaultFalse) */ + InsideUnknownDirectiveEmit: 0; + /*#endif*/ +/*+:cnd*/ +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.fs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.fs new file mode 100644 index 000000000000..03fcde15b664 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.fs @@ -0,0 +1,26 @@ +open System + +#if defaultTrue +type DefaultTrueIncluded() = do printfn "" +#else +type DefaultTrueExcluded() = do printfn "" +#endif + +//-:cnd:noEmit +#if defaultTrue +type InsideUnknownDirectiveNoEmit() = do printfn "" +#endif +//+:cnd:noEmit + +#if (defaultFalse) +type DefaultFalseExcluded() = do printfn "" +#else +type DefaultFalseIncluded() = do printfn "" +#endif + +// Without noEmit the following line will be emitted +//-:cnd +#if defaultFalse +type InsideUnknownDirectiveEmit() = do printfn "" +#endif +//+:cnd \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.haml b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.haml new file mode 100644 index 000000000000..8512c6a33721 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.haml @@ -0,0 +1,11 @@ +-##if defaultTrue + %p DefaultTrueIncluded +-##else + %p DefaultTrueExcluded +-##endif + +-##if defaultFalse + %p DefaultFalseExcluded +-##else + %p DefaultFalseIncluded +-##endif \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.js b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.js new file mode 100644 index 000000000000..2901008a3efe --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.js @@ -0,0 +1,24 @@ +//#if defaultTrue +function DefaultTrueIncluded() { } +//#else +function DefaultTrueExcluded() { } +//#endif + +//-:cnd:noEmit +//#if defaultTrue +function InsideUnknownDirectiveNoEmit() { } +//#endif +//+:cnd:noEmit + +//#if (defaultFalse) +function DefaultFalseExcluded() { } +//#else +function DefaultFalseIncluded() { } +//#endif + +// Without noEmit the following line will be emitted +//-:cnd +//#if defaultFalse +function InsideUnknownDirectiveEmit() { } +//#endif +//+:cnd \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.json new file mode 100644 index 000000000000..203a776f5014 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.json @@ -0,0 +1,53 @@ +{ + //#if defaultTrue + // This should not be uncommented + "DefaultTrueIncluded": 0, + //#else + "DefaultTrueExcluded": 0, + //#endif +//-:cnd:noEmit + //#if defaultTrue + "InsideUnknownDirectiveNoEmit": 0, + //#endif +//+:cnd:noEmit + //#if (defaultFalse) + "DefaultFalseExcluded": 0, + //#else + // This should not be uncommented + "DefaultFalseIncluded": 0, + //#endif +// Without noEmit the following line will be emitted +//-:cnd + //#if defaultFalse + "InsideUnknownDirectiveEmit": 0, + //#endif +//+:cnd + ////#if defaultTrue + //// This should be uncommented inside if + //"UncommentDefaultTrueIncluded": 0, + //#else + "NoUncommentDefaultTrueExcluded": 0, + //#endif + //#if (defaultFalse) + "NoUncommentDefaultFalseExcluded": 0, + ////#else + //// This should be uncommented inside else + //"UncommentDefaultFalseIncluded": 0, + ////#endif + + ////#if (!defaultTrue) + //// This should be uncommented inside if + //"UncommentNotDefaultTrueExcluded": 0, + //#else + // This should not be uncommented inside else + "NoUncommentNotDefaultTrueIncluded": 0, + //#endif + //#if (!defaultFalse) + // This should not be uncommented inside if + "NoUncommentNotDefaultFalseIncluded": 0, + ////#else + //// This should be uncommented inside else + //"UncommentNotDefaultFalseExcluded": 0, + ////#endif + "end": 0 +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.jsx b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.jsx new file mode 100644 index 000000000000..dbec5aa767a5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.jsx @@ -0,0 +1,15 @@ +const myElement = ( +
+ {/*#if defaultTrue +

DefaultTrueIncluded

+ #else +

DefaultTrueExcluded

+ #endif*/} + {/*#if defaultFalse +

DefaultFalseExcluded

+ #else +

DefaultFalseIncluded

+ #endif*/} +

I am a paragraph.

+
+ ); \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.othertype b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.othertype new file mode 100644 index 000000000000..7469a9168633 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.othertype @@ -0,0 +1,11 @@ +//#if defaultTrue + DefaultTrueIncluded +//#else + DefaultTrueExcluded +//#endif + +//#if defaultFalse + DefaultFalseExcluded +//#else + DefaultFalseIncluded +//#endif \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.ts b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.ts new file mode 100644 index 000000000000..c99d68b5fc2f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.ts @@ -0,0 +1,24 @@ +//#if defaultTrue +declare class DefaultTrueIncluded { } +//#else +declare class DefaultTrueExcluded { } +//#endif + +//-:cnd:noEmit +//#if defaultTrue +declare class InsideUnknownDirectiveNoEmit { } +//#endif +//+:cnd:noEmit + +//#if defaultFalse +declare class DefaultFalseExcluded { } +//#else +declare class DefaultFalseIncluded { } +//#endif + +// Without noEmit the following line will be emitted +//-:cnd +//#if defaultFalse +declare class InsideUnknownDirectiveEmit { } +//#endif +//+:cnd \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.vb b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.vb new file mode 100644 index 000000000000..3ff487c150b3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.vb @@ -0,0 +1,32 @@ +Module Module1 +'#If defaultTrue + Sub DefaultTrueIncluded() + End Sub +'#Else + Sub DefaultTrueExcluded() + End Sub +'#End If + +'-:cnd:noEmit +'#If defaultTrue + Sub InsideUnknownDirectiveNoEmit() + End Sub +'#End If +'+:cnd:noEmit + +'#If (defaultFalse) + Sub DefaultFalseExcluded() + End Sub +'#Else + Sub DefaultFalseIncluded() + End Sub +'#End If + +' Without noEmit the following line will be emitted +'-:cnd +'#If defaultFalse + Sub InsideUnknownDirectiveEmit() + End Sub +'#End If +'+:cnd +End Module \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.xml b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.xml new file mode 100644 index 000000000000..03974e9837e6 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.yml b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.yml new file mode 100644 index 000000000000..eca1b60d6b08 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateConditionalProcessing/Test.yml @@ -0,0 +1,11 @@ +#if defaultTrue +DefaultTrueIncluded +#else +DefaultTrueExcluded +#endif + +#if defaultFalse + DefaultFalseExcluded +#else + DefaultFalseIncluded +#endif \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/CSharpItemAuthor1/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/CSharpItemAuthor1/.template.config/template.json new file mode 100644 index 000000000000..8131a731281e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/CSharpItemAuthor1/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Author1", + "classifications": [ "Test Asset" ], + "name": "Basic FSharp", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateGrouping", + "precedence": "100", + "identity": "TestAssets.TemplateGrouping.CSharpItemAuthor1", + "shortName": "template-grouping", + "sourceName": "bar", + "tags": { + "language": "C#", + "type": "item" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/FSharpItemAuthor1/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/FSharpItemAuthor1/.template.config/template.json new file mode 100644 index 000000000000..2d55af073ede --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/FSharpItemAuthor1/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Author1", + "classifications": [ "Test Asset" ], + "name": "Basic FSharp", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateGrouping", + "precedence": "100", + "identity": "TestAssets.TemplateGrouping.FSharpItemAuthor1", + "shortName": "template-grouping", + "sourceName": "bar", + "tags": { + "language": "F#", + "type": "item" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/QSharpItemAuthor2/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/QSharpItemAuthor2/.template.config/template.json new file mode 100644 index 000000000000..5e4cc90118f1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/QSharpItemAuthor2/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Author2", + "classifications": [ "Test Asset" ], + "name": "Basic FSharp", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateGrouping", + "precedence": "100", + "identity": "TestAssets.TemplateGrouping.QSharpItemAuthor2", + "shortName": "template-grouping", + "sourceName": "bar", + "tags": { + "language": "Q#", + "type": "item" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/QSharpProjectAuthor2/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/QSharpProjectAuthor2/.template.config/template.json new file mode 100644 index 000000000000..7e6e46ef3eb0 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateGrouping/QSharpProjectAuthor2/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Author2", + "classifications": [ "Test Asset" ], + "name": "Basic FSharp", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateGrouping", + "precedence": "100", + "identity": "TestAssets.TemplateGrouping.QSharpProjectAuthor2", + "shortName": "template-grouping", + "sourceName": "bar", + "tags": { + "language": "Q#", + "type": "project" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/DifferentLanguagesGroup/BasicFSharp/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/DifferentLanguagesGroup/BasicFSharp/.template.config/template.json new file mode 100644 index 000000000000..4ac25509cb22 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/DifferentLanguagesGroup/BasicFSharp/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "Basic FSharp", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.DifferentLanguagesGroup", + "precedence": "100", + "identity": "TestAssets.DifferentLanguagesGroup.BasicFSharp", + "shortName": "basic", + "sourceName": "bar", + "tags": { + "language": "F#", + "type": "item" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/DifferentLanguagesGroup/BasicFSharp/bar.fs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/DifferentLanguagesGroup/BasicFSharp/bar.fs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/DifferentLanguagesGroup/BasicVB/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/DifferentLanguagesGroup/BasicVB/.template.config/template.json new file mode 100644 index 000000000000..72c815cfc676 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/DifferentLanguagesGroup/BasicVB/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "Basic VB", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.DifferentLanguagesGroup", + "precedence": "100", + "identity": "TestAssets.DifferentLanguagesGroup.BasicVB", + "shortName": "basic", + "sourceName": "bar", + "tags": { + "language": "VB", + "type": "item" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/DifferentLanguagesGroup/BasicVB/bar.vb b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/DifferentLanguagesGroup/BasicVB/bar.vb new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SamePrecedenceGroup/BasicTemplate1/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SamePrecedenceGroup/BasicTemplate1/.template.config/template.json new file mode 100644 index 000000000000..b1f999772327 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SamePrecedenceGroup/BasicTemplate1/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "Basic Template", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.SamePrecedenceGroup", + "precedence": "100", + "identity": "TestAssets.SamePrecedenceGroup.BasicTemplate1", + "shortName": "basic", + "sourceName": "bar", + "tags": { + "language": "C#", + "type": "item" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SamePrecedenceGroup/BasicTemplate1/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SamePrecedenceGroup/BasicTemplate1/bar.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SamePrecedenceGroup/BasicTemplate2/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SamePrecedenceGroup/BasicTemplate2/.template.config/template.json new file mode 100644 index 000000000000..a18bd4baa101 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SamePrecedenceGroup/BasicTemplate2/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "Basic Template", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.SamePrecedenceGroup", + "precedence": "100", + "identity": "TestAssets.SamePrecedenceGroup.BasicTemplate2", + "shortName": "basic", + "sourceName": "bar", + "tags": { + "language": "C#", + "type": "item" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SamePrecedenceGroup/BasicTemplate2/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SamePrecedenceGroup/BasicTemplate2/bar.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SameShortName/BasicFSharp/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SameShortName/BasicFSharp/.template.config/template.json new file mode 100644 index 000000000000..32541231956d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SameShortName/BasicFSharp/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "Basic FSharp", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.Group1", + "precedence": "100", + "identity": "TestAssets.DifferentLanguagesGroup.BasicFSharp", + "shortName": "basic", + "sourceName": "bar", + "tags": { + "language": "F#", + "type": "item" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SameShortName/BasicFSharp/bar.fs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SameShortName/BasicFSharp/bar.fs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SameShortName/BasicVB/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SameShortName/BasicVB/.template.config/template.json new file mode 100644 index 000000000000..fa3ffb0ffb14 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SameShortName/BasicVB/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "Basic VB", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.Group2", + "precedence": "100", + "identity": "TestAssets.DifferentLanguagesGroup.BasicVB", + "shortName": "basic", + "sourceName": "bar", + "tags": { + "language": "VB", + "type": "item" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SameShortName/BasicVB/bar.vb b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateResolution/SameShortName/BasicVB/bar.vb new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBinaryFile/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBinaryFile/.template.config/template.json new file mode 100644 index 000000000000..afab5b3bfe59 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBinaryFile/.template.config/template.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithBinaryFile", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithBinaryFile", + "precedence": "100", + "identity": "TestAssets.TemplateWithBinaryFile", + "shortName": "TestAssets.TemplateWithBinaryFile" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBinaryFile/image.png b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBinaryFile/image.png new file mode 100644 index 000000000000..f16da5ff3d98 Binary files /dev/null and b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBinaryFile/image.png differ diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/.template.config/template.json new file mode 100644 index 000000000000..3f56d0ffd9ed --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/.template.config/template.json @@ -0,0 +1,86 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithBooleanParameters", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithBooleanParameters", + "precedence": "100", + "identity": "TestAssets.TemplateWithBooleanParameters", + "shortName": "TestAssets.TemplateWithBooleanParameters", + "tags": {"type": "project"}, + "symbols": { + "A": { + "type": "parameter", + "dataType": "bool", + "replaces": "VAL_A" + }, + "B": { + "type": "parameter", + "dataType": "bool", + "defaultValue": "false", + "replaces": "VAL_B" + }, + "C": { + "type": "parameter", + "dataType": "bool", + "defaultValue": "true", + "replaces": "VAL_C" + }, + "D": { + "type": "parameter", + "dataType": "bool", + "defaultValue": "true", + "defaultIfOptionWithoutValue": "false", + "replaces": "VAL_D" + }, + "A-computed-1": { + "type": "computed", + "value": "A" + }, + "B-computed-1": { + "type": "computed", + "value": "B" + }, + "C-computed-1": { + "type": "computed", + "value": "C" + }, + "D-computed-1": { + "type": "computed", + "value": "D" + }, + "A-computed-2": { + "type": "computed", + "value": "A == true" + }, + "B-computed-2": { + "type": "computed", + "value": "B == true" + }, + "C-computed-2": { + "type": "computed", + "value": "C == true" + }, + "D-computed-2": { + "type": "computed", + "value": "D == true" + }, + "A-computed-3": { + "type": "computed", + "value": "A == \"true\"" + }, + "B-computed-3": { + "type": "computed", + "value": "B == \"true\"" + }, + "C-computed-3": { + "type": "computed", + "value": "C == \"true\"" + }, + "D-computed-3": { + "type": "computed", + "value": "D == \"true\"" + } + + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar-computed.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar-computed.cs new file mode 100644 index 000000000000..73e7b2f66ad9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar-computed.cs @@ -0,0 +1,72 @@ +//CPP2 processing +//Values: +var A = VAL_A; +var B = VAL_B; +var C = VAL_C; +var D = VAL_D; + +//computed - 1: "value": "A" +#if (A-computed-1) +var A1 = "true"; +#else +var A1 = "false"; +#endif +#if (B-computed-1) +var B1 = "true"; +#else +var B1 = "false"; +#endif +#if (C-computed-1) +var C1 = "true"; +#else +var C1 = "false"; +#endif +#if (D-computed-1) +var D1 = "true"; +#else +var D1 = "false"; +#endif +//computed - 2: "value": "A == true" +#if (A-computed-2) +var A2 = "true"; +#else +var A2 = "false"; +#endif +#if (B-computed-2) +var B2 = "true"; +#else +var B2 = "false"; +#endif +#if (C-computed-2) +var C2 = "true"; +#else +var C2 = "false"; +#endif +#if (D-computed-2) +var D2 = "true"; +#else +var D2 = "false"; +#endif +//computed - 3: "A == \"true\"" +#if (A-computed-3) +var A3 = "true"; +#else +var A3 = "false"; +#endif +#if (B-computed-3) +var B3 = "true"; +#else +var B3 = "false"; +#endif +#if (C-computed-3) +var C3 = "true"; +#else +var C3 = "false"; +#endif +#if (D-computed-3) +var D3 = "true"; +#else +var D3 = "false"; +#endif + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar.cs new file mode 100644 index 000000000000..b0bb2d597bcd --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar.cs @@ -0,0 +1,72 @@ +//CPP processing +//Values: +var A = VAL_A; +var B = VAL_B; +var C = VAL_C; +var D = VAL_D; + +// if (A) syntax +#if (A) +var A = "true"; +#else +var A = "false"; +#endif +#if (B) +var B = "true"; +#else +var B = "false"; +#endif +#if (C) +var C = "true"; +#else +var C = "false"; +#endif +#if (D) +var D = "true"; +#else +var D = "false"; +#endif + +// if (A == true) syntax +#if (A == true) +var A = "true"; +#else +var A = "false"; +#endif +#if (B == true) +var B = "true"; +#else +var B = "false"; +#endif +#if (C == true) +var C = "true"; +#else +var C = "false"; +#endif +#if (D == true) +var D = "true"; +#else +var D = "false"; +#endif + +// if (A == "true") syntax +#if (A == "true") +var A = "true"; +#else +var A = "false"; +#endif +#if (B == "true") +var B = "true"; +#else +var B = "false"; +#endif +#if (C == "true") +var C = "true"; +#else +var C = "false"; +#endif +#if (D == "true") +var D = "true"; +#else +var D = "false"; +#endif diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar.props b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar.props new file mode 100644 index 000000000000..29520a63a536 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar.props @@ -0,0 +1,89 @@ + + +VAL_A +VAL_B +VAL_C +VAL_D + + +A-true +B-true +C-true +D-true + +A-true +B-true +C-true +D-true + +A-true +B-true +C-true +D-true + + + + A-true + + A-false + + +B-true + +B-false + + +C-true + +C-false + + +D-true + +D-false + + + + +A-true + +A-false + + +B-true + +B-false + + +C-true + +C-false + + +D-true + +D-false + + + + + +A-true + +A-false + + +B-true + +B-false + + +C-true + +C-false + + +D-true + +D-false + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar.vb b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar.vb new file mode 100644 index 000000000000..1be6d1d8a538 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBooleanParameters/bar.vb @@ -0,0 +1,73 @@ +'VB processing +'Values: +var A = VAL_A; +var B = VAL_B; +var C = VAL_C; +var D = VAL_D; + + +'( A ) syntax +'#If( A ) +var A = "true"; +'#Else +var A = "false"; +'#End If +'#If( B ) +var B = "true"; +'#Else +var B = "false"; +'#End If +'#If( C ) +var C = "true"; +'#Else +var C = "false"; +'#End If +'#If( D ) +var D = "true"; +'#Else +var D = "false"; +'#End If + +'( A == true) syntax +'#If( A == true) +var A = "true"; +'#Else +var A = "false"; +'#End If +'#If( B == true) +var B = "true"; +'#Else +var B = "false"; +'#End If +'#If( C == true) +var C = "true"; +'#Else +var C = "false"; +'#End If +'#If( D == true) +var D = "true"; +'#Else +var D = "false"; +'#End If + +'( A == "true") syntax +'#If( A == "true") +var A = "true"; +'#Else +var A = "false"; +'#End If +'#If( B == "true") +var B = "true"; +'#Else +var B = "false"; +'#End If +'#If( C == "true") +var C = "true"; +'#Else +var C = "false"; +'#End If +'#If( D == "true") +var D = "true"; +'#Else +var D = "false"; +'#End If diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBrokenGeneratedInComputed/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBrokenGeneratedInComputed/.template.config/template.json new file mode 100644 index 000000000000..622ff46fa3f1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBrokenGeneratedInComputed/.template.config/template.json @@ -0,0 +1,83 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.TemplateWithBrokenGeneratedInComputed", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithBrokenGeneratedInComputed", + "precedence": "100", + "identity": "TestAssets.TemplateWithBrokenGeneratedInComputed", + "shortName": "TestAssets.TemplateWithBrokenGeneratedInComputed", + "defaultName": "theDefaultName", + "symbols": { + "preset": { + "displayName": "Preset", + "type": "parameter", + "datatype": "choice", + "defaultValue": "recommended", + "description": "Selects setup type", + "choices": [ + { + "choice": "recommended", + "description": "Recommended set of options to create a production-ready app targeting multiple platforms", + "displayName": "Default" + }, + { + "choice": "blank", + "description": "Smallest set of options, with no extra dependencies, to create an app targeting multiple platforms", + "displayName": "Blank" + } + ] + }, + "navigation": { + "displayName": "Navigation", + "description": "Configures navigation in the application", + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "regions", + "displayName": "Regions", + "description": "Uses Uno.Extensions.Navigation to navigate using regions" + }, + { + "choice": "blank", + "displayName": "Blank", + "description": "Provides Blank App experience with default WinUI Frame Navigation" + } + ] + }, + "dependencyInjection": { + "displayName": "Dependency Injection", + "description": "Use dependency injection for registering and accessing services", + "type": "parameter", + "datatype": "bool" + }, + "navigationEvaluator": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "navigation", + "fallbackVariableName": "presetNavigationDefault" + } + }, + "useDependencyInjection": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "dependencyInjection", + "fallbackVariableName": "presetDependencyInjectionDefault" + } + }, + "useExtensionsNavigation": { + "type": "computed", + "datatype": "bool", + "value": "useDependencyInjection && navigationEvaluator != 'blank'" + }, + "useFrameNav": { + "type": "computed", + "datatype": "bool", + "value": "!useDependencyInjection || navigationEvaluator == 'blank'" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBrokenGeneratedInComputed/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBrokenGeneratedInComputed/MyProject.Helper.csproj new file mode 100644 index 000000000000..85c927ce5e8a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBrokenGeneratedInComputed/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBrokenGeneratedInComputed/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBrokenGeneratedInComputed/Program.cs new file mode 100644 index 000000000000..7d0f1c875783 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithBrokenGeneratedInComputed/Program.cs @@ -0,0 +1,22 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { +#if (useExtensionsNavigation) + Console.WriteLine("Use extensions navigation."); +#else + Console.WriteLine("Don't use extensions navigation."); +#endif + +#if (useFrameNav) + Console.WriteLine("Use frame navigation."); +#else + Console.WriteLine("Don't use frame navigation."); +#endif + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/.template.config/template.json new file mode 100644 index 000000000000..0222d2d3ce2c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/.template.config/template.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithCaseSensitiveNameBasedRenames", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithCaseSensitiveNameBasedRenames", + "precedence": "100", + "identity": "TestAssets.TemplateWithCaseSensitiveNameBasedRenames", + "shortName": "TestAssets.TemplateWithCaseSensitiveNameBasedRenames", + "sourceName": "RenamePart", + "primaryOutputs": [ + { + "path": "Norenamepart/FileNorenamepart.txt" + }, + { + "path": "Norenamepart/FileYesRenamePart.txt" + }, + { + "path": "YesRenamePart/FileNorenamepart.txt" + }, + { + "path": "YesRenamePart/FileYesRenamePart.txt" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileNorenamepart.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileNorenamepart.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileNorenamepart.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileYesRenamePart.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileYesRenamePart.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/Norenamepart/FileYesRenamePart.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/YesRenamePart/FileNorenamepart.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/YesRenamePart/FileNorenamepart.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/YesRenamePart/FileNorenamepart.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/YesRenamePart/FileYesRenamePart.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/YesRenamePart/FileYesRenamePart.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCaseSensitiveNameBasedRenames/YesRenamePart/FileYesRenamePart.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCircleDependencyInMacros/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCircleDependencyInMacros/.template.config/template.json new file mode 100644 index 000000000000..0175cc1bc661 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCircleDependencyInMacros/.template.config/template.json @@ -0,0 +1,42 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.TemplateWithCircleDependencyInMacros", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithCircleDependencyInMacros", + "precedence": "100", + "identity": "TestAssets.TemplateWithCircleDependencyInMacros", + "shortName": "TestAssets.TemplateWithCircleDependencyInMacros", + "defaultName": "theDefaultName", + "symbols": { + "switchCheck": { + "type": "generated", + "generator": "switch", + "datatype": "string", + "parameters": { + "evaluator": "C++", + "cases": [ + { + "condition": "(switchCheck2 == 'regions')", + "value": "regions" + } + ] + } + }, + "switchCheck2": { + "type": "generated", + "generator": "switch", + "datatype": "string", + "parameters": { + "evaluator": "C++", + "cases": [ + { + "condition": "(switchCheck == 'regions')", + "value": "regions" + } + ] + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCircleDependencyInMacros/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCircleDependencyInMacros/MyProject.Helper.csproj new file mode 100644 index 000000000000..85c927ce5e8a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCircleDependencyInMacros/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCircleDependencyInMacros/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCircleDependencyInMacros/Program.cs new file mode 100644 index 000000000000..7d0f1c875783 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCircleDependencyInMacros/Program.cs @@ -0,0 +1,22 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { +#if (useExtensionsNavigation) + Console.WriteLine("Use extensions navigation."); +#else + Console.WriteLine("Don't use extensions navigation."); +#endif + +#if (useFrameNav) + Console.WriteLine("Use frame navigation."); +#else + Console.WriteLine("Don't use frame navigation."); +#endif + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCliHostFile/.template.config/dotnetcli.host.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCliHostFile/.template.config/dotnetcli.host.json new file mode 100644 index 000000000000..0a745bcbd060 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCliHostFile/.template.config/dotnetcli.host.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "test-param": { + "isHidden": "true", + "shortName": "p", + "longName": "param" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCliHostFile/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCliHostFile/.template.config/template.json new file mode 100644 index 000000000000..000be11dfdf1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCliHostFile/.template.config/template.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithCliHostFile", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithCliHostFile", + "precedence": "100", + "identity": "TestAssets.TemplateWithCliHostFile", + "shortName": "TestAssets.TemplateWithCliHostFile", + "sourceName": "bar", + "symbols": { + "test-param": { + "type": "parameter", + "description": "some description", + "datatype": "choice", + "choices": [ + { + "choice": "ch1", + "displayName": "Choice 1" + }, + { + "choice": "ch2", + "displayName": "Choice 2" + } + ], + "replaces": "replacetext", + "defaultValue": "ch1", + "displayName": "User Parameter" + } + }, + "primaryOutputs": [ + { + "path": "bar.cs" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCliHostFile/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCliHostFile/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithCliHostFile/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInDerivedThroughGenerated/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInDerivedThroughGenerated/.template.config/template.json new file mode 100644 index 000000000000..d4aadb2d9b2e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInDerivedThroughGenerated/.template.config/template.json @@ -0,0 +1,120 @@ +{ + "author": "Test Asset", + "classifications": [ + "Test Asset" + ], + "name": "TestAssets.TemplateWithComputedInDerivedThroughGenerated", + "tags": { + "type": "project" + }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithComputedInDerivedThroughGenerated", + "precedence": "100", + "identity": "TestAssets.TemplateWithComputedInDerivedThroughGenerated", + "shortName": "TestAssets.TemplateWithComputedInDerivedThroughGenerated", + "defaultName": "theDefaultName", + "symbols": { + "preset": { + "displayName": "Preset", + "type": "parameter", + "datatype": "choice", + "defaultValue": "recommended", + "description": "Selects setup type", + "choices": [ + { + "choice": "recommended", + "description": "Recommended set of options to create a production-ready app targeting multiple platforms", + "displayName": "Default" + }, + { + "choice": "blank", + "description": "Smallest set of options, with no extra dependencies, to create an app targeting multiple platforms", + "displayName": "Blank" + } + ] + }, + "navigation": { + "displayName": "Navigation", + "description": "Configures navigation in the application", + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "regions", + "displayName": "Regions", + "description": "Uses Uno.Extensions.Navigation to navigate using regions" + }, + { + "choice": "blank", + "displayName": "Blank", + "description": "Provides Blank App experience with default WinUI Frame Navigation" + } + ] + }, + "presetDependencyInjectionDefault": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "false" + } + }, + "presetNavigationDefault": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "blank" + } + }, + "dependencyInjection": { + "displayName": "Dependency Injection", + "description": "Use dependency injection for registering and accessing services", + "type": "parameter", + "datatype": "bool" + }, + "navigationEvaluator": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "navigation", + "fallbackVariableName": "presetNavigationDefault" + } + }, + "useDependencyInjection": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "dependencyInjection", + "fallbackVariableName": "presetDependencyInjectionDefault" + } + }, + "useFrameNav": { + "type": "computed", + "datatype": "bool", + "value": "!useDependencyInjection || navigationEvaluator == 'blank'" + }, + "joinMacroTest": { + "type": "generated", + "generator": "join", + "replaces": "%VAL%", + "parameters": { + "symbols": [ + { + "type": "const", + "value": "STRINGFORJOIN" + }, + { + "type": "ref", + "value": "useFrameNav" + } + ] + } + }, + "testDerived": { + "type": "derived", + "valueSource": "joinMacroTest", + "valueTransform": "lowerCaseInvariant", + "fileRename": "Program", + "description": "A value derived from the 'joinMacroTest' param, used to rename Program.cs" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInDerivedThroughGenerated/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInDerivedThroughGenerated/MyProject.Helper.csproj new file mode 100644 index 000000000000..85c927ce5e8a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInDerivedThroughGenerated/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInDerivedThroughGenerated/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInDerivedThroughGenerated/Program.cs new file mode 100644 index 000000000000..2cbe99d25c1d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInDerivedThroughGenerated/Program.cs @@ -0,0 +1,16 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { +#if (useFrameNav) + Console.WriteLine($"Use frame navigation. JoinMacroTest: %VAL%"); +#else + Console.WriteLine($"Don't use frame navigation."); +#endif + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInGenerated/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInGenerated/.template.config/template.json new file mode 100644 index 000000000000..4b019c7168d8 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInGenerated/.template.config/template.json @@ -0,0 +1,113 @@ +{ + "author": "Test Asset", + "classifications": [ + "Test Asset" + ], + "name": "TestAssets.TemplateWithComputedInGenerated", + "tags": { + "type": "project" + }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithComputedInGenerated", + "precedence": "100", + "identity": "TestAssets.TemplateWithComputedInGenerated", + "shortName": "TestAssets.TemplateWithComputedInGenerated", + "defaultName": "theDefaultName", + "symbols": { + "preset": { + "displayName": "Preset", + "type": "parameter", + "datatype": "choice", + "defaultValue": "recommended", + "description": "Selects setup type", + "choices": [ + { + "choice": "recommended", + "description": "Recommended set of options to create a production-ready app targeting multiple platforms", + "displayName": "Default" + }, + { + "choice": "blank", + "description": "Smallest set of options, with no extra dependencies, to create an app targeting multiple platforms", + "displayName": "Blank" + } + ] + }, + "navigation": { + "displayName": "Navigation", + "description": "Configures navigation in the application", + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "regions", + "displayName": "Regions", + "description": "Uses Uno.Extensions.Navigation to navigate using regions" + }, + { + "choice": "blank", + "displayName": "Blank", + "description": "Provides Blank App experience with default WinUI Frame Navigation" + } + ] + }, + "presetDependencyInjectionDefault": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "false" + } + }, + "presetNavigationDefault": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "blank" + } + }, + "dependencyInjection": { + "displayName": "Dependency Injection", + "description": "Use dependency injection for registering and accessing services", + "type": "parameter", + "datatype": "bool" + }, + "navigationEvaluator": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "navigation", + "fallbackVariableName": "presetNavigationDefault" + } + }, + "useDependencyInjection": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "dependencyInjection", + "fallbackVariableName": "presetDependencyInjectionDefault" + } + }, + "useFrameNav": { + "type": "computed", + "datatype": "bool", + "value": "!useDependencyInjection || navigationEvaluator == 'blank'" + }, + "joinMacroTest": { + "type": "generated", + "generator": "join", + "replaces": "%VAL%", + "parameters": { + "symbols": [ + { + "type": "const", + "value": "stringforjoin" + }, + { + "type": "ref", + "value": "useFrameNav" + } + ] + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInGenerated/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInGenerated/MyProject.Helper.csproj new file mode 100644 index 000000000000..85c927ce5e8a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInGenerated/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInGenerated/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInGenerated/Program.cs new file mode 100644 index 000000000000..2cbe99d25c1d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithComputedInGenerated/Program.cs @@ -0,0 +1,16 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { +#if (useFrameNav) + Console.WriteLine($"Use frame navigation. JoinMacroTest: %VAL%"); +#else + Console.WriteLine($"Don't use frame navigation."); +#endif + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditionalParameters/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditionalParameters/.template.config/template.json new file mode 100644 index 000000000000..5fed8038da98 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditionalParameters/.template.config/template.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithConditionalParameters", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithConditionalParameters", + "precedence": "100", + "identity": "TestAssets.TemplateWithConditionalParameters", + "shortName": "TestAssets.TemplateWithConditionalParameters", + "symbols": { + "paramA": { + "type": "parameter", + "datatype": "string", + "isEnabled": "A_enabled", + "isRequired": true, + "replaces": "placeholderA" + }, + "paramB": { + "type": "parameter", + "datatype": "string", + "isEnabled": "B_enabled", + "isRequired": true, + "replaces": "placeholderB" + }, + "A_enabled": { + "type": "parameter", + "datatype": "bool" + }, + "B_enabled": { + "type": "parameter", + "datatype": "bool" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditionalParameters/Test.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditionalParameters/Test.cs new file mode 100644 index 000000000000..0de65665c311 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditionalParameters/Test.cs @@ -0,0 +1,11 @@ + +// value of paramA: placeholderA +// value of paramB: placeholderB + +//#if( paramA ) + // A is enabled +//#endif + +//#if( paramB ) + // B is enabled +//#endif diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.dockerignore b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.dockerignore new file mode 100644 index 000000000000..c1a1b6e50b8c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.dockerignore @@ -0,0 +1,9 @@ +#if (A) +# comment foo +foo +#endif +##if (B) +## comment bar +#bar +#endif +baz diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.editorconfig b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.editorconfig new file mode 100644 index 000000000000..c1a1b6e50b8c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.editorconfig @@ -0,0 +1,9 @@ +#if (A) +# comment foo +foo +#endif +##if (B) +## comment bar +#bar +#endif +baz diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.gitattributes b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.gitattributes new file mode 100644 index 000000000000..506efc1e28c9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.gitattributes @@ -0,0 +1,9 @@ +#if (A) +# comment foo +foo +#endif +##if (B) +## comment bar +#bar +#endif +baz diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.gitignore b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.gitignore new file mode 100644 index 000000000000..c1a1b6e50b8c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.gitignore @@ -0,0 +1,9 @@ +#if (A) +# comment foo +foo +#endif +##if (B) +## comment bar +#bar +#endif +baz diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.template.config/template.json new file mode 100644 index 000000000000..918ed5ab3db9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/.template.config/template.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithConditions", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithConditions", + "precedence": "100", + "identity": "TestAssets.TemplateWithConditions", + "shortName": "TestAssets.TemplateWithConditions", + "symbols": { + "A": { + "type": "parameter", + "dataType": "bool", + "defaultValue": "false" + }, + "B": { + "type": "parameter", + "dataType": "bool", + "defaultValue": "false" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/Dockerfile b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/Dockerfile new file mode 100644 index 000000000000..c1a1b6e50b8c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/Dockerfile @@ -0,0 +1,9 @@ +#if (A) +# comment foo +foo +#endif +##if (B) +## comment bar +#bar +#endif +baz diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/Package.appxmanifest b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/Package.appxmanifest new file mode 100644 index 000000000000..5f4ebcedf4c1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/Package.appxmanifest @@ -0,0 +1,41 @@ + + + + + + + + UWP App Example + Microsoft Corporation + Assets\StoreLogo-sdk.png + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/nuget.config b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/nuget.config new file mode 100644 index 000000000000..70125a73910d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/nuget.config @@ -0,0 +1,9 @@ + + +foo + + +baz diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.axaml b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.axaml new file mode 100644 index 000000000000..2ffa8a2d8520 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.cake b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.cake new file mode 100644 index 000000000000..c3097539d3be --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.cake @@ -0,0 +1,9 @@ +#if (A) +// comment foo +foo +#endif +#if (B) +// comment bar +bar +#endif +baz diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.md b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.md new file mode 100644 index 000000000000..70125a73910d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.md @@ -0,0 +1,9 @@ + + +foo + + +baz diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.ps1 b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.ps1 new file mode 100644 index 000000000000..2e38c58b22b2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.ps1 @@ -0,0 +1,9 @@ +#if (A) +# comment foo +A true +#endif +##if (B) +## comment B true +#B true +#endif +common text diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.sln b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.sln new file mode 100644 index 000000000000..c1a1b6e50b8c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.sln @@ -0,0 +1,9 @@ +#if (A) +# comment foo +foo +#endif +##if (B) +## comment bar +#bar +#endif +baz diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.slnx b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.slnx new file mode 100644 index 000000000000..70125a73910d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.slnx @@ -0,0 +1,9 @@ + + +foo + + +baz diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.yaml b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.yaml new file mode 100644 index 000000000000..c1a1b6e50b8c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithConditions/test.yaml @@ -0,0 +1,9 @@ +#if (A) +# comment foo +foo +#endif +##if (B) +## comment bar +#bar +#endif +baz diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolFileRename/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolFileRename/.template.config/template.json new file mode 100644 index 000000000000..377f6c2cc5b5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolFileRename/.template.config/template.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithDerivedSymbolFileRename", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithDerivedSymbolFileRename", + "precedence": "100", + "identity": "TestAssets.TemplateWithDerivedSymbolFileRename", + "shortName": "TestAssets.TemplateWithDerivedSymbolFileRename", + "sourceName": "Company.Web.Application1", + "symbols": { + "DerivedRename": { + "description": "The final part of a multi-dotted name", + "FileRename": "Application1", + "type": "derived", + "valueSource": "name", + "valueTransform": "AfterLastDot" + } + }, + "Forms": { + "AfterLastDot": { + "identifier": "replace", + "pattern": "^.*\\.(?=[^\\.]+$)", // match everything up to and including the final "." + "replacement": "" + } + }, + "primaryOutputs": [ + { + "path": "Application1.cs" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolFileRename/Application1.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolFileRename/Application1.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolWithValueForms/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolWithValueForms/.template.config/template.json new file mode 100644 index 000000000000..406c9c3668c9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolWithValueForms/.template.config/template.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithDerivedSymbolWithValueForms", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithDerivedSymbolWithValueForms", + "precedence": "100", + "identity": "TestAssets.TemplateWithDerivedSymbolWithValueForms", + "shortName": "TestAssets.TemplateWithDerivedSymbolWithValueForms", + "sourceName": "Company.Web.SomeApp1", + "symbols": { + "DerivedRename": { + "description": "The final part of a multi-dotted name", + "FileRename": "SomeApp1", + "type": "derived", + "valueSource": "name", + "valueTransform": "AfterLastDot", + "forms": { + "global": [ "identity", "ChainAS" ] + }, + "replaces": "SomeApp1" + } + }, + "Forms": { + "AfterLastDot": { + "identifier": "replace", + "pattern": "^.*\\.(?=[^\\.]+$)", // match everything up to and including the final "." + "replacement": "" + }, + "ABecomesZ": { + "identifier": "replace", + "pattern": "A", + "replacement": "Z" + }, + "SBecomesDollar": { + "identifier": "replace", + "pattern": "S", + "replacement": "$" + }, + "ChainAS": { + "identifier": "chain", + "steps": [ "ABecomesZ", "SBecomesDollar"] + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolWithValueForms/ContentTest.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolWithValueForms/ContentTest.txt new file mode 100644 index 000000000000..2d3e4590a8f4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolWithValueForms/ContentTest.txt @@ -0,0 +1,2 @@ +SomeApp1 +$omeZpp1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolWithValueForms/SomeApp1.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithDerivedSymbolWithValueForms/SomeApp1.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithFileRenameDate/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithFileRenameDate/.template.config/template.json new file mode 100644 index 000000000000..905a2ef3ec11 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithFileRenameDate/.template.config/template.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.TemplateWithFileRenameDate", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithFileRenameDate", + "precedence": "100", + "identity": "TestAssets.TemplateWithFileRenameDate", + "shortName": "TestAssets.TemplateWithFileRenameDate", + "symbols": { + "migrationName": { + "type": "parameter", + "datatype": "text", + "isRequired": true + }, + "nameLower": { + "type": "generated", + "generator": "casing", + "parameters": { + "source": "migrationName", + "toLower": true + }, + "fileRename": "name" + }, + "date": { + "type": "generated", + "generator": "now", + "parameters": { + "format": "yyyyMMdd" + }, + "fileRename": "date" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithFileRenameDate/date_name.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithFileRenameDate/date_name.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithFileRenameDate/date_name.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/.template.config/template.json new file mode 100644 index 000000000000..1a304ec7a49d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/.template.config/template.json @@ -0,0 +1,46 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateGeneratedSymbolWithRefToDerivedSymbol", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder", + "precedence": "100", + "identity": "TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder", + "shortName": "TestAssets.TemplateGenSymWithRefToDerivedSym_DiffOrder", + "symbols": { + "NugetToolName": { + "type": "parameter", + "datatype": "text", + "isRequired": true, + "description": "tool name" + }, + "PackageName": { + "type": "generated", + "generator": "join", + "FileRename": "ToolExtension", + "replaces": "PackageName_SpaceHolder", + "parameters": { + "symbols": [ + { + "type": "ref", + "value": "ToolNameCapitalCase" + }, + { + "type": "const", + "value": "Frameworks" + } + ], + "separator": "." + } + }, + "ToolNameCapitalCase": { + "type": "derived", + "datatype": "text", + "valueSource": "NugetToolName", + "valueTransform": "firstUpperCase", + "FileRename": "Tool", + "replaces": "Tool" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/MyProject.Helper.csproj new file mode 100644 index 000000000000..ff96b2dbc74e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/MyProject.Helper.csproj @@ -0,0 +1,11 @@ + + + + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/Tool.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/Tool.cs new file mode 100644 index 000000000000..ffe45d916b2e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/Tool.cs @@ -0,0 +1,8 @@ +using System; + +namespace MyProject.Con +{ + public class Tool + { + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/ToolExtension.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/ToolExtension.cs new file mode 100644 index 000000000000..74d550f28120 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGenSymbolWithRefToDerivedSymbol_DifferentOrder/ToolExtension.cs @@ -0,0 +1,8 @@ +using System; + +namespace MyProject.Con +{ + public class ToolExtensions + { + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedInComputed/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedInComputed/.template.config/template.json new file mode 100644 index 000000000000..b2645ee7b33b --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedInComputed/.template.config/template.json @@ -0,0 +1,97 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.TemplateWithGeneratedInComputed", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithGeneratedInComputed", + "precedence": "100", + "identity": "TestAssets.TemplateWithGeneratedInComputed", + "shortName": "TestAssets.TemplateWithGeneratedInComputed", + "defaultName": "theDefaultName", + "symbols": { + "preset": { + "displayName": "Preset", + "type": "parameter", + "datatype": "choice", + "defaultValue": "recommended", + "description": "Selects setup type", + "choices": [ + { + "choice": "recommended", + "description": "Recommended set of options to create a production-ready app targeting multiple platforms", + "displayName": "Default" + }, + { + "choice": "blank", + "description": "Smallest set of options, with no extra dependencies, to create an app targeting multiple platforms", + "displayName": "Blank" + } + ] + }, + "navigation": { + "displayName": "Navigation", + "description": "Configures navigation in the application", + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "regions", + "displayName": "Regions", + "description": "Uses Uno.Extensions.Navigation to navigate using regions" + }, + { + "choice": "blank", + "displayName": "Blank", + "description": "Provides Blank App experience with default WinUI Frame Navigation" + } + ] + }, + "presetDependencyInjectionDefault": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "false" + } + }, + "presetNavigationDefault": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "blank" + } + }, + "dependencyInjection": { + "displayName": "Dependency Injection", + "description": "Use dependency injection for registering and accessing services", + "type": "parameter", + "datatype": "bool" + }, + "navigationEvaluator": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "navigation", + "fallbackVariableName": "presetNavigationDefault" + } + }, + "useDependencyInjection": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "dependencyInjection", + "fallbackVariableName": "presetDependencyInjectionDefault" + } + }, + "useExtensionsNavigation": { + "type": "computed", + "datatype": "bool", + "value": "useDependencyInjection && navigationEvaluator != 'blank'" + }, + "useFrameNav": { + "type": "computed", + "datatype": "bool", + "value": "!useDependencyInjection || navigationEvaluator == 'blank'" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedInComputed/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedInComputed/MyProject.Helper.csproj new file mode 100644 index 000000000000..85c927ce5e8a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedInComputed/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedInComputed/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedInComputed/Program.cs new file mode 100644 index 000000000000..7d0f1c875783 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedInComputed/Program.cs @@ -0,0 +1,22 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { +#if (useExtensionsNavigation) + Console.WriteLine("Use extensions navigation."); +#else + Console.WriteLine("Don't use extensions navigation."); +#endif + +#if (useFrameNav) + Console.WriteLine("Use frame navigation."); +#else + Console.WriteLine("Don't use frame navigation."); +#endif + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSwitchInComputed/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSwitchInComputed/.template.config/template.json new file mode 100644 index 000000000000..997943708b21 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSwitchInComputed/.template.config/template.json @@ -0,0 +1,96 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TestAssets.TemplateWithGeneratedSwitchInComputed", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithGeneratedSwitchInComputed", + "precedence": "100", + "identity": "TestAssets.TemplateWithGeneratedSwitchInComputed", + "shortName": "TestAssets.TemplateWithGeneratedSwitchInComputed", + "defaultName": "theDefaultName", + "symbols": { + "preset": { + "displayName": "Preset", + "type": "parameter", + "datatype": "choice", + "defaultValue": "recommended", + "description": "Selects setup type", + "choices": [ + { + "choice": "recommended", + "description": "Recommended set of options to create a production-ready app targeting multiple platforms", + "displayName": "Default" + }, + { + "choice": "blank", + "description": "Smallest set of options, with no extra dependencies, to create an app targeting multiple platforms", + "displayName": "Blank" + } + ] + }, + "navigation": { + "displayName": "Navigation", + "description": "Configures navigation in the application", + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "choice": "regions", + "displayName": "Regions", + "description": "Uses Uno.Extensions.Navigation to navigate using regions" + }, + { + "choice": "blank", + "displayName": "Blank", + "description": "Provides Blank App experience with default WinUI Frame Navigation" + } + ] + }, + "dependencyInjection": { + "displayName": "Dependency Injection", + "description": "Use dependency injection for registering and accessing services", + "type": "parameter", + "datatype": "bool" + }, + "switchCheck": { + "type": "generated", + "generator": "switch", + "datatype": "string", + "parameters": { + "evaluator": "C++", + "cases": [ + { + "condition": "(useDependencyInjection && navigationEvaluator != 'blank')", + "value": "regions" + }, + { + "condition": "(!useDependencyInjection || navigationEvaluator == 'blank')", + "value": "blank" + } + ] + } + }, + "navigationEvaluator": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "navigation", + "fallbackVariableName": "presetNavigationDefault" + } + }, + "useDependencyInjection": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "dependencyInjection", + "fallbackVariableName": "presetDependencyInjectionDefault" + } + }, + "useExtensionsNavigation": { + "type": "computed", + "datatype": "bool", + "value": "switchCheck != 'blank'" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSwitchInComputed/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSwitchInComputed/MyProject.Helper.csproj new file mode 100644 index 000000000000..85c927ce5e8a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSwitchInComputed/MyProject.Helper.csproj @@ -0,0 +1,7 @@ + + + + net7.0 + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSwitchInComputed/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSwitchInComputed/Program.cs new file mode 100644 index 000000000000..d93422db78b5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSwitchInComputed/Program.cs @@ -0,0 +1,16 @@ +using System; + +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { +#if (useExtensionsNavigation) + Console.WriteLine("Use extensions navigation."); +#else + Console.WriteLine("Don't use extensions navigation."); +#endif + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/.template.config/template.json new file mode 100644 index 000000000000..dc8e7b0681dc --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/.template.config/template.json @@ -0,0 +1,46 @@ +{ + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateGeneratedSymbolWithRefToDerivedSymbol", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateGenSymWithRefToDerivedSym", + "precedence": "100", + "identity": "TestAssets.TemplateGenSymWithRefToDerivedSym", + "shortName": "TestAssets.TemplateGenSymWithRefToDerivedSym", + "symbols": { + "NugetToolName": { + "type": "parameter", + "datatype": "text", + "isRequired": true, + "description": "tool name" + }, + "ToolNameCapitalCase": { + "type": "derived", + "datatype": "text", + "valueSource": "NugetToolName", + "valueTransform": "firstUpperCase", + "FileRename": "Tool", + "replaces": "Tool" + }, + "PackageName": { + "type": "generated", + "generator": "join", + "FileRename": "ToolExtension", + "replaces": "PackageName_SpaceHolder", + "parameters": { + "symbols": [ + { + "type": "ref", + "value": "ToolNameCapitalCase" + }, + { + "type": "const", + "value": "Frameworks" + } + ], + "separator": "." + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/MyProject.Helper.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/MyProject.Helper.csproj new file mode 100644 index 000000000000..ff96b2dbc74e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/MyProject.Helper.csproj @@ -0,0 +1,11 @@ + + + + net7.0 + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/Tool.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/Tool.cs new file mode 100644 index 000000000000..ffe45d916b2e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/Tool.cs @@ -0,0 +1,8 @@ +using System; + +namespace MyProject.Con +{ + public class Tool + { + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/ToolExtension.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/ToolExtension.cs new file mode 100644 index 000000000000..74d550f28120 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithGeneratedSymbolWithRefToDerivedSymbol/ToolExtension.cs @@ -0,0 +1,8 @@ +using System; + +namespace MyProject.Con +{ + public class ToolExtensions + { + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithJoinAndFolderRename/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithJoinAndFolderRename/.template.config/template.json new file mode 100644 index 000000000000..b96535b8405f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithJoinAndFolderRename/.template.config/template.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithJoinAndFolderRename", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithJoinAndFolderRename", + "precedence": "100", + "identity": "TestAssets.TemplateWithJoinAndFolderRename", + "shortName": "TestAssets.TemplateWithJoinAndFolderRename", + "symbols": { + "company": { + "type": "parameter", + "dataType": "string", + "defaultValue": "Microsoft" + }, + "product": { + "type": "parameter", + "dataType": "string", + "defaultValue": "Visual Studio" + }, + "joinedRename": { + "type": "generated", + "generator": "join", + "fileRename": "Api", + "parameters": { + "symbols": [ + { + "type": "const", + "value": "Source" + }, + { + "type": "const", + "value": "Api" + }, + { + "type": "ref", + "value": "company" + }, + { + "type": "ref", + "value": "product" + } + ], + "separator": "/" + } + } + }, + "primaryOutputs": [ + { + "path": "Api/bar.cs" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithJoinAndFolderRename/Api/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithJoinAndFolderRename/Api/bar.cs new file mode 100644 index 000000000000..b3b9332a37ef --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithJoinAndFolderRename/Api/bar.cs @@ -0,0 +1 @@ +Content is not relevant \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/.template.config/localize/templatestrings.de-DE.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/.template.config/localize/templatestrings.de-DE.json new file mode 100644 index 000000000000..14667dc1ea8e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/.template.config/localize/templatestrings.de-DE.json @@ -0,0 +1,15 @@ +{ + "name": "name_de-DE:äÄßöÖüÜ", + "description": "desc_de-DE:äÄßöÖüÜ", + "symbols/someSymbol/displayName": "sym0_displayName_de-DE:äÄßöÖüÜ", + "symbols/someSymbol/description": "sym0_desc_de-DE:äÄßöÖüÜ", + "symbols/someChoice/description": "sym1_desc_de-DE:äÄßöÖüÜ", + "symbols/someChoice/choices/choice0/description": "sym1_choice0_de-DE:äÄßöÖüÜ", + "symbols/someChoice/choices/choice1/description": "sym1_choice1_de-DE:äÄßöÖüÜ", + "postActions/pa0/description": "pa0_desc_de-DE:äÄßöÖüÜ", + "postActions/pa0/manualInstructions/first_instruction/text": "pa0_manualInstructions_de-DE:äÄßöÖüÜ", + "postActions/pa1/description": "pa1_desc_de-DE:äÄßöÖüÜ", + "postActions/pa1/manualInstructions/third_instruction/text": "pa1_manualInstructions1_de-DE:äÄßöÖüÜ", + "postActions/pa2/description": "pa2_desc_de-DE:äÄßöÖüÜ", + "postActions/pa2/manualInstructions/default/text": "pa2_manualInstructions_de-DE:äÄßöÖüÜ" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/.template.config/localize/templatestrings.tr.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/.template.config/localize/templatestrings.tr.json new file mode 100644 index 000000000000..f20e1586e890 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/.template.config/localize/templatestrings.tr.json @@ -0,0 +1,14 @@ +{ + "name": "name_tr-TR:çÇğĞıIİöÖşŞüÜ", + "description": "desc_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someSymbol/displayName": "sym0_displayName_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someSymbol/description": "sym0_desc_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someChoice/description": "sym1_desc_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someChoice/displayName": "sym1_displayName_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someChoice/choices/choice0/description": "sym1_choice0_tr-TR:çÇğĞıIİöÖşŞüÜ", + "symbols/someChoice/choices/choice2/description": "sym1_choice2_tr-TR:çÇğĞıIİöÖşŞüÜ", + "postActions/pa0/description": "pa0_desc_tr-TR:çÇğĞıIİöÖşŞüÜ", + "postActions/pa0/manualInstructions/first_instruction/text": "pa0_manualInstructions_tr-TR:çÇğĞıIİöÖşŞüÜ", + "postActions/pa1/description": "pa1_desc_tr-TR:çÇğĞıIİöÖşŞüÜ", + "postActions/pa1/manualInstructions/first_instruction/text": "pa1_manualInstructions0_tr-TR:çÇğĞıIİöÖşŞüÜ" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/.template.config/template.json new file mode 100644 index 000000000000..de6a904cee9f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/.template.config/template.json @@ -0,0 +1,97 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "name", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithLocalization", + "precedence": "100", + "identity": "TestAssets.TemplateWithLocalization", + "shortName": "TestAssets.TemplateWithLocalization", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "bar", + "description": "desc", + "symbols": { + "someSymbol": { + "displayName": "sym0_displayName", + "description": "sym0_desc", + "type": "parameter", + "datatype": "string", + "defaultValue": "RegularDefaultString", + "replaces": "OriginalString", + "DefaultIfOptionWithoutValue": "NoValueDefaultString" + }, + "someChoice": { + "type": "parameter", + "displayName": "sym1_displayName", + "description": "sym1_desc", + "datatype": "choice", + "choices": [ + { + "choice": "choice0", + "displayName": "sym1_choice0_displayName", + "description": "sym1_choice0" + }, + { + "choice": "choice1", + "displayName": "sym1_choice1_displayName", + "description": "sym1_choice1" + }, + { + "choice": "choice2", + "displayName": "sym1_choice2_displayName", + "description": "sym1_choice2" + } + ], + "replaces": "choice1", + "defaultValue": "choice1" + } + }, + "postActions": [ + { + "id": "pa0", + "description": "pa0_desc", + "manualInstructions": [ + { + "id": "first_instruction", + "text": "pa0_manualInstructions" + } + ], + "actionId": "58E1433C-303B-4713-8A47-26E7B03BD8A5", + "continueOnError": true + }, + { + "id": "pa1", + "description": "pa1_desc", + "manualInstructions": [ + { + "id": "first_instruction", + "text": "pa1_manualInstructions0" + }, + { + "id": "second_instruction", + "text": "pa1_manualInstructions1" + }, + { + "id": "third_instruction", + "text": "pa1_manualInstructions2" + } + ], + "actionId": "2B5CDBB7-1FAC-41EC-ADCC-08A73021BC40", + "continueOnError": true + }, + { + "id": "pa2", + "manualInstructions": [ + { + "text": "pa2_manualInstructions" + } + ], + "actionId": "58E1433C-303B-4713-8A47-26E7B03BD8A5", + "continueOnError": true + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithLocalization/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultiValueChoice/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultiValueChoice/.template.config/template.json new file mode 100644 index 000000000000..d8878c96f41a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultiValueChoice/.template.config/template.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithMultiValueChoice", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithMultiValueChoice", + "precedence": "100", + "identity": "TestAssets.TemplateWithMultiValueChoice", + "shortName": "TestAssets.TemplateWithMultiValueChoice", + "symbols": { + "Platform": { + "type": "parameter", + "description": "The target framework 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" + }, + "joinedRename": { + "type": "generated", + "generator": "join", + "replaces": "SupportedPlatforms", + "parameters": { + "symbols": [ + { + "type": "ref", + "value": "Platform" + } + ], + "separator": ", ", + "removeEmptyValues": true, + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultiValueChoice/Test.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultiValueChoice/Test.cs new file mode 100644 index 000000000000..5143fe803ed3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultiValueChoice/Test.cs @@ -0,0 +1,39 @@ + +// This file is generated for platfrom: SupportedPlatforms + +#if( Platform == "Windows" ) + // Quoted Windows +#else + // NOT Quoted Windows +#endif + + +#if( Platform == Windows ) + // Windows +#else + // NOT Windows +#endif + +#if( Platform == "MacOS" ) + // Quoted MacOS +#else + // NOT Quoted MacOS +#endif + +#if( Platform == MacOS ) + // MacOS +#else + // NOT MacOS +#endif + +#if( Platform == "iOS" ) + // Quoted iOS +#else + // NOT Quoted iOS +#endif + +#if( Platform == iOS ) + // iOS +#else + // NOT iOS +#endif diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndCoalesce/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndCoalesce/.template.config/template.json new file mode 100644 index 000000000000..77c80e34a28c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndCoalesce/.template.config/template.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithMultipleChoicesAndCoalesce", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithMultipleChoicesAndCoalesce", + "precedence": "100", + "identity": "TestAssets.TemplateWithMultipleChoicesAndCoalesce", + "shortName": "TestAssets.TemplateWithMultipleChoicesAndCoalesce", + "symbols": { + "preset": { + "displayName": "Preset", + "type": "parameter", + "datatype": "choice", + "defaultValue": "recommended", + "description": "Selects setup type", + "choices": [ + { + "choice": "recommended", + "description": "Recommended set of options to create a production-ready app targeting multiple platforms", + "displayName": "Default" + }, + { + "choice": "blank", + "description": "Smallest set of options, with no extra dependencies, to create an app targeting multiple platforms", + "displayName": "Blank" + } + ] + }, + "tests": { + "displayName": "Tests", + "type": "parameter", + "datatype": "choice", + "replaces": "user_selectedtests", + "allowMultipleValues": true, + "enableQuotelessLiterals": true, + "choices": [ + { + "choice": "unit", + "displayName": "Unit Tests" + }, + { + "choice": "ui", + "displayName": "UI Tests" + } + ] + }, + "presetTestsDefault": { + "type": "generated", + "generator": "switch", + "replaces": "preset_tests", + "parameters": { + "evaluator": "C++", + "datatype": "string", + "cases": [ + { + "condition": "(preset == 'recommended')", + "value": "unit|ui" + }, + { + "condition": "(preset == 'blank')", + "value": "" + } + ] + } + }, + "testsEvaluator": { + "type": "generated", + "generator": "coalesce", + "replaces": "final_tests", + "parameters": { + "sourceVariableName": "tests", + "fallbackVariableName": "presetTestsDefault" + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndCoalesce/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndCoalesce/Program.cs new file mode 100644 index 000000000000..c2868571c8b5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndCoalesce/Program.cs @@ -0,0 +1,12 @@ +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("Preset test is: preset_tests."); + Console.WriteLine("User's choice is: user_selectedtests."); + Console.WriteLine("The final set is: final_tests."); + } + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndPartialMatches/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndPartialMatches/.template.config/template.json new file mode 100644 index 000000000000..56a3baf4aaf1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndPartialMatches/.template.config/template.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithMultipleChoicesAndPartialMatches", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithMultipleChoicesAndPartialMatches", + "precedence": "100", + "identity": "TestAssets.TemplateWithMultipleChoicesAndPartialMatches", + "shortName": "TestAssets.TemplateWithMultipleChoicesAndPartialMatches", + "symbols": { + "tests": { + "displayName": "Tests", + "type": "parameter", + "datatype": "choice", + "replaces": "user_selectedtests", + "allowMultipleValues": true, + "enableQuotelessLiterals": true, + "choices": [ + { + "choice": "aab", + "displayName": "aab" + }, + { + "choice": "aaa", + "displayName": "aaa" + }, + { + "choice": "aa", + "displayName": "aa" + } + ] + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndPartialMatches/Program.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndPartialMatches/Program.cs new file mode 100644 index 000000000000..c6fa1ba813cf --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleChoicesAndPartialMatches/Program.cs @@ -0,0 +1,10 @@ +namespace ConsoleApp +{ + internal class Program + { + static void Main(string[] args) + { + Console.WriteLine("User's choice is: user_selectedtests."); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFile/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFile/.template.config/template.json new file mode 100644 index 000000000000..e92bda19f829 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFile/.template.config/template.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithMultipleRenamesOnSameFile", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithMultipleRenamesOnSameFile", + "precedence": "100", + "identity": "TestAssets.TemplateWithMultipleRenamesOnSameFile", + "shortName": "TestAssets.TemplateWithMultipleRenamesOnSameFile", + "symbols": { + "fooRename": { + "type": "parameter", + "dataType": "string", + "fileRename": "foo" + }, + "barRename": { + "type": "parameter", + "dataType": "string", + "fileRename": "bar" + } + }, + "primaryOutputs": [ + { + "path": "barandfoo.txt" + }, + { + "path": "foobar.txt" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFile/barandfoo.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFile/barandfoo.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFile/barandfoo.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFile/foobar.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFile/foobar.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFile/foobar.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap/.template.config/template.json new file mode 100644 index 000000000000..5c5a626a8e20 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap/.template.config/template.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap", + "precedence": "100", + "identity": "TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap", + "shortName": "TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap", + "symbols": { + "fooRename": { + "type": "parameter", + "dataType": "string", + "fileRename": "foo" + }, + "barRename": { + "type": "parameter", + "dataType": "string", + "fileRename": "bar" + } + }, + "primaryOutputs": [ + { + "path": "foo.txt" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap/foo.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap/foo.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesInducedOverlap/foo.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesOverlap/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesOverlap/.template.config/template.json new file mode 100644 index 000000000000..12c6d6c86b95 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesOverlap/.template.config/template.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithMultipleRenamesOnSameFileHandlesOverlap", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesOverlap", + "precedence": "100", + "identity": "TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesOverlap", + "shortName": "TestAssets.TemplateWithMultipleRenamesOnSameFileHandlesOverlap", + "symbols": { + "fooRename": { + "type": "parameter", + "dataType": "string", + "fileRename": "foo" + }, + "oobRename": { + "type": "parameter", + "dataType": "string", + "fileRename": "oob" + } + }, + "primaryOutputs": [ + { + "path": "foob.txt" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesOverlap/foob.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesOverlap/foob.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithMultipleRenamesOnSameFileHandlesOverlap/foob.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfForLocalhost/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfForLocalhost/.template.config/template.json new file mode 100644 index 000000000000..a72b6eb1bf3d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfForLocalhost/.template.config/template.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "name": "TestAssets.TemplateWithOnlyIfForLocalhost", + "tags": { "type": "project" }, + "classifications": [ "Test Asset" ], + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TTestAssets.TemplateWithOnlyIfForLocalhost", + "precedence": "100", + "identity": "TestAsset.TemplateWithOnlyIfForLocalhost", + "shortName": "TestAsset.TemplateWithOnlyIfForLocalhost", + "symbols": { + "default-port": { + "type": "parameter", + "datatype": "int", + "description": "The default port number. If not provided, a new random port will be found.", + "defaultValue": "0" + }, + "GeneratedPort": { + "type": "generated", + "generator": "port", + "datatype": "int", + "defaultValue": "100" + }, + "OverrideDefaultPort": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "default-port", + "fallbackVariableName": "GeneratedPort" + }, + "replaces": "12345", + "onlyIf": [ + { + "after": "localhost:" + } + ] + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfForLocalhost/test.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfForLocalhost/test.json new file mode 100644 index 000000000000..7290c3b8a772 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfForLocalhost/test.json @@ -0,0 +1,8 @@ +{ + "hostingManifest": { + "applicationUrl": "http://localhost:12345" + }, + "hostingManifest2": { + "applicationUrl": "http://check:12345" + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfStatement/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfStatement/.template.config/template.json new file mode 100644 index 000000000000..63f709242e4d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfStatement/.template.config/template.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "name": "TestAssets.TemplateWithOnlyIfStatement", + "tags": { "type": "project" }, + "classifications": [ "Test Asset" ], + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TTestAssets.emplateWithOnlyIfStatement", + "precedence": "100", + "identity": "TestAsset.TemplateWithOnlyIfStatement", + "shortName": "TestAsset.TemplateWithOnlyIfStatement", + "symbols": { + "default-port": { + "type": "parameter", + "datatype": "int", + "description": "The default port number. If not provided, a new random port will be found.", + "defaultValue": "0" + }, + "GeneratedPort": { + "type": "generated", + "generator": "port", + "datatype": "int", + "defaultValue": "100" + }, + "OverrideDefaultPort": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "default-port", + "fallbackVariableName": "GeneratedPort" + }, + "replaces": "12345", + "onlyIf": [ + { + "after": "\"defaultPort\": " + } + ] + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfStatement/test.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfStatement/test.json new file mode 100644 index 000000000000..ce237c7a4610 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithOnlyIfStatement/test.json @@ -0,0 +1,10 @@ +{ + "hostingManifest": { + "defaultPort": 12345, + "isPublic": false + }, + "hostingManifest_2": { + "dPort": 12345, + "isPublic": true + } +} \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPlaceholderFiles/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPlaceholderFiles/.template.config/template.json new file mode 100644 index 000000000000..a10636d66ad1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPlaceholderFiles/.template.config/template.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithPlaceholderFiles", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithPlaceholderFiles", + "precedence": "100", + "identity": "TestAssets.TemplateWithPlaceholderFiles", + "shortName": "TestAssets.TemplateWithPlaceholderFiles", + "sources": [ + { + "source": "./Src/Path/", + "target": "./Target/", + "exclude": [ "foo/**" ] + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPlaceholderFiles/Src/Path/bar/_._ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPlaceholderFiles/Src/Path/bar/_._ new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPlaceholderFiles/Src/Path/bar/_._ @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPlaceholderFiles/Src/Path/foo/_._ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPlaceholderFiles/Src/Path/foo/_._ new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPlaceholderFiles/Src/Path/foo/_._ @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPortsAndCoalesce/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPortsAndCoalesce/.template.config/template.json new file mode 100644 index 000000000000..43a009235af5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPortsAndCoalesce/.template.config/template.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithPortsAndCoalesce", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithPortsAndCoalesce", + "precedence": "100", + "identity": "TestAssets.TemplateWithPortsAndCoalesce", + "shortName": "TestAssets.TemplateWithPortsAndCoalesce", + "symbols": { + "userPort1": { + "type": "parameter", + "dataType": "integer" + }, + "generatedPort1": { + "type": "generated", + "generator": "port" + }, + "port1": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "userPort1", + "fallbackVariableName": "generatedPort1" + }, + "replaces": "1234" + }, + "userPort2": { + "type": "parameter", + "dataType": "integer" + }, + "generatedPort2": { + "type": "generated", + "generator": "port" + }, + "port2": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "userPort2", + "fallbackVariableName": "generatedPort2" + }, + "replaces": "1235" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPortsAndCoalesce/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPortsAndCoalesce/bar.cs new file mode 100644 index 000000000000..5c81eb78b55a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPortsAndCoalesce/bar.cs @@ -0,0 +1,2 @@ +The port is 1234 +The port is 1235 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultName/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultName/.template.config/template.json new file mode 100644 index 000000000000..3f4c6c6d0408 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultName/.template.config/template.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithPreferDefaultName", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithPreferDefaultName", + "identity": "TestAssets.TemplateWithPreferDefaultName", + "shortName": "TestAssets.TemplateWithPreferDefaultName", + "preferDefaultName": true, + "sourceName": "toChange", + "defaultName": "theDefaultName" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultName/toChange.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultName/toChange.cs new file mode 100644 index 000000000000..d43b1f381617 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultName/toChange.cs @@ -0,0 +1,3 @@ +using System; + +Console.log("Hello there! This is a test"); diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultNameButNoDefaultName/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultNameButNoDefaultName/.template.config/template.json new file mode 100644 index 000000000000..3b862c88dbfd --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultNameButNoDefaultName/.template.config/template.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithPreferDefaultName", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithPreferDefaultName", + "identity": "TestAssets.TemplateWithPreferDefaultName", + "shortName": "TestAssets.TemplateWithPreferDefaultName", + "preferDefaultName": true, + "sourceName": "toChange" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultNameButNoDefaultName/toChange.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultNameButNoDefaultName/toChange.cs new file mode 100644 index 000000000000..d43b1f381617 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithPreferDefaultNameButNoDefaultName/toChange.cs @@ -0,0 +1,3 @@ +using System; + +Console.log("Hello there! This is a test"); diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRegexMatchMacro/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRegexMatchMacro/.template.config/template.json new file mode 100644 index 000000000000..18cdba5e727a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRegexMatchMacro/.template.config/template.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithRegexMatchMacro", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithRegexMatchMacro", + "precedence": "100", + "identity": "TestAssets.TemplateWithRegexMatchMacro", + "shortName": "TestAssets.TemplateWithRegexMatchMacro", + "symbols": { + "isMatch": { + "type": "generated", + "generator": "regexMatch", + "dataType": "bool", + "replaces": "test.value1", + "parameters": { + "source": "name", + "pattern": "^hello$" + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRegexMatchMacro/bar.2.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRegexMatchMacro/bar.2.cs new file mode 100644 index 000000000000..2da2fd50f852 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRegexMatchMacro/bar.2.cs @@ -0,0 +1 @@ +test.value1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRegexMatchMacro/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRegexMatchMacro/bar.cs new file mode 100644 index 000000000000..2da2fd50f852 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRegexMatchMacro/bar.cs @@ -0,0 +1 @@ +test.value1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/.template.config/template.json new file mode 100644 index 000000000000..e8a2b89b3ea7 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/.template.config/template.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithRenames", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithRenames", + "precedence": "100", + "identity": "TestAssets.TemplateWithRenames", + "shortName": "TestAssets.TemplateWithRenames", + "symbols": { + "foo": { + "type": "parameter", + "dataType": "string", + "fileRename": "bar" + }, + "fooUC": { + "type": "generated", + "generator": "casing", + "parameters": { + "source": "foo", + "toLower": false + }, + "fileRename": "bar_uc" + }, + "testForms": { + "type": "parameter", + "dataType": "string", + "fileRename": "MyProject", + "forms": { + "global": [ "identity", "uc", "lc" ] + } + } + }, + "forms": { + "uc": { + "identifier": "uppercase" + }, + "lc": { + "identifier": "lowercase" + } + }, + "primaryOutputs": [ + { + "path": "MyProject1.cs" + }, + { + "path": "myproject2.cs" + }, + { + "path": "MYPROJECT3.cs" + }, + { + "path": "bar.cs" + }, + { + "path": "bar_uc.cs" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/MYPROJECT3.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/MYPROJECT3.cs new file mode 100644 index 000000000000..612586fd6399 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/MYPROJECT3.cs @@ -0,0 +1,9 @@ +using System; +using System.Text; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.TemplateWithRenames +{ + class MYPROJECT3 + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/MyProject1.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/MyProject1.cs new file mode 100644 index 000000000000..baaf3595e397 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/MyProject1.cs @@ -0,0 +1,9 @@ +using System; +using System.Text; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.TemplateWithRenames +{ + class MyProject1 + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/bar/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/bar/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/bar/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/myproject2.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/myproject2.cs new file mode 100644 index 000000000000..24525ffbe444 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/myproject2.cs @@ -0,0 +1,9 @@ +using System; +using System.Text; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.TemplateWithRenames +{ + class myproject2 + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/uc/bar_uc.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/uc/bar_uc.cs new file mode 100644 index 000000000000..b7029210ac04 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithRenames/uc/bar_uc.cs @@ -0,0 +1,9 @@ +using System; +using System.Text; + +namespace Microsoft.TemplateEngine.EndToEndTestHarness.test_templates.TemplateWithRenames +{ + class bar_uc + { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceBasedRenames/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceBasedRenames/.template.config/template.json new file mode 100644 index 000000000000..cc9d7bba62f2 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceBasedRenames/.template.config/template.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourceBasedRenames", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceBasedRenames", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceBasedRenames", + "shortName": "TestAssets.TemplateWithSourceBasedRenames", + "sourceName": "foo", + "sources": [ + { + "rename": { + "foo.cs": "bar.cs" + } + }, + { + "rename": { + "foo.cs": "baz.cs" + } + } + ], + "symbols": { + "barRename": { + "type": "parameter", + "dataType": "string", + "fileRename": "bar" + } + }, + "primaryOutputs": [ + { + "path": "bar.cs" + }, + { + "path": "baz.cs" + } + ] +} + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceBasedRenames/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceBasedRenames/foo.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceBasedRenames/foo.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceName/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceName/.template.config/template.json new file mode 100644 index 000000000000..bfdbe9da6363 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceName/.template.config/template.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourceName", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceName", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceName", + "shortName": "TestAssets.TemplateWithSourceName", + "sourceName": "bar", + "primaryOutputs": [ + { + "path": "bar.cs" + }, + { + "path": "bar/bar.cs" + }, + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceName/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceName/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceName/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceName/bar/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceName/bar/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceName/bar/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourceAndTargetPaths/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourceAndTargetPaths/.template.config/template.json new file mode 100644 index 000000000000..c88ad1956836 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourceAndTargetPaths/.template.config/template.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourceNameAndCustomSourceAndTargetPaths", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceNameAndCustomSourceAndTargetPaths", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceNameAndCustomSourceAndTargetPaths", + "shortName": "TestAssets.TemplateWithSourceNameAndCustomSourceAndTargetPaths", + "sourceName": "foo", + "sources": [ + { + "source": "./Src/Custom/Path/", + "target": "./Target/Output/" + } + ], + "primaryOutputs": [ + { + "path": "Target/Output/foo/foo.cs" + }, + { + "path": "Target/Output/foo.name.txt" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourceAndTargetPaths/Src/Custom/Path/foo.name.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourceAndTargetPaths/Src/Custom/Path/foo.name.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourceAndTargetPaths/Src/Custom/Path/foo.name.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourceAndTargetPaths/Src/Custom/Path/foo/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourceAndTargetPaths/Src/Custom/Path/foo/foo.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourceAndTargetPaths/Src/Custom/Path/foo/foo.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourcePath/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourcePath/.template.config/template.json new file mode 100644 index 000000000000..cb2a3eb643d7 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourcePath/.template.config/template.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourceNameAndCustomSourcePath", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceNameAndCustomSourcePath", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceNameAndCustomSourcePath", + "shortName": "TestAssets.TemplateWithSourceNameAndCustomSourcePath", + "sourceName": "foo", + "sources": [ + { + "source": "./Custom/Path/" + } + ], + "primaryOutputs": [ + { + "path": "foo/foo.cs" + }, + { + "path": "foo.name.txt" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourcePath/Custom/Path/foo.name.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourcePath/Custom/Path/foo.name.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourcePath/Custom/Path/foo.name.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourcePath/Custom/Path/foo/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomSourcePath/Custom/Path/foo/foo.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomTargetPath/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomTargetPath/.template.config/template.json new file mode 100644 index 000000000000..95750c699b4a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomTargetPath/.template.config/template.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourceNameAndCustomTargetPath", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceNameAndCustomTargetPath", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceNameAndCustomTargetPath", + "shortName": "TestAssets.TemplateWithSourceNameAndCustomTargetPath", + "sourceName": "foo", + "sources": [ + { + "target": "./Custom/Path/" + } + ], + "primaryOutputs": [ + { + "path": "Custom/Path/foo/foo.cs" + }, + { + "path": "Custom/Path/foo.name.txt" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomTargetPath/foo.name.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomTargetPath/foo.name.txt new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomTargetPath/foo.name.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomTargetPath/foo/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomTargetPath/foo/foo.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameAndCustomTargetPath/foo/foo.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameInTargetPathGetsRenamed/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameInTargetPathGetsRenamed/.template.config/template.json new file mode 100644 index 000000000000..f51aee450e7d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameInTargetPathGetsRenamed/.template.config/template.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourceNameInTargetPathGetsRenamed", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourceNameInTargetPathGetsRenamed", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourceNameInTargetPathGetsRenamed", + "shortName": "TestAssets.TemplateWithSourceNameInTargetPathGetsRenamed", + "sourceName": "foo", + "sources": [ + { + "source": "./", + "target": "./bar/foo/" + } + ], + "primaryOutputs": [ + { + "path": "bar/foo/foo.cs" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameInTargetPathGetsRenamed/foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourceNameInTargetPathGetsRenamed/foo.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourcePathOutsideConfigRoot/MountPointRoot/foo/bar/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourcePathOutsideConfigRoot/MountPointRoot/foo/bar/.template.config/template.json new file mode 100644 index 000000000000..afa22b8e8bba --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourcePathOutsideConfigRoot/MountPointRoot/foo/bar/.template.config/template.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithSourcePathOutsideConfigRoot", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithSourcePathOutsideConfigRoot", + "precedence": "100", + "identity": "TestAssets.TemplateWithSourcePathOutsideConfigRoot", + "shortName": "TestAssets.TemplateWithSourcePathOutsideConfigRoot", + "sourceName": "foo", + "sources": [ + { + "source": "../../../", // This is MountPointRoot + "target": "./blah/" + } + ], + "primaryOutputs": [ + { + "path": "blah/MountPointRoot/mount.foo.cs" + }, + { + "path": "blah/MountPointRoot/foo/foo.foo.cs" + }, + { + "path": "blah/MountPointRoot/foo/bar/bar.foo.cs" + } + ] +} + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourcePathOutsideConfigRoot/MountPointRoot/foo/bar/bar.foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourcePathOutsideConfigRoot/MountPointRoot/foo/bar/bar.foo.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourcePathOutsideConfigRoot/MountPointRoot/foo/foo.foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourcePathOutsideConfigRoot/MountPointRoot/foo/foo.foo.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourcePathOutsideConfigRoot/MountPointRoot/mount.foo.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithSourcePathOutsideConfigRoot/MountPointRoot/mount.foo.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithStringCoalesce/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithStringCoalesce/.template.config/template.json new file mode 100644 index 000000000000..685762c792a9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithStringCoalesce/.template.config/template.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithStringCoalesce", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithStringCoalesce", + "precedence": "100", + "identity": "TestAssets.TemplateWithStringCoalesce", + "shortName": "TestAssets.TemplateWithStringCoalesce", + "symbols": { + "userVal": { + "type": "parameter", + "dataType": "string", + "defaultIfOptionWithoutValue": "A" + }, + "generatedVal": { + "type": "generated", + "generator": "constant", + "parameters": { + "value": "fallback" + } + }, + "port1": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "userVal", + "fallbackVariableName": "generatedVal" + }, + "replaces": "%VAL%" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithStringCoalesce/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithStringCoalesce/bar.cs new file mode 100644 index 000000000000..870dd072e09c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithStringCoalesce/bar.cs @@ -0,0 +1 @@ +var str = "%VAL%"; diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithTags/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithTags/.template.config/template.json new file mode 100644 index 000000000000..32ee1bf13b22 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithTags/.template.config/template.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithTags", + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithTags", + "precedence": "100", + "identity": "TestAssets.TemplateWithTags", + "shortName": "TestAssets.TemplateWithTags", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "bar" +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithTags/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithTags/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithTags/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithUnspecifiedSourceName/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithUnspecifiedSourceName/.template.config/template.json new file mode 100644 index 000000000000..fffb6cf13081 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithUnspecifiedSourceName/.template.config/template.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithUnspecifiedSourceName", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithUnspecifiedSourceName", + "precedence": "100", + "identity": "TestAssets.TemplateWithUnspecifiedSourceName", + "shortName": "TestAssets.TemplateWithUnspecifiedSourceName", + "primaryOutputs": [ + { + "path": "bar.cs" + }, + { + "path": "bar/bar.cs" + } + ] +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithUnspecifiedSourceName/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithUnspecifiedSourceName/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithUnspecifiedSourceName/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithUnspecifiedSourceName/bar/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithUnspecifiedSourceName/bar/bar.cs new file mode 100644 index 000000000000..5f282702bb03 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithUnspecifiedSourceName/bar/bar.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/.template.config/template.json new file mode 100644 index 000000000000..4633a5896464 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/.template.config/template.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "TemplateWithValueForms", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.TemplateWithValueForms", + "precedence": "100", + "identity": "TestAssets.TemplateWithValueForms", + "shortName": "TestAssets.TemplateWithValueForms", + "symbols": { + "foo": { + "type": "parameter", + "dataType": "string", + "replaces": "test.value1", + "fileRename": "bar.2", + "forms": { + "global": [ "identity", "chained", "chained2", "dotToUnderscore" ] + } + }, + "param1": { + "type": "parameter", + "dataType": "string", + "replaces": "Param1TestValue", + "fileRename": "Param1TestValue", + "forms": { + "global": [ "identity", "first_lc", "kebab" ] + } + }, + "param2": { + "type": "parameter", + "dataType": "string", + "replaces": "param2TestValue", + "fileRename": "param2TestValue", + "forms": { + "global": [ "identity", "first_uc" ] + } + }, + "param3": { + "type": "parameter", + "dataType": "string", + "replaces": "param 3 test value", + "fileRename": "param 3 test value", + "forms": { + "global": [ "identity", "title" ] + } + } + }, + "forms": { + "chained": { + "identifier": "chain", + "steps": [ "dotToUnderscore", "digitToBang" ] + }, + "chained2": { + "identifier": "chain", + "steps": [ "dotToQuestionMark", "digitToBang" ] + }, + "digitToBang": { + "identifier": "replace", + "pattern": "\\d", + "replacement": "!" + }, + "dotToUnderscore": { + "identifier": "replace", + "pattern": "\\.", + "replacement": "_" + }, + "dotToQuestionMark": { + "identifier": "replace", + "pattern": "\\.", + "replacement": "?" + }, + "first_lc": { + "identifier": "firstLowerCaseInvariant" + }, + "first_uc": { + "identifier": "firstUpperCaseInvariant" + }, + "kebab": { + "identifier": "kebabCase" + }, + "title": { + "identifier": "titleCase" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/FirstLetterCaseForms/Param2TestValue.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/FirstLetterCaseForms/Param2TestValue.cs new file mode 100644 index 000000000000..954866c47654 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/FirstLetterCaseForms/Param2TestValue.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string Param2TestValue; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/FirstLetterCaseForms/param1TestValue.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/FirstLetterCaseForms/param1TestValue.cs new file mode 100644 index 000000000000..9f820bd82140 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/FirstLetterCaseForms/param1TestValue.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string param1TestValue; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/IdentityForms/Param1TestValue.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/IdentityForms/Param1TestValue.cs new file mode 100644 index 000000000000..efa6b67ae518 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/IdentityForms/Param1TestValue.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string Param1TestValue; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/IdentityForms/param 3 test value.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/IdentityForms/param 3 test value.cs new file mode 100644 index 000000000000..2858e4a7214f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/IdentityForms/param 3 test value.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string param3 = "param 3 test value"; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/IdentityForms/param2TestValue.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/IdentityForms/param2TestValue.cs new file mode 100644 index 000000000000..8f30cb18219e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/IdentityForms/param2TestValue.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string param2TestValue; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/KebabCaseForms/param-1-test-value.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/KebabCaseForms/param-1-test-value.cs new file mode 100644 index 000000000000..a4da2185fd93 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/KebabCaseForms/param-1-test-value.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string param-1-test-value; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/TitleCaseForms/Param 3 Test Value.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/TitleCaseForms/Param 3 Test Value.cs new file mode 100644 index 000000000000..381cf93e1d6d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/TitleCaseForms/Param 3 Test Value.cs @@ -0,0 +1,7 @@ +namespace Test +{ + class ContentReplacementTest + { + private string param3 = "Param 3 Test Value"; + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/bar.2.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/bar.2.cs new file mode 100644 index 000000000000..34bc7952e83a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/bar.2.cs @@ -0,0 +1,3 @@ +test.value1 +test_value! +test?value! \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/bar.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/bar.cs new file mode 100644 index 000000000000..34bc7952e83a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TemplateWithValueForms/bar.cs @@ -0,0 +1,3 @@ +test.value1 +test_value! +test?value! \ No newline at end of file diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TestTemplate/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TestTemplate/.template.config/template.json new file mode 100644 index 000000000000..1ec221b7b0a3 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TestTemplate/.template.config/template.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "SampleTestTemplate", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.SampleTestTemplate", + "precedence": "100", + "identity": "TestAssets.SampleTestTemplate", + "shortName": "TestAssets.SampleTestTemplate", + "symbols": { + "paramA": { + "type": "parameter", + "datatype": "string", + "replaces": "placeholderA", + "defaultValue": "false" + }, + "paramB": { + "type": "parameter", + "datatype": "string", + "isRequired": true, + "replaces": "placeholderB" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TestTemplate/Test.cs b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TestTemplate/Test.cs new file mode 100644 index 000000000000..0de65665c311 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/TestTemplate/Test.cs @@ -0,0 +1,11 @@ + +// value of paramA: placeholderA +// value of paramB: placeholderB + +//#if( paramA ) + // A is enabled +//#endif + +//#if( paramB ) + // B is enabled +//#endif diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbol/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbol/.template.config/template.json new file mode 100644 index 000000000000..1dfcfb3c6489 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbol/.template.config/template.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "ValueForms.DerivedSymbol", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.ValueForms.DerivedSymbol", + "precedence": "100", + "identity": "TestAssets.ValueForms.DerivedSymbol", + "shortName": "TestAssets.ValueForms.DerivedSymbol", + "sourceName": "My.Web.App", + "symbols": { + "nameUpperSnake": { + "type": "derived", + "valueSource": "name", + "valueTransform": "identity", //avoids transformation + "replaces": "My.Web.App", + "forms": { + "global": [ "UpperCaseForm", "UpperCaseSnake" ] + } + } + }, + "forms": { + "DotToSnake": { + "identifier": "replace", + "pattern": "\\.", + "replacement": "_" + }, + "UpperCaseForm": { + "identifier": "upperCase" + }, + "UpperCaseSnake": { + "identifier": "chain", + "steps": [ "UpperCaseForm", "DotToSnake" ] + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbol/My.Web.App.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbol/My.Web.App.txt new file mode 100644 index 000000000000..7a0b9a90747c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbol/My.Web.App.txt @@ -0,0 +1,3 @@ +My.Web.App +MY.WEB.APP +MY_WEB_APP diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbolFromGeneratedSymbol/.template.config/template.json b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbolFromGeneratedSymbol/.template.config/template.json new file mode 100644 index 000000000000..755e7ee93f5c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbolFromGeneratedSymbol/.template.config/template.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Test Asset", + "classifications": [ "Test Asset" ], + "name": "ValueForms.DerivedSymbolFromGeneratedSymbol", + "tags": { "type": "project" }, + "generatorVersions": "[1.0.0.0-*)", + "groupIdentity": "TestAssets.ValueForms.DerivedSymbolFromGeneratedSymbol", + "precedence": "100", + "identity": "TestAssets.ValueForms.DerivedSymbolFromGeneratedSymbol", + "shortName": "TestAssets.ValueForms.DerivedSymbolFromGeneratedSymbol", + "sourceName": "My.Web.App", + "symbols": { + "nameUpper": { + "description": "replaces content matching 'MY.WEB.APP' to the uppercased '{sourceName}', e.g. sourceName 'Foo.Bar' becomes 'FOO.BAR'", + "type": "generated", + "generator": "casing", + "parameters": { + "source": "name", + "toLower": false + }, + "replaces": "MY.WEB.APP" + }, + "nameUpperSnake": { + "description": "replaces content matching 'MY_WEB_APP' to the uppercased and underscored '{sourceName}', e.g. sourceName 'Foo.Bar' becomes 'FOO_BAR'", + "type": "derived", + "valueSource": "nameUpper", + "valueTransform": "DotToSnake", + "replaces": "MY_WEB_APP" + } + }, + "forms": { + "DotToSnake": { + "identifier": "replace", + "pattern": "\\.", + "replacement": "_" + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbolFromGeneratedSymbol/My.Web.App.txt b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbolFromGeneratedSymbol/My.Web.App.txt new file mode 100644 index 000000000000..7a0b9a90747c --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.TestTemplates/test_templates/ValueForms/DerivedSymbolFromGeneratedSymbol/My.Web.App.txt @@ -0,0 +1,3 @@ +My.Web.App +MY.WEB.APP +MY_WEB_APP diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/CombinedListTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/CombinedListTests.cs new file mode 100644 index 000000000000..4897b5af2f8a --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/CombinedListTests.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.TemplateEngine.Utils.UnitTests +{ + public class CombinedListTests + { + [Theory(DisplayName = nameof(VerifyCombinedListCombinesCorrectly))] + [InlineData(new int[] { }, new int[] { })] + [InlineData(new int[] { 5 }, new int[] { })] + [InlineData(new int[] { }, new int[] { 3 })] + [InlineData(new int[] { 1, 2 }, new int[] { })] + [InlineData(new int[] { }, new int[] { 1, 2 })] + [InlineData(new int[] { 1 }, new int[] { 1 })] + [InlineData(new int[] { 1 }, new int[] { 1, 2, 3 })] + [InlineData(new int[] { 1, 2, 3 }, new int[] { 1 })] + [InlineData(new int[] { 1, 2 }, new int[] { 1, 2, 3 })] + [InlineData(new int[] { 1, 2, 3, 4, 5 }, new int[] { 1, 2, 3 })] + + public void VerifyCombinedListCombinesCorrectly(IReadOnlyList listOne, IReadOnlyList listTwo) + { + CombinedList combined = new CombinedList(listOne, listTwo); + + List manuallyAppended = new List(); + manuallyAppended.AddRange(listOne); + manuallyAppended.AddRange(listTwo); + + Assert.Equal(combined.Count, manuallyAppended.Count); + + int enumerationCount = 0; + foreach (int value in combined) + { + enumerationCount++; + } + + Assert.Equal(enumerationCount, combined.Count); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/DefaultTemplatePackageProviderTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/DefaultTemplatePackageProviderTests.cs new file mode 100644 index 000000000000..a759217f79a9 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/DefaultTemplatePackageProviderTests.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Utils.UnitTests +{ + public class DefaultTemplatePackageProviderTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public DefaultTemplatePackageProviderTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: this.GetType().Name, virtualize: true); + } + + [Fact] + public async Task ReturnsFoldersAndNuPkgs() + { + var thisDir = Path.GetDirectoryName(typeof(DefaultTemplatePackageProviderTests).Assembly.Location); + //Pass in 5 folders + var folders = Directory.GetDirectories(Path.Combine(thisDir!, "..", "..", "..", "..", "..", "test", "Microsoft.TemplateEngine.TestTemplates", "test_templates")).Take(5); + //And one *.nupkg, but that folder contains 2 .nupkg files + var nupkgs = new[] { Path.Combine(thisDir!, "..", "..", "..", "..", "..", "test", "Microsoft.TemplateEngine.TestTemplates", "nupkg_templates", "*.nupkg") }; + + var provider = new DefaultTemplatePackageProvider(null!, _engineEnvironmentSettings, nupkgs, folders); + var sources = await provider.GetAllTemplatePackagesAsync(default); + + //Total should be 7 + Assert.Equal(7, sources.Count); + + Assert.True(sources[0].LastChangeTime > new DateTime(2000, 1, 1)); + Assert.False(string.IsNullOrWhiteSpace(sources[0].MountPointUri)); + Assert.Equal(provider, sources[0].Provider); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/DirectedGraphTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/DirectedGraphTests.cs new file mode 100644 index 000000000000..ad88af7039c1 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/DirectedGraphTests.cs @@ -0,0 +1,233 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Xunit; + +namespace Microsoft.TemplateEngine.Utils.UnitTests +{ + public class DirectedGraphTests + { + public static IEnumerable DirectedGraphHasCycleData() + { + List empty = new List(); + + yield return new object?[] { new Dictionary?>(), false, empty, empty }; + yield return new object?[] { new Dictionary?>() { { 5, null } }, false, empty, new List() { 5 } }; + yield return new object?[] { new Dictionary?>() { { 5, null }, { 6, null }, { 7, null } }, false, empty, new List() { 5, 6, 7 } }; + yield return new object?[] + { + new Dictionary?>() + { + { 5, new HashSet() { 6, 7 } }, { 6, new HashSet() { 7 } }, { 7, null } + }, + false, + empty, + new List() { 7, 6, 5 } + }; + yield return new object?[] + { + new Dictionary?>() + { + { 5, new HashSet() { 6, 7, 8, 9, 10, 11 } }, + { 6, new HashSet() { 20, 40, 50 } }, + { 7, new HashSet() { 20, 8 } }, + { 12, new HashSet() { 5, 13, 9, 20, 100 } }, + { 13, new HashSet() { 20, 8, 30, 5 } } + }, + false, + empty, + new List() { 8, 9, 10, 11, 20, 40, 50, 100, 30, 7, 6, 5, 13, 12 } + }; + + yield return new object?[] { new Dictionary?>() { { 5, new HashSet() { 5 } } }, true, new List() { 5, 5 }, empty }; + yield return new object?[] + { + new Dictionary?>() + { + { 5, new HashSet() { 6 } }, { 6, new HashSet() { 5 } } + }, + true, + new List() { 5, 6, 5 }, + empty + }; + yield return new object?[] + { + new Dictionary?>() + { + { 5, new HashSet() { 6, 7, 8, 9, 10, 11 } }, + { 6, new HashSet() { 20, 40, 50 } }, + { 7, new HashSet() { 20, 8 } }, + { 12, new HashSet() { 5, 13, 9, 20, 100 } }, + { 13, new HashSet() { 20, 8, 30, 5 } }, + { 40, new HashSet() { 5 } } + }, + true, + new List() { 5, 6, 40, 5 }, + empty + }; + yield return new object?[] + { + new Dictionary?>() + { + { 5, new HashSet() { 6, 7, 8, 9, 10, 11 } }, + { 6, new HashSet() { 20, 40, 50 } }, + { 7, new HashSet() { 20, 8 } }, + { 12, new HashSet() { 5, 13, 9, 20, 100 } }, + { 13, new HashSet() { 20, 8, 30, 5 } }, + { 40, new HashSet() { 6 } } + }, + true, + new List() { 6, 40, 6 }, + empty + }; + } + + [Theory] + [MemberData(nameof(DirectedGraphHasCycleData))] +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + public void HasCycleTests(Dictionary> dependencies, bool shouldHaveCycle, IReadOnlyList expectedCycle, IReadOnlyList unused) +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + { + new DirectedGraph(dependencies).HasCycle(out IReadOnlyList cycle).Should().Be(shouldHaveCycle); + cycle.Should().BeEquivalentTo(expectedCycle, options => options.WithStrictOrdering()); + //cycle.SequenceEqual(expectedCycle).Should().BeTrue(); + } + + [Theory] + [MemberData(nameof(DirectedGraphHasCycleData))] +#pragma warning disable xUnit1026 // Theory methods should use all of their parameters + public void EnumerateTopologicalSortTests(Dictionary> dependencies, bool shouldHaveCycle, IReadOnlyList unused, IReadOnlyList expectedOrder) +#pragma warning restore xUnit1026 // Theory methods should use all of their parameters + { + new DirectedGraph(dependencies).TryGetTopologicalSort(out IReadOnlyList order).Should().Be(!shouldHaveCycle); + if (!shouldHaveCycle) + { + order.Should().BeEquivalentTo(expectedOrder, options => options.WithStrictOrdering()); + } + } + + public static IEnumerable DirectedGraphSubgraphData() + { + HashSet empty = new HashSet(); + + Dictionary> graphA = new Dictionary>() + { + { 1, new HashSet() { 10, 20, 30 } }, + { 10, new HashSet() { 100, 200 } }, + { 20, new HashSet() { 200 } }, + { 30, new HashSet() { 300 } }, + { 300, new HashSet() { 1000, 2000 } }, + { 1000, new HashSet() { 20 } }, + }; + + Dictionary> graphB = new Dictionary>() + { + { 1, new HashSet() { 10, 20, 30 } }, + { 10, new HashSet() { 100 } }, + { 20, new HashSet() { 100 } }, + { 30, new HashSet() { 10 } }, + { 100, new HashSet() { 30 } }, + }; + + yield return new object?[] { graphA, new List() { 1 }, true, new Dictionary>() { { 1, empty } } }; + yield return new object?[] { graphA, new List() { 1 }, false, new Dictionary>() }; + yield return new object?[] { graphA, new List() { 10 }, true, new Dictionary>() { { 1, new HashSet() { 10 } }, { 10, empty } } }; + yield return new object?[] { graphA, new List() { 10 }, false, new Dictionary>() { { 1, empty } } }; + yield return new object?[] + { + graphA, + new List() { 20 }, + true, + new Dictionary>() + { + { 1, new HashSet() { 20, 30 } }, + { 1000, new HashSet() { 20 } }, + { 300, new HashSet() { 1000 } }, + { 30, new HashSet() { 300 } }, + { 20, empty } + } + }; + yield return new object?[] + { + graphA, + new List() { 20, 300, 30 }, + true, + new Dictionary>() + { + { 1, new HashSet() { 20, 30 } }, + { 1000, new HashSet() { 20 } }, + { 300, new HashSet() { 1000 } }, + { 30, new HashSet() { 300 } }, + { 20, empty } + } + }; + yield return new object?[] + { + graphA, + new List() { 20 }, + false, + new Dictionary>() + { + { 1, new HashSet() { 30 } }, + { 1000, new HashSet() }, + { 300, new HashSet() { 1000 } }, + { 30, new HashSet() { 300 } }, + } + }; + yield return new object?[] + { + graphA, + new List() { 20, 300, 30 }, + false, + new Dictionary>() + { + { 1, new HashSet() { 30 } }, + { 1000, new HashSet() }, + { 300, new HashSet() { 1000 } }, + { 30, new HashSet() { 300 } }, + } + }; + yield return new object?[] + { + graphA, + new List() { 100, 30 }, + false, + new Dictionary>() + { + { 1, new HashSet() { 10 } }, + { 10, empty }, + } + }; + + yield return new object?[] { graphB, new List() { 10 }, true, graphB }; + yield return new object?[] { graphB, new List() { 10 }, false, graphB }; + yield return new object?[] { graphB, new List() { 10, 100 }, true, graphB }; + yield return new object?[] { graphB, new List() { 10, 100 }, false, graphB }; + yield return new object?[] { graphB, new List() { 1 }, true, new Dictionary>() { { 1, empty } } }; + yield return new object?[] { graphB, new List() { 1 }, false, new Dictionary>() }; + + yield return new object?[] { graphB, new List() { 20 }, true, new Dictionary>() { { 1, new HashSet() { 20 } }, { 20, empty } } }; + yield return new object?[] { graphB, new List() { 20 }, false, new Dictionary>() { { 1, empty } } }; + } + + [Theory] + [MemberData(nameof(DirectedGraphSubgraphData))] + public void GetSubGraphDependandOnVerticesTests(Dictionary> dependencies, IReadOnlyList vertices, bool includeSeedVertices, Dictionary> expectedResult) + { + var result = new DirectedGraph(dependencies).GetSubGraphDependentOnVertices(vertices, includeSeedVertices); + TestAreEquivalent(result, expectedResult); + } + + private static void TestAreEquivalent(DirectedGraph actual, Dictionary> expected) + { + actual.DependenciesMap.Keys.Should() + .BeEquivalentTo(expected.Keys, options => options.WithoutStrictOrdering()); + + foreach (var item in expected) + { + actual.DependenciesMap[item.Key].Should().BeEquivalentTo(item.Value, options => options.WithoutStrictOrdering()); + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/EqualityExtensionsTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/EqualityExtensionsTests.cs new file mode 100644 index 000000000000..f1834ad648d4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/EqualityExtensionsTests.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.TemplateEngine.Utils.UnitTests +{ + public class EqualityExtensionsTests + { + [Fact(DisplayName = "AllAreTheSameDefaultComparerTrueTest")] + public void AllAreTheSameDefaultComparerTrueTest() + { + IDictionary items = new Dictionary() + { + { 1, "this" }, + { 2, "this" }, + { 3, "this" }, + { 4, "this" } + }; + static string Selector(KeyValuePair x) => x.Value; + + Assert.True(items.AllAreTheSame(Selector)); + } + + [Fact(DisplayName = "AllAreTheSameDefaultComparerFailsTest")] + public void AllAreTheSameDefaultComparerFailsTest() + { + IDictionary items = new Dictionary() + { + { 1, "this" }, + { 2, "that" }, + { 3, "other" }, + { 4, "thing" } + }; + static string Selector(KeyValuePair x) => x.Value; + + Assert.False(items.AllAreTheSame(Selector)); + } + + [Fact(DisplayName = "AllAreTheSameCustomComparerTest")] + public void AllAreTheSameCustomComparerTest() + { + IDictionary items = new Dictionary() + { + { 1, "this" }, + { 2, "that" }, + { 3, "four" }, + { 4, "long" } + }; + static string Selector(KeyValuePair x) => x.Value; + + static bool LengthComparer(string? x, string? y) => x!.Length == y!.Length; + + // they're all the same length + Assert.True(items.AllAreTheSame(Selector, LengthComparer)); + + static bool UpperComparer(string? x, string? y) => x!.ToUpper() == y!.ToUpper(); + Assert.False(items.AllAreTheSame(Selector, UpperComparer)); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/GlobTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/GlobTests.cs new file mode 100644 index 000000000000..cb5c07557ebf --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/GlobTests.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.TemplateEngine.Utils.UnitTests +{ + public class GlobTests + { + [Fact(DisplayName = nameof(VerifyLeadGlobPathSpanning))] + public void VerifyLeadGlobPathSpanning() + { + Glob g = Glob.Parse("**/file"); + Assert.True(g.IsMatch("file")); + Assert.True(g.IsMatch("a/file")); + Assert.True(g.IsMatch("a/b/file")); + Assert.True(g.IsMatch("a/b/c/file")); + Assert.False(g.IsMatch("other")); + Assert.False(g.IsMatch("a/other")); + Assert.False(g.IsMatch("a/b/other")); + Assert.False(g.IsMatch("a/b/c/other")); + Assert.False(g.IsMatch("file/stuff")); + Assert.False(g.IsMatch("file.txt")); + Assert.False(g.IsMatch("thefile")); + } + + [Fact(DisplayName = nameof(VerifyGlobExactPathSpanning))] + public void VerifyGlobExactPathSpanning() + { + Glob g = Glob.Parse("a/**/b"); + Assert.True(g.IsMatch("a/b")); + Assert.True(g.IsMatch("a/x/b")); + Assert.True(g.IsMatch("a/x/y/b")); + Assert.False(g.IsMatch("z/a/x/y/b")); + Assert.False(g.IsMatch("z/a/b")); + } + + [Fact(DisplayName = nameof(VerifyGlobPathSpanning))] + public void VerifyGlobPathSpanning() + { + Glob g = Glob.Parse("a/**"); + Assert.True(g.IsMatch("a/b")); + Assert.True(g.IsMatch("a/x/b")); + Assert.True(g.IsMatch("a/x/y/b")); + Assert.False(g.IsMatch("z/a/x/y/b")); + Assert.False(g.IsMatch("z/a/b")); + } + + [Fact(DisplayName = nameof(VerifyGlobCharacterGroups))] + public void VerifyGlobCharacterGroups() + { + Glob g = Glob.Parse("f[Oo]o"); + Assert.True(g.IsMatch("foo")); + Assert.True(g.IsMatch("fOo")); + Assert.True(g.IsMatch("a/foo")); + Assert.True(g.IsMatch("z/a/x/y/fOo")); + Assert.False(g.IsMatch("z/a/x/y/fOO")); + } + + [Fact(DisplayName = nameof(VerifyGlobWildcard))] + public void VerifyGlobWildcard() + { + Glob g = Glob.Parse("f*o"); + Assert.True(g.IsMatch("foo")); + Assert.True(g.IsMatch("foooooooo")); + Assert.False(g.IsMatch("foot")); + } + + [Fact(DisplayName = nameof(VerifyGlobNegate))] + public void VerifyGlobNegate() + { + Glob g = Glob.Parse("!f*o"); + Assert.False(g.IsMatch("foo")); + Assert.False(g.IsMatch("foooooooo")); + Assert.True(g.IsMatch("foot")); + } + + [Fact(DisplayName = nameof(VerifyGlobEscape))] + public void VerifyGlobEscape() + { + Glob g = Glob.Parse(@"\[[\[\ \]]"); + Assert.True(g.IsMatch("[ ")); + Assert.True(g.IsMatch("[]")); + Assert.True(g.IsMatch("[[")); + Assert.False(g.IsMatch("]")); + } + + [Fact(DisplayName = nameof(VerifyGlobKitchenSink))] + public void VerifyGlobKitchenSink() + { + Glob g = Glob.Parse("**/[Dd]ocuments/**/*.htm*"); + Assert.True(g.IsMatch("Documents/git.html")); + Assert.True(g.IsMatch("Documents/ppc/ppc.html")); + Assert.True(g.IsMatch("tools/perf/Documents/perf.html")); + + g = Glob.Parse("**/[Dd]ocuments/**/*p*.htm*"); + Assert.False(g.IsMatch("Documents/git.html")); + Assert.True(g.IsMatch("Documents/ppc/ppc.html")); + Assert.True(g.IsMatch("tools/perf/Documents/perf.html")); + + g = Glob.Parse("[Dd]ocuments/**/*.htm*"); + Assert.True(g.IsMatch("Documents/git.html")); + Assert.True(g.IsMatch("Documents/ppc/ppc.html")); + Assert.False(g.IsMatch("tools/perf/Documents/perf.html")); + + g = Glob.Parse("[Dd]ocuments/**/*.h*"); + Assert.True(g.IsMatch("Documents/git.html")); + Assert.True(g.IsMatch("Documents/ppc/ppc.html")); + Assert.False(g.IsMatch("tools/perf/Documents/perf.html")); + + g = Glob.Parse("[Dd]ocuments/**/*.html"); + Assert.True(g.IsMatch("Documents/git.html")); + Assert.True(g.IsMatch("Documents/ppc/ppc.html")); + Assert.False(g.IsMatch("tools/perf/Documents/perf.html")); + + g = Glob.Parse("[Dd]ocuments/*.html"); + Assert.True(g.IsMatch("Documents/git.html")); + Assert.False(g.IsMatch("Documents/ppc/ppc.html")); + Assert.False(g.IsMatch("tools/perf/Documents/perf.html")); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/InMemoryFileSystemTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/InMemoryFileSystemTests.cs new file mode 100644 index 000000000000..7da80beb1b12 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/InMemoryFileSystemTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; +using Microsoft.TemplateEngine.Mocks; +using Xunit; + +namespace Microsoft.TemplateEngine.Utils.UnitTests +{ + public class InMemoryFileSystemTests + { + [Fact(DisplayName = nameof(VerifyMultipleVirtualizationsAreHandled))] + public void VerifyMultipleVirtualizationsAreHandled() + { + IPhysicalFileSystem mockFileSystem = new MockFileSystem(); + IPhysicalFileSystem virtualized1 = new InMemoryFileSystem(Directory.GetCurrentDirectory().CombinePaths("test1"), mockFileSystem); + IPhysicalFileSystem virtualized2 = new InMemoryFileSystem(Directory.GetCurrentDirectory().CombinePaths("test2"), virtualized1); + + string testFilePath = Directory.GetCurrentDirectory().CombinePaths("test1", "test.txt"); + virtualized2.CreateFile(testFilePath).Dispose(); + Assert.False(mockFileSystem.FileExists(testFilePath)); + Assert.True(virtualized1.FileExists(testFilePath)); + Assert.True(virtualized2.FileExists(testFilePath)); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/InstallRequestPathResolutionTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/InstallRequestPathResolutionTests.cs new file mode 100644 index 000000000000..00043a9c0304 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/InstallRequestPathResolutionTests.cs @@ -0,0 +1,131 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateEngine.Utils.UnitTests +{ + public class InstallRequestPathResolutionTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public InstallRequestPathResolutionTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: this.GetType().Name, virtualize: true); + } + + [Fact] + public void CanResolvePath() + { + IEnumerable installPath = InstallRequestPathResolution.ExpandMaskedPath(Directory.GetCurrentDirectory(), _engineEnvironmentSettings); + Assert.Equal(Directory.GetCurrentDirectory(), installPath.Single()); + } + + [Fact] + public void CanTrimTrailingSeparator() + { + IEnumerable installPath = InstallRequestPathResolution.ExpandMaskedPath(Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar, _engineEnvironmentSettings); + Assert.Equal(Directory.GetCurrentDirectory(), installPath.Single()); + } + + [Fact] + public void CanResolveCurrentPath() + { + IEnumerable installPath = InstallRequestPathResolution.ExpandMaskedPath(".", _engineEnvironmentSettings); + Assert.Equal(Directory.GetCurrentDirectory(), installPath.Single()); + } + + [Fact] + public void CanResolveParentPath() + { + IEnumerable installPath = InstallRequestPathResolution.ExpandMaskedPath("..", _engineEnvironmentSettings); + Assert.Equal(Path.GetDirectoryName(Directory.GetCurrentDirectory()), installPath.Single()); + } + + [Fact] + public void CanResolveSubdirectories() + { + var testRootDir = TestUtils.CreateTemporaryFolder(); + Directory.CreateDirectory(Path.Combine(testRootDir, "dir1")); + Directory.CreateDirectory(Path.Combine(testRootDir, "dir2")); + Directory.CreateDirectory(Path.Combine(testRootDir, "dir3")); + + IEnumerable installPath = InstallRequestPathResolution.ExpandMaskedPath(Path.Combine(testRootDir, "*"), _engineEnvironmentSettings); + + Assert.Equal(3, installPath.Count()); + Assert.Contains(Path.Combine(testRootDir, "dir1"), installPath); + } + + [Fact] + public void CanResolveMaskedSubdirectories() + { + var testRootDir = TestUtils.CreateTemporaryFolder(); + Directory.CreateDirectory(Path.Combine(testRootDir, "dir1")); + Directory.CreateDirectory(Path.Combine(testRootDir, "dar2")); + Directory.CreateDirectory(Path.Combine(testRootDir, "dir33")); + + IEnumerable installPath = InstallRequestPathResolution.ExpandMaskedPath(Path.Combine(testRootDir, "dir*"), _engineEnvironmentSettings); + + Assert.Equal(2, installPath.Count()); + Assert.Contains(Path.Combine(testRootDir, "dir1"), installPath); + Assert.Contains(Path.Combine(testRootDir, "dir33"), installPath); + } + + [Fact] + public void CanResolveMaskedFiles() + { + var testRootDir = TestUtils.CreateTemporaryFolder(); + File.Create(Path.Combine(testRootDir, "1.nupkg")); + File.Create(Path.Combine(testRootDir, "2.nupkg")); + File.Create(Path.Combine(testRootDir, "3.txt")); + + IEnumerable installPath = InstallRequestPathResolution.ExpandMaskedPath(Path.Combine(testRootDir, "*.nupkg"), _engineEnvironmentSettings); + + Assert.Equal(2, installPath.Count()); + Assert.Contains(Path.Combine(testRootDir, "1.nupkg"), installPath); + Assert.Contains(Path.Combine(testRootDir, "2.nupkg"), installPath); + } + + [Fact] + public void CannotResolveInvalidPath() + { + IEnumerable installPath = InstallRequestPathResolution.ExpandMaskedPath("|path|", _engineEnvironmentSettings); + Assert.Equal("|path|", installPath.Single()); + } + + [Fact] + public void CannotResolveNonExistingPath() + { + Assert.False(File.Exists("path")); + IEnumerable installPath = InstallRequestPathResolution.ExpandMaskedPath("path", _engineEnvironmentSettings); + Assert.Equal("path", installPath.Single()); + + installPath = InstallRequestPathResolution.ExpandMaskedPath("path\\", _engineEnvironmentSettings); + Assert.Equal("path\\", installPath.Single()); + } + + [Fact] + public void CannotResolveMaskedPathInFolder() + { + var testRootDir = TestUtils.CreateTemporaryFolder(); + Directory.CreateDirectory(Path.Combine(testRootDir, "dir")); + File.Create(Path.Combine(testRootDir, "dir", "1.nupkg")); + File.Create(Path.Combine(testRootDir, "dir", "2.nupkg")); + + IEnumerable installPath = InstallRequestPathResolution.ExpandMaskedPath(Path.Combine(testRootDir, "*", "*.nupkg"), _engineEnvironmentSettings); + Assert.Equal(Path.Combine(testRootDir, "*", "*.nupkg"), installPath.Single()); + } + + [Fact] + public void CanResolveParentOfRootFolder() + { + string dir = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "C:\\" : "/"; + + IEnumerable installPath = InstallRequestPathResolution.ExpandMaskedPath(dir + "..", _engineEnvironmentSettings); + Assert.Equal(dir, installPath.Single()); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/ListExtensionsTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/ListExtensionsTests.cs new file mode 100644 index 000000000000..c6eea96dd72d --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/ListExtensionsTests.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.TemplateEngine.Utils.UnitTests +{ + public class ListExtensionsTests + { + [Fact(DisplayName = nameof(GroupByExtensionTest))] + public void GroupByExtensionTest() + { + List templatesToGroup = new List + { + new GroupByTestStruct() + { + _identity = "1", + _groupIdentity = null + }, + new GroupByTestStruct() + { + _identity = "2", + _groupIdentity = string.Empty + }, + new GroupByTestStruct() + { + _identity = "3", + _groupIdentity = null + }, + new GroupByTestStruct() + { + _identity = "4", + _groupIdentity = string.Empty + }, + new GroupByTestStruct() + { + _identity = "5", + _groupIdentity = "TemplateGroup" + }, + new GroupByTestStruct() + { + _identity = "6", + _groupIdentity = "templategroup" + }, + new GroupByTestStruct() + { + _identity = "7", + _groupIdentity = "TemplateGroup2" + }, + new GroupByTestStruct() + { + _identity = "8", + _groupIdentity = "other" + }, + new GroupByTestStruct() + { + _identity = "9", + _groupIdentity = "templategroup" + } + }; + + var templateGroups = templatesToGroup.GroupBy(x => x._groupIdentity, x => !string.IsNullOrEmpty(x._groupIdentity), StringComparer.OrdinalIgnoreCase); + Assert.Equal(7, templateGroups.Count()); + var groupWithExpectedMultipleElements = templateGroups.Single(g => g.Key?.Equals("TemplateGroup", StringComparison.OrdinalIgnoreCase) ?? false); + Assert.Equal(3, groupWithExpectedMultipleElements.Count()); + Assert.Single(groupWithExpectedMultipleElements, s => s._identity == "5"); + Assert.Single(groupWithExpectedMultipleElements, s => s._identity == "6"); + Assert.Single(groupWithExpectedMultipleElements, s => s._identity == "9"); + } + + internal struct GroupByTestStruct + { + internal string? _identity; + internal string? _groupIdentity; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/Microsoft.TemplateEngine.Utils.UnitTests.csproj b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/Microsoft.TemplateEngine.Utils.UnitTests.csproj new file mode 100644 index 000000000000..d530d5d99d24 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/Microsoft.TemplateEngine.Utils.UnitTests.csproj @@ -0,0 +1,18 @@ + + + + $(NetCurrent);$(NetFrameworkCurrent) + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/VersionStringTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/VersionStringTests.cs new file mode 100644 index 000000000000..deb189ee15eb --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/VersionStringTests.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.TemplateEngine.Utils.UnitTests +{ + public class VersionStringTests + { + [Theory(DisplayName = nameof(VerifyVersionIsWellFormedCheckerTest))] + [InlineData("1.0.0.0", true)] + [InlineData("1.0.0", true)] + [InlineData("1.0", true)] + [InlineData("1", false)] + [InlineData("1.0.0.0.", false)] + [InlineData("1.0.0.", false)] + [InlineData("1.0.", false)] + [InlineData("1.", false)] + [InlineData("MyVersion", false)] + [InlineData("1.0.0.A", false)] + [InlineData("A.0.0.0", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void VerifyVersionIsWellFormedCheckerTest(string? versionString, bool expectedParseResult) + { + Assert.Equal(expectedParseResult, VersionStringHelpers.IsVersionWellFormed(versionString)); + } + + [Theory(DisplayName = nameof(VerifyVersionComparisonTest))] + [InlineData("", "", null)] + [InlineData(null, null, null)] + [InlineData("1.0.0.0", "1.0.0.0", 0)] + [InlineData("1.0.0.0", null, null)] + [InlineData(null, "1.0.0.0", null)] + [InlineData("1.0.0.0", "1.1.0.0", -1)] + [InlineData("1.1.0.0", "1.0.0.0", 1)] + [InlineData("1.0.0", "1.1", -1)] + [InlineData("1.1", "1.0.0", 1)] + public void VerifyVersionComparisonTest(string? version1, string? version2, int? expectedComparison) + { + Assert.Equal(expectedComparison, VersionStringHelpers.CompareVersions(version1, version2)); + } + + [Theory(DisplayName = nameof(VersionParseCompare))] + [InlineData("1.0.0.0", "1.0.0.0", true)] + [InlineData("1.0.0.0", "1.0.0", true)] + [InlineData("1.0.0.0", "1.0", true)] + [InlineData("1.0.0.0", "1.1", false)] + [InlineData("2.0.0.0", "1.1", false)] + [InlineData("[1.0.0.0-*)", "1.0.0.0", true)] + [InlineData("[1.0.0.0-*)", "1.1.0.0", true)] + [InlineData("[1.0.0.0-*)", "1.0.1.0", true)] + [InlineData("[1.0.0.0-*)", "1.0.0.1", true)] + [InlineData("(1.0.0.0-*)", "1.0.0.0", false)] + [InlineData("(*-2.0.0.0)", "1.0.0.0", true)] + [InlineData("(*-2.0.0.0)", "1.5.0.0", true)] + [InlineData("(*-2.0.0.0)", "2.0.0.0", false)] + [InlineData("(*-2.0.0.0]", "2.0.0.0", true)] + [InlineData("[1.1.0.0-1.2.0.0]", "1.0.0.0", false)] + [InlineData("[1.1.0.0-1.2.0.0]", "1.1.0.0", true)] + [InlineData("[1.1.0.0-1.2.0.0]", "1.2.0.0", true)] + [InlineData("[1.1.0.0-1.2.0.0]", "1.2.0.1", false)] + [InlineData("(1.1.0.0-1.2.0.0)", "1.1.0.0", false)] + [InlineData("(1.1.0.0-1.2.0.0)", "1.2.0.0", false)] + [InlineData("(1.1.0.0-1.2.0.0)", "1.1.1.0", true)] + [InlineData("(1.1.0.0-1.2.0.0)", "1.1.0.1", true)] + public void VersionParseCompare(string allowed, string proposed, bool expected) + { + Assert.True(VersionStringHelpers.TryParseVersionSpecification(allowed, out IVersionSpecification? checker)); + Assert.NotNull(checker); + Assert.Equal(expected, checker.CheckIfVersionIsValid(proposed)); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/WellKnownSearchFiltersTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/WellKnownSearchFiltersTests.cs new file mode 100644 index 000000000000..81333ab45a2f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Utils.UnitTests/WellKnownSearchFiltersTests.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions.TemplateFiltering; +using Microsoft.TemplateEngine.Mocks; +using Xunit; + +namespace Microsoft.TemplateEngine.Utils.UnitTests +{ + public class WellKnownSearchFiltersTests + { + [Theory] + [InlineData("test", "test", MatchKind.Exact)] + [InlineData("test1||test2", "test1", MatchKind.Exact)] + [InlineData("test1||test2", "test", MatchKind.Mismatch)] + [InlineData("test1||test2", null, null)] + public void TagFilterTests_TemplateWithTags(string templateTags, string? testTag, MatchKind? kind) + { + const string separator = "||"; + var templateTagsArray = templateTags.Split(new[] { separator }, System.StringSplitOptions.None); + + MockTemplateInfo template = new MockTemplateInfo("console", name: "Long name for Console App", identity: "Console.App.T1", groupIdentity: "Console.App.Test") + .WithTag("language", "L1") + .WithTag("type", "project") + .WithBaselineInfo("app", "standard") + .WithClassifications(templateTagsArray); + + var filter = WellKnownSearchFilters.ClassificationFilter(testTag); + MatchInfo? result = filter(template); + Assert.Equal(kind, result?.Kind); + } + + [Theory] + [InlineData("test", MatchKind.Mismatch)] + [InlineData(null, null)] + public void TagFilterTests_TemplateWithoutTags(string? testTag, MatchKind? kind) + { + MockTemplateInfo template = new MockTemplateInfo("console", name: "Long name for Console App", identity: "Console.App.T1", groupIdentity: "Console.App.Test") + .WithTag("language", "L1") + .WithTag("type", "project") + .WithBaselineInfo("app", "standard"); + + var filter = WellKnownSearchFilters.ClassificationFilter(testTag); + MatchInfo? result = filter(template); + Assert.Equal(kind, result?.Kind); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/AllComponents.cs b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/AllComponents.cs new file mode 100644 index 000000000000..0ea057eff3f0 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/AllComponents.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.TestHelper; +using Xunit; + +namespace Microsoft.TemplateSearch.Common.UnitTests +{ + public class AllComponents + { + [Fact] + public void TestAllSearchComponentsAdded() + { + var assemblyCatalog = new AssemblyComponentCatalog(new[] { typeof(Components).Assembly }); + + var expectedTypeNames = assemblyCatalog.Select(pair => pair.Item1.FullName + ";" + pair.Item2.GetType().FullName).OrderBy(name => name); + var actualTypeNames = Components.AllComponents.Select(t => t.Type.FullName + ";" + t.Instance.GetType().FullName).OrderBy(name => name); + + Assert.Equal(expectedTypeNames, actualTypeNames); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/Microsoft.TemplateSearch.Common.UnitTests.csproj b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/Microsoft.TemplateSearch.Common.UnitTests.csproj new file mode 100644 index 000000000000..d7f1c7b705d5 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/Microsoft.TemplateSearch.Common.UnitTests.csproj @@ -0,0 +1,24 @@ + + + + $(NetCurrent) + + + + + + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/MockTemplateSearchSource.cs b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/MockTemplateSearchSource.cs new file mode 100644 index 000000000000..4e24bca1557e --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/MockTemplateSearchSource.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateSearch.Common.Abstractions; + +namespace Microsoft.TemplateSearch.Common.UnitTests +{ + public class MockTemplateSearchProviderFactory : ITemplateSearchProviderFactory + { + private readonly ITemplateSearchProvider _searchProvider; + + public MockTemplateSearchProviderFactory(Guid id, string displayName, MockTemplateSearchProvider searchProvider) + { + Id = id; + DisplayName = displayName; + _searchProvider = searchProvider; + searchProvider.Factory = this; + } + + public string DisplayName { get; } + + public Guid Id { get; } + + public ITemplateSearchProvider CreateProvider(IEngineEnvironmentSettings environmentSettings, IReadOnlyDictionary> additionalDataReaders) + { + return _searchProvider; + } + } + + public class MockTemplateSearchProvider : ITemplateSearchProvider + { + private ITemplateSearchProviderFactory? _factory; + + public bool WasSearched { get; private set; } + + public IReadOnlyList<(ITemplatePackageInfo PackageInfo, IReadOnlyList MatchedTemplates)> Results { get; set; } = []; + + public ITemplateSearchProviderFactory Factory + { + get => _factory ?? throw new Exception($"{nameof(Factory)} is not set."); + + set => _factory = value; + } + + Task MatchedTemplates)>> ITemplateSearchProvider.SearchForTemplatePackagesAsync( + Func packFilters, + Func> matchingTemplatesFilter, + CancellationToken cancellationToken) + { + WasSearched = true; + return Task.FromResult(Results); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetMetadataSearchProviderTests.cs b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetMetadataSearchProviderTests.cs new file mode 100644 index 000000000000..b1d5df298712 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetMetadataSearchProviderTests.cs @@ -0,0 +1,401 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateSearch.Common.Abstractions; +using Microsoft.TemplateSearch.Common.Providers; +using Xunit; + +namespace Microsoft.TemplateSearch.Common.UnitTests +{ + public class NuGetMetadataSearchProviderTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public NuGetMetadataSearchProviderTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _environmentSettingsHelper = environmentSettingsHelper; + } + + [Fact] + public async Task SearchOnlineCache() + { + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateSearchProviderFactory), new NuGetMetadataSearchProviderFactory()); + + const string templateName = "api"; + + static IReadOnlyList Filter(TemplatePackageSearchData templatePack) => templatePack.Templates + .Where(t => ((ITemplateInfo)t).Name.Contains(templateName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var searchCoordinator = new TemplateSearchCoordinator(engineEnvironmentSettings); + async Task> Search() + { + var result = await searchCoordinator.SearchAsync(p => true, Filter, default); + if (result != null && result.Count > 0 && !string.IsNullOrWhiteSpace(result[0].ErrorMessage)) + { + var errorMessage = result[0].ErrorMessage; + if (errorMessage!.Contains("The SSL connection could not be established")) + { + throw new HttpRequestException(errorMessage); + } + } + return result!; + } + + var searchResult = await TestUtils.AttemptSearch, HttpRequestException>(3, TimeSpan.FromSeconds(10), Search); + + Assert.NotNull(searchResult); + Assert.Single(searchResult); + Assert.True(searchResult[0].Success); + Assert.True(string.IsNullOrWhiteSpace(searchResult[0].ErrorMessage)); + Assert.True(searchResult[0].SearchHits.Count > 0); + } + + [Fact] + public async Task SearchOverrideCache() + { + string searchFilePath = GenerateLocalCache(); + var environment = new MockEnvironment(); + environment.SetEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", searchFilePath); + + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true, environment: environment); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateSearchProviderFactory), new NuGetMetadataSearchProviderFactory()); + + const string templateName = "foo"; + + static IReadOnlyList Filter(TemplatePackageSearchData templatePack) => templatePack.Templates + .Where(t => ((ITemplateInfo)t).Name.Contains(templateName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var searchCoordinator = new TemplateSearchCoordinator(engineEnvironmentSettings); + var searchResult = await searchCoordinator.SearchAsync(p => true, Filter, default); + + Assert.NotNull(searchResult); + Assert.Single(searchResult); + Assert.True(searchResult[0].Success); + Assert.True(string.IsNullOrWhiteSpace(searchResult[0].ErrorMessage)); + Assert.True(searchResult[0].SearchHits.Count > 0); + + //provider should not copy local file to settings + Assert.False(engineEnvironmentSettings.Host.FileSystem.FileExists(Path.Combine(engineEnvironmentSettings.Paths.HostVersionSettingsDir, "nugetTemplateSearchInfo.json"))); + } + + [Fact] + public async Task SearchOverrideCache_FailsWhenFileDoesntExist() + { + string searchFilePath = "do-not-exist"; + var environment = new MockEnvironment(); + environment.SetEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", searchFilePath); + + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true, environment: environment); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateSearchProviderFactory), new NuGetMetadataSearchProviderFactory()); + + const string templateName = "foo"; + + static IReadOnlyList Filter(TemplatePackageSearchData templatePack) => templatePack.Templates + .Where(t => ((ITemplateInfo)t).Name.Contains(templateName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var searchCoordinator = new TemplateSearchCoordinator(engineEnvironmentSettings); + var searchResult = await searchCoordinator.SearchAsync(p => true, Filter, default); + + Assert.NotNull(searchResult); + Assert.Single(searchResult); + Assert.False(searchResult[0].Success); + Assert.False(string.IsNullOrWhiteSpace(searchResult[0].ErrorMessage)); + Assert.True(searchResult[0].SearchHits.Count == 0); + Assert.Equal("Local search cache 'do-not-exist' does not exist.", searchResult[0].ErrorMessage); + + //provider should not copy local file to settings + Assert.False(engineEnvironmentSettings.Host.FileSystem.FileExists(Path.Combine(engineEnvironmentSettings.Paths.HostVersionSettingsDir, "nugetTemplateSearchInfo.json"))); + } + + [Fact] + public async Task SearchLocalCache() + { + var environment = new MockEnvironment(); + environment.SetEnvironmentVariable("DOTNET_NEW_LOCAL_SEARCH_FILE_ONLY", "true"); + + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true, environment: environment); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateSearchProviderFactory), new NuGetMetadataSearchProviderFactory()); + + const string templateName = "api"; + + IReadOnlyList Filter(TemplatePackageSearchData templatePack) => templatePack.Templates + .Where(t => ((ITemplateInfo)t).Name.Contains(templateName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var searchCoordinator = new TemplateSearchCoordinator(engineEnvironmentSettings); + async Task> Search() + { + var result = await searchCoordinator.SearchAsync(p => true, Filter, default); + if (result != null && result.Count > 0 && !string.IsNullOrWhiteSpace(result[0].ErrorMessage)) + { + var errorMessage = result[0].ErrorMessage; + if (errorMessage!.Contains("The SSL connection could not be established")) + { + throw new HttpRequestException(errorMessage); + } + } + return result!; + } + var searchResult = await TestUtils.AttemptSearch, HttpRequestException>(3, TimeSpan.FromSeconds(10), Search); + + Assert.NotNull(searchResult); + Assert.Single(searchResult); + Assert.False(searchResult[0].Success); + Assert.False(string.IsNullOrWhiteSpace(searchResult[0].ErrorMessage)); + Assert.True(searchResult[0].SearchHits.Count == 0); + Assert.Equal($"Local search cache '{Path.Combine(engineEnvironmentSettings.Paths.HostVersionSettingsDir, "nugetTemplateSearchInfo.json")}' does not exist.", searchResult[0].ErrorMessage); + + environment.SetEnvironmentVariable("DOTNET_NEW_LOCAL_SEARCH_FILE_ONLY", null); + searchResult = await TestUtils.AttemptSearch, HttpRequestException>(3, TimeSpan.FromSeconds(10), Search); + + Assert.NotNull(searchResult); + Assert.Single(searchResult); + Assert.True(searchResult[0].Success); + Assert.True(string.IsNullOrWhiteSpace(searchResult[0].ErrorMessage)); + Assert.True(searchResult[0].SearchHits.Count > 0); + + environment.SetEnvironmentVariable("DOTNET_NEW_LOCAL_SEARCH_FILE_ONLY", "true"); + searchResult = await TestUtils.AttemptSearch, HttpRequestException>(3, TimeSpan.FromSeconds(10), Search); + + Assert.NotNull(searchResult); + Assert.Single(searchResult); + Assert.True(searchResult[0].Success); + Assert.True(string.IsNullOrWhiteSpace(searchResult[0].ErrorMessage)); + Assert.True(searchResult[0].SearchHits.Count > 0); + } + + [Fact] + public async Task SearchReturnsErrorOnIncorrectCache() + { + var jsonObject = JsonNode.Parse(JsonSerializer.Serialize(new { randomField = "smth" }))!; + string searchFilePath = Path.Combine(TestUtils.CreateTemporaryFolder(), "searchCacheV2.json"); + File.WriteAllText(searchFilePath, jsonObject.ToString()); + + var environment = new MockEnvironment(); + environment.SetEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", searchFilePath); + + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true, environment: environment); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateSearchProviderFactory), new NuGetMetadataSearchProviderFactory()); + + const string templateName = "foo"; + + static IReadOnlyList Filter(TemplatePackageSearchData templatePack) => templatePack.Templates + .Where(t => ((ITemplateInfo)t).Name.Contains(templateName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var searchCoordinator = new TemplateSearchCoordinator(engineEnvironmentSettings); + var searchResult = await searchCoordinator.SearchAsync(p => true, Filter, default); + + Assert.NotNull(searchResult); + Assert.Single(searchResult); + Assert.False(searchResult[0].Success); + Assert.False(string.IsNullOrWhiteSpace(searchResult[0].ErrorMessage)); + Assert.Equal("The template search cache data is not supported.", searchResult[0].ErrorMessage); + Assert.True(searchResult[0].SearchHits.Count == 0); + + //provider should not copy local file to settings + Assert.False(engineEnvironmentSettings.Host.FileSystem.FileExists(Path.Combine(engineEnvironmentSettings.Paths.HostVersionSettingsDir, "nugetTemplateSearchInfo.json"))); + } + + [Fact] + public async Task SearchReturnsErrorOnIncorrectV1Cache() + { + var jsonObject = JsonNode.Parse(JsonSerializer.Serialize(new { version = "1.0.0.0", randomField = "smth" }))!; + string searchFilePath = Path.Combine(TestUtils.CreateTemporaryFolder(), "searchCache.json"); + File.WriteAllText(searchFilePath, jsonObject.ToString()); + + var environment = new MockEnvironment(); + environment.SetEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", searchFilePath); + + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true, environment: environment); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateSearchProviderFactory), new NuGetMetadataSearchProviderFactory()); + + const string templateName = "foo"; + + static IReadOnlyList Filter(TemplatePackageSearchData templatePack) => templatePack.Templates + .Where(t => ((ITemplateInfo)t).Name.Contains(templateName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var searchCoordinator = new TemplateSearchCoordinator(engineEnvironmentSettings); + var searchResult = await searchCoordinator.SearchAsync(p => true, Filter, default); + + Assert.NotNull(searchResult); + Assert.Single(searchResult); + Assert.False(searchResult[0].Success); + Assert.False(string.IsNullOrWhiteSpace(searchResult[0].ErrorMessage)); + Assert.True(searchResult[0].SearchHits.Count == 0); + Assert.Equal("The template search cache data is not valid.", searchResult[0].ErrorMessage); + + //provider should not copy local file to settings + Assert.False(engineEnvironmentSettings.Host.FileSystem.FileExists(Path.Combine(engineEnvironmentSettings.Paths.HostVersionSettingsDir, "nugetTemplateSearchInfo.json"))); + } + + [Fact] + public async Task SearchReturnsErrorOnIncorrectV2Cache() + { + var jsonObject = JsonNode.Parse(JsonSerializer.Serialize(new { version = "2.0", randomField = "smth" }))!; + string searchFilePath = Path.Combine(TestUtils.CreateTemporaryFolder(), "searchCache.json"); + File.WriteAllText(searchFilePath, jsonObject.ToString()); + + var environment = new MockEnvironment(); + environment.SetEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", searchFilePath); + + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true, environment: environment); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateSearchProviderFactory), new NuGetMetadataSearchProviderFactory()); + + const string templateName = "foo"; + + static IReadOnlyList Filter(TemplatePackageSearchData templatePack) => templatePack.Templates + .Where(t => ((ITemplateInfo)t).Name.Contains(templateName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var searchCoordinator = new TemplateSearchCoordinator(engineEnvironmentSettings); + var searchResult = await searchCoordinator.SearchAsync(p => true, Filter, default); + + Assert.NotNull(searchResult); + Assert.Single(searchResult); + Assert.False(searchResult[0].Success); + Assert.False(string.IsNullOrWhiteSpace(searchResult[0].ErrorMessage)); + Assert.True(searchResult[0].SearchHits.Count == 0); + Assert.Equal("The template search cache data is not valid.", searchResult[0].ErrorMessage); + + //provider should not copy local file to settings + Assert.False(engineEnvironmentSettings.Host.FileSystem.FileExists(Path.Combine(engineEnvironmentSettings.Paths.HostVersionSettingsDir, "nugetTemplateSearchInfo.json"))); + } + + [Fact] + public async Task SearchReturnsErrorOnIncorrectVersionCache() + { + var jsonObject = JsonNode.Parse(JsonSerializer.Serialize(new { version = "3.0", TemplatePackages = Array.Empty() }))!; + string searchFilePath = Path.Combine(TestUtils.CreateTemporaryFolder(), "searchCache.json"); + File.WriteAllText(searchFilePath, jsonObject.ToString()); + + var environment = new MockEnvironment(); + environment.SetEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", searchFilePath); + + IEngineEnvironmentSettings engineEnvironmentSettings = _environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true, environment: environment); + engineEnvironmentSettings.Components.AddComponent(typeof(ITemplateSearchProviderFactory), new NuGetMetadataSearchProviderFactory()); + + const string templateName = "foo"; + + static IReadOnlyList Filter(TemplatePackageSearchData templatePack) => templatePack.Templates + .Where(t => ((ITemplateInfo)t).Name.Contains(templateName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var searchCoordinator = new TemplateSearchCoordinator(engineEnvironmentSettings); + var searchResult = await searchCoordinator.SearchAsync(p => true, Filter, default); + + Assert.NotNull(searchResult); + Assert.Single(searchResult); + Assert.False(searchResult[0].Success); + Assert.False(string.IsNullOrWhiteSpace(searchResult[0].ErrorMessage)); + Assert.True(searchResult[0].SearchHits.Count == 0); + Assert.Equal("The template search cache data is not supported.", searchResult[0].ErrorMessage); + + //provider should not copy local file to settings + Assert.False(engineEnvironmentSettings.Host.FileSystem.FileExists(Path.Combine(engineEnvironmentSettings.Paths.HostVersionSettingsDir, "nugetTemplateSearchInfo.json"))); + } + + private string GenerateLocalCache() + { + ITemplatePackageInfo packOneInfo = new MockTemplatePackageInfo("PackOne", "1.0.0"); + ITemplatePackageInfo packTwoInfo = new MockTemplatePackageInfo("PackTwo", "1.6.0"); + ITemplatePackageInfo packThreeInfo = new MockTemplatePackageInfo("PackThree", "2.1"); + + ITemplateInfo fooOneTemplate = + new MockTemplateInfo("foo1", name: "MockFooTemplateOne", identity: "Mock.Foo.1", groupIdentity: "Mock.Foo", author: "TestAuthor") + .WithClassifications("CSharp", "Library") + .WithDescription("Mock Foo template one") + .WithChoiceParameter("Framework", "netcoreapp3.0", "netcoreapp3.1") + .WithTag("language", "C#") + .WithTag("type", "project"); + + ITemplateInfo fooTwoTemplate = + new MockTemplateInfo("foo2", name: "MockFooTemplateTwo", identity: "Mock.Foo.2", groupIdentity: "Mock.Foo") + .WithClassifications("CSharp", "Console") + .WithDescription("Mock Foo template two") + .WithChoiceParameter("Framework", "netcoreapp2.0", "netcoreapp2.1", "netcoreapp3.1") + .WithTag("language", "C#"); + + ITemplateInfo barCSharpTemplate = + new MockTemplateInfo("barC", name: "MockBarCsharpTemplate", identity: "Mock.Bar.1.Csharp", groupIdentity: "Mock.Bar") + .WithClassifications("CSharp") + .WithDescription("Mock Bar CSharp template") + .WithTag("language", "C#"); + + ITemplateInfo barFSharpTemplate = + new MockTemplateInfo("barF", name: "MockBarFSharpTemplate", identity: "Mock.Bar.1.FSharp", groupIdentity: "Mock.Bar") + .WithClassifications("FSharp") + .WithDescription("Mock Bar FSharp template") + .WithTag("language", "F#"); + + var fooOneTemplateData = new TemplateSearchData(fooOneTemplate); + var fooTwoTemplateData = new TemplateSearchData(fooTwoTemplate); + var barCSharpTemplateData = new TemplateSearchData(barCSharpTemplate); + var barFSharpTemplateData = new TemplateSearchData(barFSharpTemplate); + + var packOne = new TemplatePackageSearchData(packOneInfo, new[] { fooOneTemplateData }); + var packTwo = new TemplatePackageSearchData(packTwoInfo, new[] { fooTwoTemplateData }); + var packThree = new TemplatePackageSearchData(packThreeInfo, new[] { barCSharpTemplateData, barFSharpTemplateData }); + + var cache = new TemplateSearchCache(new[] { packOne, packTwo, packThree }); + + string targetPath = Path.Combine(TestUtils.CreateTemporaryFolder(), "searchCacheV2.json"); + File.WriteAllText(targetPath, cache.ToJObject().ToString()); + return targetPath; + } + + private class MockEnvironment : IEnvironment + { + private readonly Dictionary _envVars = new Dictionary(); + private readonly IReadOnlyList _fallbackVars = new[] { "USERPROFILE", "HOME" }; + + public string NewLine => Environment.NewLine; + + public int ConsoleBufferWidth => 100; + + public void SetEnvironmentVariable(string name, string? value) + { + if (value != null) + { + _envVars[name] = value; + } + else + { + _envVars.Remove(name); + } + } + + //not supported as the mock, but not needed. + public string ExpandEnvironmentVariables(string name) => Environment.ExpandEnvironmentVariables(name); + + public string? GetEnvironmentVariable(string name) + { + if (_fallbackVars.Contains(name)) + { + return Environment.GetEnvironmentVariable(name); + } + + if (_envVars.TryGetValue(name, out string? value)) + { + return value; + } + return null; + } + + public IReadOnlyDictionary GetEnvironmentVariables() + { + return _envVars; + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetTemplateSearchInfo.json b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetTemplateSearchInfo.json new file mode 100644 index 000000000000..ae146c55634f --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetTemplateSearchInfo.json @@ -0,0 +1,138 @@ +{ + "Version": "1.0.0.3", + "TemplateCache": [ + { + "ConfigMountPointId": "9c7aced3-f3b1-45b3-93a9-8d6f19d000f3", + "Author": "Microsoft", + "Classifications": [ + "Azure Functions", + "Solution" + ], + "DefaultName": "Company.FunctionApp", + "Description": "", + "Identity": "Microsoft.AzureFunctions.ProjectTemplate.CSharp.3.x", + "GeneratorId": "0c434df7-e2cb-4dee-b216-d7c58c8eb4b3", + "GroupIdentity": "Microsoft.AzureFunctions.ProjectTemplates", + "Precedence": 100, + "Name": "Azure Functions", + "ShortNameList": [ + "func" + ], + "Tags": { + "language": { + "Description": null, + "ChoicesAndDescriptions": { + "C#": "" + }, + "DefaultValue": "C#" + }, + "type": { + "Description": null, + "ChoicesAndDescriptions": { + "project": "" + }, + "DefaultValue": "project" + } + }, + "CacheParameters": { + "name": { + "DataType": "string", + "DefaultValue": null, + "Description": "The default name symbol" + }, + "StorageConnectionStringValue": { + "DataType": "AzureStorage", + "DefaultValue": "UseDevelopmentStorage=true", + "Description": "The connection string for your Azure WebJobs Storage." + }, + "AzureFunctionsVersion": { + "DataType": null, + "DefaultValue": "V3", + "Description": "The setting that determines the target release" + } + }, + "ConfigPlace": "/content/ProjectTemplate-CSharp/.template.config/template.json", + "LocaleConfigMountPointId": "00000000-0000-0000-0000-000000000000", + "LocaleConfigPlace": null, + "HostConfigMountPointId": "9c7aced3-f3b1-45b3-93a9-8d6f19d000f3", + "HostConfigPlace": "/content/ProjectTemplate-CSharp/.template.config/dotnetcli.host.json", + "ThirdPartyNotices": null, + "BaselineInfo": {}, + "HasScriptRunningPostActions": false, + "ConfigTimestampUtc": null + }, + { + "ConfigMountPointId": "9c7aced3-f3b1-45b3-93a9-8d6f19d000f3", + "Author": "Microsoft", + "Classifications": [ + "Azure Functions", + "Solution" + ], + "DefaultName": "Company.FunctionApp", + "Description": "", + "Identity": "Microsoft.AzureFunctions.ProjectTemplate.FSharp.3.x", + "GeneratorId": "0c434df7-e2cb-4dee-b216-d7c58c8eb4b3", + "GroupIdentity": "Microsoft.AzureFunctions.ProjectTemplates", + "Precedence": 100, + "Name": "Azure Functions", + "ShortNameList": [ + "func" + ], + "Tags": { + "language": { + "Description": null, + "ChoicesAndDescriptions": { + "F#": "" + }, + "DefaultValue": "F#" + }, + "type": { + "Description": null, + "ChoicesAndDescriptions": { + "project": "" + }, + "DefaultValue": "project" + } + }, + "CacheParameters": { + "name": { + "DataType": "string", + "DefaultValue": null, + "Description": "The default name symbol" + }, + "StorageConnectionStringValue": { + "DataType": "AzureStorage", + "DefaultValue": "UseDevelopmentStorage=True", + "Description": "The connection string for your Azure WebJobs Storage." + } + }, + "ConfigPlace": "/content/ProjectTemplate-FSharp/.template.config/template.json", + "LocaleConfigMountPointId": "00000000-0000-0000-0000-000000000000", + "LocaleConfigPlace": null, + "HostConfigMountPointId": "9c7aced3-f3b1-45b3-93a9-8d6f19d000f3", + "HostConfigPlace": "/content/ProjectTemplate-FSharp/.template.config/dotnetcli.host.json", + "ThirdPartyNotices": null, + "BaselineInfo": {}, + "HasScriptRunningPostActions": false, + "ConfigTimestampUtc": null + } + ], + "PackToTemplateMap": { + "Microsoft.Azure.WebJobs.ProjectTemplates": { + "Version": "3.1.1791", + "TotalDownloads": 3891162, + "TemplateIdentificationEntry": [ + { + "Identity": "Microsoft.AzureFunctions.ProjectTemplate.CSharp.3.x", + "GroupIdentity": "Microsoft.AzureFunctions.ProjectTemplates" + }, + { + "Identity": "Microsoft.AzureFunctions.ProjectTemplate.FSharp.3.x", + "GroupIdentity": "Microsoft.AzureFunctions.ProjectTemplates" + } + ] + } + }, + "AdditionalData": { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetTemplateSearchInfoWithInvalidData.json b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetTemplateSearchInfoWithInvalidData.json new file mode 100644 index 000000000000..c8c7956b31eb --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetTemplateSearchInfoWithInvalidData.json @@ -0,0 +1,136 @@ +{ + "Version": "1.0.0.3", + "TemplateCache": [ + { + "ConfigMountPointId": "9c7aced3-f3b1-45b3-93a9-8d6f19d000f3", + "Author": "Microsoft", + "Classifications": [ + "Azure Functions", + "Solution" + ], + "DefaultName": "Company.FunctionApp", + "Description": "", + "Identity": "Microsoft.AzureFunctions.ProjectTemplate.CSharp.3.x", + "GeneratorId": "0c434df7-e2cb-4dee-b216-d7c58c8eb4b3", + "GroupIdentity": "Microsoft.AzureFunctions.ProjectTemplates", + "Precedence": 100, + "Name": "Azure Functions", + "ShortNameList": [ + "func" + ], + "Tags": { + "language": { + "Description": null, + "ChoicesAndDescriptions": { + "C#": "" + }, + "DefaultValue": "C#" + }, + "type": { + "Description": null, + "ChoicesAndDescriptions": { + "project": "" + }, + "DefaultValue": "project" + } + }, + "CacheParameters": { + "name": { + "DataType": "string", + "DefaultValue": null, + "Description": "The default name symbol" + }, + "StorageConnectionStringValue": { + "DataType": "AzureStorage", + "DefaultValue": "UseDevelopmentStorage=true", + "Description": "The connection string for your Azure WebJobs Storage." + }, + "AzureFunctionsVersion": { + "DataType": null, + "DefaultValue": "V3", + "Description": "The setting that determines the target release" + } + }, + "ConfigPlace": "/content/ProjectTemplate-CSharp/.template.config/template.json", + "LocaleConfigMountPointId": "00000000-0000-0000-0000-000000000000", + "LocaleConfigPlace": null, + "HostConfigMountPointId": "9c7aced3-f3b1-45b3-93a9-8d6f19d000f3", + "HostConfigPlace": "/content/ProjectTemplate-CSharp/.template.config/dotnetcli.host.json", + "ThirdPartyNotices": null, + "BaselineInfo": {}, + "HasScriptRunningPostActions": false, + "ConfigTimestampUtc": null + }, + { + "ConfigMountPointId": "9c7aced3-f3b1-45b3-93a9-8d6f19d000f3", + "Author": "Microsoft", + "Classifications": [ + "Azure Functions", + "Solution" + ], + "DefaultName": "Company.FunctionApp", + "Description": "", + "Identity": "Microsoft.AzureFunctions.ProjectTemplate.FSharp.3.x", + "GeneratorId": "0c434df7-e2cb-4dee-b216-d7c58c8eb4b3", + "GroupIdentity": "Microsoft.AzureFunctions.ProjectTemplates", + "Precedence": 100, + "Name": "", + "ShortNameList": [], + "Tags": { + "language": { + "Description": null, + "ChoicesAndDescriptions": { + "F#": "" + }, + "DefaultValue": "F#" + }, + "type": { + "Description": null, + "ChoicesAndDescriptions": { + "project": "" + }, + "DefaultValue": "project" + } + }, + "CacheParameters": { + "name": { + "DataType": "string", + "DefaultValue": null, + "Description": "The default name symbol" + }, + "StorageConnectionStringValue": { + "DataType": "AzureStorage", + "DefaultValue": "UseDevelopmentStorage=True", + "Description": "The connection string for your Azure WebJobs Storage." + } + }, + "ConfigPlace": "/content/ProjectTemplate-FSharp/.template.config/template.json", + "LocaleConfigMountPointId": "00000000-0000-0000-0000-000000000000", + "LocaleConfigPlace": null, + "HostConfigMountPointId": "9c7aced3-f3b1-45b3-93a9-8d6f19d000f3", + "HostConfigPlace": "/content/ProjectTemplate-FSharp/.template.config/dotnetcli.host.json", + "ThirdPartyNotices": null, + "BaselineInfo": {}, + "HasScriptRunningPostActions": false, + "ConfigTimestampUtc": null + } + ], + "PackToTemplateMap": { + "Microsoft.Azure.WebJobs.ProjectTemplates": { + "Version": "3.1.1791", + "TotalDownloads": 3891162, + "TemplateIdentificationEntry": [ + { + "Identity": "Microsoft.AzureFunctions.ProjectTemplate.CSharp.3.x", + "GroupIdentity": "Microsoft.AzureFunctions.ProjectTemplates" + }, + { + "Identity": "Microsoft.AzureFunctions.ProjectTemplate.FSharp.3.x", + "GroupIdentity": "Microsoft.AzureFunctions.ProjectTemplates" + } + ] + } + }, + "AdditionalData": { + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetTemplateSearchInfo_v2.json b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetTemplateSearchInfo_v2.json new file mode 100644 index 000000000000..8eba20ee4f78 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/NuGetTemplateSearchInfo_v2.json @@ -0,0 +1,114 @@ +{ + "TemplatePackages": [ + { + "Name": "Microsoft.Azure.WebJobs.ProjectTemplates", + "Version": "3.1.1812", + "TotalDownloads": 4429007, + "Templates": [ + { + "Identity": "Microsoft.AzureFunctions.ProjectTemplate.CSharp.3.x", + "GroupIdentity": "Microsoft.AzureFunctions.ProjectTemplates", + "Precedence": 100, + "Name": "Azure Functions", + "ShortNameList": [ + "func" + ], + "Author": "Microsoft", + "Classifications": [ + "Azure Functions", + "Solution" + ], + "TagsCollection": { + "language": "C#", + "type": "project" + }, + "Parameters": [ + { + "Name": "StorageConnectionStringValue", + "DataType": "AzureStorage", + "Description": "The connection string for your Azure WebJobs Storage.", + "Priority": 2 + }, + { + "Name": "AzureFunctionsVersion", + "DataType": "string", + "Description": "The setting that determines the target release", + "Priority": 2 + } + ] + }, + { + "Identity": "Microsoft.AzureFunctions.ProjectTemplate.FSharp.3.x", + "GroupIdentity": "Microsoft.AzureFunctions.ProjectTemplates", + "Precedence": 100, + "Name": "Azure Functions", + "ShortNameList": [ + "func" + ], + "Author": "Microsoft", + "Classifications": [ + "Azure Functions", + "Solution" + ], + "TagsCollection": { + "language": "F#", + "type": "project" + }, + "Parameters": [ + { + "Name": "StorageConnectionStringValue", + "DataType": "AzureStorage", + "Description": "The connection string for your Azure WebJobs Storage.", + "Priority": 2 + } + ] + }, + { + "Identity": "Azure.Function.CSharp.HttpTrigger.2.x", + "GroupIdentity": "Azure.Function.HttpTrigger", + "Name": "HttpTrigger", + "ShortNameList": [ + "http" + ], + "Author": "Microsoft", + "Classifications": [ + "Azure Function", + "Trigger", + "Http" + ], + "TagsCollection": { + "language": "C#", + "type": "item", + "AccessRights": "Function" + }, + "Parameters": [ + { + "Name": "AccessRights", + "DataType": "choice", + "Choices": { + "Function": { + "Description": "Function" + }, + "Anonymous": { + "Description": "Anonymous" + }, + "Admin": { + "Description": "Admin" + } + } + }, + { + "Name": "name", + "DataType": "string" + }, + { + "Name": "namespace", + "DataType": "string" + } + ] + } + ] + } + ], + "version": "2.0" +} diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/TemplateSearchCacheReaderTests.cs b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/TemplateSearchCacheReaderTests.cs new file mode 100644 index 000000000000..a3b2a1642333 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/TemplateSearchCacheReaderTests.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using FakeItEasy; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.TemplateEngine; +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateSearch.Common.Abstractions; +using Microsoft.TemplateSearch.Common.Providers; +using Xunit; + +namespace Microsoft.TemplateSearch.Common.UnitTests +{ + public class TemplateSearchCacheReaderTests : IClassFixture + { + private readonly EnvironmentSettingsHelper _environmentSettingsHelper; + + public TemplateSearchCacheReaderTests(EnvironmentSettingsHelper helper) + { + _environmentSettingsHelper = helper; + } + + [Fact] + public void CanReadSearchMetadata() + { + var environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + string content = File.ReadAllText("NuGetTemplateSearchInfo.json"); + JsonObject cache = JExtensions.ParseJsonObject(content); + + var parsedCache = TemplateSearchCache.FromJObject(cache, environmentSettings.Host.Logger); + + Assert.Single(parsedCache.TemplatePackages); + Assert.Equal(2, parsedCache.TemplatePackages.Sum(p => p.Templates.Count)); + + Assert.IsAssignableFrom(parsedCache.TemplatePackages[0].Templates[0]); + + //can read tags + Assert.Equal(2, ((ITemplateInfo)parsedCache.TemplatePackages[0].Templates[0]).TagsCollection.Count); + + //can read parameters + Assert.Equal(5, ((ITemplateInfo)parsedCache.TemplatePackages[0].Templates[0]).ParameterDefinitions.Count); + } + + [Fact] + public void CanReadSearchMetadata_V2() + { + var environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + string content = File.ReadAllText("NuGetTemplateSearchInfo_v2.json"); + JsonObject cache = JExtensions.ParseJsonObject(content); + + var parsedCache = TemplateSearchCache.FromJObject(cache, environmentSettings.Host.Logger); + + Assert.Single(parsedCache.TemplatePackages); + Assert.Equal(3, parsedCache.TemplatePackages.Sum(p => p.Templates.Count)); + + Assert.IsAssignableFrom(parsedCache.TemplatePackages[0].Templates[0]); + + //can read tags + Assert.Equal(2, ((ITemplateInfo)parsedCache.TemplatePackages[0].Templates[0]).TagsCollection.Count); + + //can read parameters: 2 tags + 3 cache parameters + Assert.Equal(2, ((ITemplateInfo)parsedCache.TemplatePackages[0].Templates[0]).ParameterDefinitions.Count); + + Assert.Equal(3, ((ITemplateInfo)parsedCache.TemplatePackages[0].Templates[2]).ParameterDefinitions.Count); + Assert.Equal(1, ((ITemplateInfo)parsedCache.TemplatePackages[0].Templates[2]).ParameterDefinitions.Count(p => p.DataType == "choice")); + Assert.Equal(3, ((ITemplateInfo)parsedCache.TemplatePackages[0].Templates[2]).ParameterDefinitions.Single(p => p.DataType == "choice").Choices?.Count); + } + + [Fact] + public void CanSkipInvalidEntriesSearchMetadata() + { + var environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + string content = File.ReadAllText("NuGetTemplateSearchInfoWithInvalidData.json"); + JsonObject cache = JExtensions.ParseJsonObject(content); + + var parsedCache = TemplateSearchCache.FromJObject(cache, environmentSettings.Host.Logger); + + Assert.Single(parsedCache.TemplatePackages); + Assert.Equal(1, parsedCache.TemplatePackages.Sum(p => p.Templates.Count)); + + Assert.IsAssignableFrom(parsedCache.TemplatePackages[0].Templates[0]); + Assert.Equal("Microsoft.AzureFunctions.ProjectTemplate.CSharp.3.x", ((ITemplateInfo)parsedCache.TemplatePackages[0].Templates[0]).Identity); + } + + [Fact] + public async Task CanReadSearchMetadata_FromBlob() + { + var environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + var sourceFileProvider = new NuGetMetadataSearchProvider( + A.Fake(), + environmentSettings, + new Dictionary>()); + async Task Search() => await sourceFileProvider.GetSearchFileAsync(default); + await TestUtils.AttemptSearch(3, TimeSpan.FromSeconds(10), Search); + string content = environmentSettings.Host.FileSystem.ReadAllText(Path.Combine(environmentSettings.Paths.HostVersionSettingsDir, "nugetTemplateSearchInfo.json")); + var jObj = JExtensions.ParseJsonObject(content); + Assert.NotNull(TemplateSearchCache.FromJObject(jObj, environmentSettings.Host.Logger, null)); + } + + [Fact] + public async Task CanReadLegacySearchMetadata_FromBlob() + { + var environmentSettings = _environmentSettingsHelper.CreateEnvironment(virtualize: true); + var sourceFileProvider = new NuGetMetadataSearchProvider( + A.Fake(), + environmentSettings, + new Dictionary>(), + new[] { "https://go.microsoft.com/fwlink/?linkid=2087906&clcid=0x409" }); //v1 search cache + async Task Search() => await sourceFileProvider.GetSearchFileAsync(default); + await TestUtils.AttemptSearch(3, TimeSpan.FromSeconds(10), Search); + string content = environmentSettings.Host.FileSystem.ReadAllText(Path.Combine(environmentSettings.Paths.HostVersionSettingsDir, "nugetTemplateSearchInfo.json")); + var jObj = JExtensions.ParseJsonObject(content); +#pragma warning disable CS0618 // Type or member is obsolete + Assert.True(LegacySearchCacheReader.TryReadDiscoveryMetadata(jObj, environmentSettings.Host.Logger, null, out _)); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void CanReadSearchMetadata_V2_E2E() + { + Guid postAction1 = Guid.NewGuid(); + Guid postAction2 = Guid.NewGuid(); + + ITemplateInfo mockTemplate = new MockTemplateInfo("shortName", "Full Name", "test.identity", "test.group.identity", 100, "test author") + .WithClassifications("test", "asset") + .WithDescription("my test description") + .WithTag("language", "CSharp") + .WithParameters("param1", "param2") + .WithChoiceParameter("choice", "var1", "var2", "var3") + .WithPostActions(postAction1, postAction2); + + TemplateSearchData template = new TemplateSearchData(mockTemplate); + + var mockPackage = A.Fake(); + A.CallTo(() => mockPackage.Name).Returns("pack"); + A.CallTo(() => mockPackage.Version).Returns("packVer"); + A.CallTo(() => mockPackage.Description).Returns("description"); + A.CallTo(() => mockPackage.IconUrl).Returns("https://icon"); + + TemplatePackageSearchData package = new TemplatePackageSearchData(mockPackage, new[] { template }); + TemplateSearchCache cache = new TemplateSearchCache(new[] { package }); + + JsonObject jobj = cache.ToJObject(); + + TemplateSearchCache deserializedCache = TemplateSearchCache.FromJObject(jobj, NullLogger.Instance); + + Assert.Equal("2.0", deserializedCache.Version); + Assert.Single(deserializedCache.TemplatePackages); + Assert.Single(deserializedCache.TemplatePackages[0].Templates); + + Assert.Equal("pack", deserializedCache.TemplatePackages[0].Name); + Assert.Equal("packVer", deserializedCache.TemplatePackages[0].Version); + Assert.Equal("description", deserializedCache.TemplatePackages[0].Description); + Assert.Equal("https://icon", deserializedCache.TemplatePackages[0].IconUrl); + + var templateToTest = deserializedCache.TemplatePackages[0].Templates[0]; + + Assert.Equal("shortName", templateToTest.ShortNameList[0]); + Assert.Equal("Full Name", templateToTest.Name); + Assert.Equal("test.identity", templateToTest.Identity); + Assert.Equal("test.group.identity", templateToTest.GroupIdentity); + Assert.Equal(100, templateToTest.Precedence); + Assert.Equal("test author", templateToTest.Author); + Assert.Equal(2, templateToTest.Classifications.Count); + + Assert.Equal("my test description", templateToTest.Description); + Assert.Equal("CSharp", templateToTest.TagsCollection["language"]); + + Assert.Equal(3, templateToTest.ParameterDefinitions.Count); + + Assert.Single(templateToTest.ParameterDefinitions, p => p.DataType == "choice"); + Assert.Equal(3, templateToTest.ParameterDefinitions.Single(p => p.DataType == "choice").Choices?.Count); + Assert.True(templateToTest.ParameterDefinitions.Single(p => p.DataType == "choice").Choices?.ContainsKey("var1")); + + Assert.Equal(2, ((ITemplateInfo)templateToTest).PostActions.Count); + Assert.Equal(new[] { postAction1, postAction2 }, ((ITemplateInfo)templateToTest).PostActions); + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/TemplateSearchCoordinatorTests.cs b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/TemplateSearchCoordinatorTests.cs new file mode 100644 index 000000000000..843bdca523d4 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.Common.UnitTests/TemplateSearchCoordinatorTests.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.Abstractions; +using Microsoft.TemplateEngine.Mocks; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateSearch.Common.Abstractions; +using Xunit; + +namespace Microsoft.TemplateSearch.Common.UnitTests +{ + public class TemplateSearchCoordinatorTests : IClassFixture + { + private readonly IEngineEnvironmentSettings _engineEnvironmentSettings; + + public TemplateSearchCoordinatorTests(EnvironmentSettingsHelper environmentSettingsHelper) + { + _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true); + } + + private static readonly ITemplatePackageInfo FooPackInfo = new MockTemplatePackageInfo("fooPack", "1.0.0"); + + private static readonly ITemplatePackageInfo BarPackInfo = new MockTemplatePackageInfo("barPack", "2.0.0"); + + private static readonly ITemplatePackageInfo RedPackInfo = new MockTemplatePackageInfo("redPack", "1.1"); + + private static readonly ITemplatePackageInfo BluePackInfo = new MockTemplatePackageInfo("bluePack", "2.1"); + + private static readonly ITemplatePackageInfo GreenPackInfo = new MockTemplatePackageInfo("greenPack", "3.0.0"); + + [Fact] + public async Task TwoSourcesAreBothSearched() + { + var provider1 = new MockTemplateSearchProvider(); + var provider2 = new MockTemplateSearchProvider(); + _engineEnvironmentSettings.Components.AddComponent( + typeof(ITemplateSearchProviderFactory), + new MockTemplateSearchProviderFactory(Guid.NewGuid(), "provider1", provider1)); + + _engineEnvironmentSettings.Components.AddComponent( + typeof(ITemplateSearchProviderFactory), + new MockTemplateSearchProviderFactory(Guid.NewGuid(), "provider2", provider2)); + + var searchCoordinator = new TemplateSearchCoordinator(_engineEnvironmentSettings); + var searchResult = await searchCoordinator.SearchAsync(p => true, p => p.Templates.ToList(), default); + + Assert.Equal(2, searchResult.Count); + Assert.Single(searchResult, r => r.Provider.Factory.DisplayName == "provider1"); + Assert.Single(searchResult, r => r.Provider.Factory.DisplayName == "provider2"); + + Assert.True(provider2.WasSearched); + } + + [Fact] + public async Task SourcesCorrectlyReturnResults() + { + List createdProviders = new List(); + var sourcesToSetup = GetMockNameSearchResults(); + foreach (var source in sourcesToSetup) + { + var provider = new MockTemplateSearchProvider(); + createdProviders.Add(provider); + provider.Results = source.Value; + _engineEnvironmentSettings.Components.AddComponent( + typeof(ITemplateSearchProviderFactory), + new MockTemplateSearchProviderFactory(Guid.NewGuid(), source.Key, provider)); + } + + const string templateName = "foo"; + + static IReadOnlyList Filter(TemplatePackageSearchData templatePack) => templatePack.Templates + .Where(t => ((ITemplateInfo)t).Name.Contains(templateName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var searchCoordinator = new TemplateSearchCoordinator(_engineEnvironmentSettings); + var searchResult = await searchCoordinator.SearchAsync(p => true, Filter, default); + Assert.Equal(2, searchResult.Count); + + var searchResultDictionary = searchResult.ToDictionary(r => r.Provider.Factory.DisplayName); + + Assert.True(searchResultDictionary.ContainsKey("source one")); + Assert.True(searchResultDictionary.ContainsKey("source two")); + + Assert.Equal(3, searchResultDictionary["source two"].SearchHits.Count); + Assert.Equal(2, searchResultDictionary["source one"].SearchHits.Count); + + Assert.True(createdProviders.All(p => p.WasSearched)); + } + + private static IReadOnlyDictionary MatchedTemplates)>> GetMockNameSearchResults() + { + Dictionary)>> dataForSources = new(); + + ITemplateInfo sourceOneTemplateOne = new MockTemplateInfo("foo1", name: "MockFooTemplateOne", identity: "Mock.Foo.1").WithDescription("Mock Foo template one"); + ITemplateInfo sourceOneTemplateTwo = new MockTemplateInfo("foo2", name: "MockFooTemplateTwo", identity: "Mock.Foo.2").WithDescription("Mock Foo template two"); + ITemplateInfo sourceOneTemplateThree = new MockTemplateInfo("bar1", name: "MockBarTemplateOne", identity: "Mock.Bar.1").WithDescription("Mock Bar template one"); + + var packOne = (FooPackInfo, (IReadOnlyList)new[] { sourceOneTemplateOne, sourceOneTemplateTwo }); + var packTwo = (BarPackInfo, new[] { sourceOneTemplateThree }); + + dataForSources["source one"] = new[] { packOne, packTwo }; + + ITemplateInfo sourceTwoTemplateOne = new MockTemplateInfo("red", name: "MockRedTemplate", identity: "Mock.Red.1").WithDescription("Mock red template"); + ITemplateInfo sourceTwoTemplateTwo = new MockTemplateInfo("blue", name: "MockBlueTemplate", identity: "Mock.Blue.1").WithDescription("Mock blue template"); + ITemplateInfo sourceTwoTemplateThree = new MockTemplateInfo("green", name: "MockGreenTemplate", identity: "Mock.Green.1").WithDescription("Mock green template"); + + var red = (RedPackInfo, (IReadOnlyList)new[] { sourceTwoTemplateOne }); + var blue = (BluePackInfo, new[] { sourceTwoTemplateTwo }); + var green = (GreenPackInfo, new[] { sourceTwoTemplateThree }); + + dataForSources["source two"] = new[] { red, blue, green }; + + return dataForSources; + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests.csproj b/test/TemplateEngine/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests.csproj new file mode 100644 index 000000000000..3707971ea1ac --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests.csproj @@ -0,0 +1,16 @@ + + + + $(NetCurrent) + + + + + + + + + + + + diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests/NuGetTests.cs b/test/TemplateEngine/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests/NuGetTests.cs new file mode 100644 index 000000000000..785dda51e719 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests/NuGetTests.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using FluentAssertions; +using Microsoft.TemplateSearch.TemplateDiscovery.NuGet; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests +{ + public class NuGetTests + { + [Fact] + public async Task CanReadPackageInfo() + { + string nuGetOrgFeed = "https://api.nuget.org/v3/index.json"; + var repository = Repository.Factory.GetCoreV3(nuGetOrgFeed); + ServiceIndexResourceV3 indexResource = repository.GetResource(); + IReadOnlyList searchResources = indexResource.GetServiceEntries("SearchQueryService"); + string queryString = $"{searchResources[0].Uri}?q=Microsoft.DotNet.Common.ProjectTemplates.5.0&skip=0&take=10&prerelease=true&semVerLevel=2.0.0"; + Uri queryUri = new Uri(queryString); + using HttpClient client = new HttpClient(); + using HttpResponseMessage response = await client.GetAsync(queryUri, CancellationToken.None); + if (response.IsSuccessStatusCode) + { + string responseText = await response.Content.ReadAsStringAsync(CancellationToken.None); + + NuGetPackageSearchResult resultsForPage = NuGetPackageSearchResult.FromJObject(JsonNode.Parse(responseText)!.AsObject()); + Assert.Equal(1, resultsForPage.TotalHits); + Assert.Single(resultsForPage.Data); + + var packageInfo = resultsForPage.Data[0]; + + Assert.Equal("Microsoft.DotNet.Common.ProjectTemplates.5.0", packageInfo.Name); + Assert.NotEmpty(packageInfo.Version); + Assert.True(packageInfo.TotalDownloads > 0); + Assert.True(packageInfo.Reserved); + Assert.Contains("Microsoft", packageInfo.Owners); + packageInfo.Description.Should().NotBeNullOrEmpty(); + packageInfo.IconUrl.Should().NotBeNullOrEmpty(); + } + else + { + Assert.Fail("HTTP request failed."); + } + } + } +} diff --git a/test/TemplateEngine/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests/TemplateDiscoveryTests.cs b/test/TemplateEngine/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests/TemplateDiscoveryTests.cs new file mode 100644 index 000000000000..f31041934d24 --- /dev/null +++ b/test/TemplateEngine/Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests/TemplateDiscoveryTests.cs @@ -0,0 +1,584 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Nodes; +using Microsoft.TemplateEngine.CommandUtils; +using Microsoft.TemplateEngine.TestHelper; +using Microsoft.TemplateEngine.Tests; +using Xunit.Abstractions; + +namespace Microsoft.TemplateSearch.TemplateDiscovery.IntegrationTests +{ + public class TemplateDiscoveryTests : TestBase + { + private readonly ITestOutputHelper _log; + + public TemplateDiscoveryTests(ITestOutputHelper log) + { + _log = log; + } + + [Fact] + public async Task CanRunDiscoveryTool() + { + string testDir = TestUtils.CreateTemporaryFolder(); + using var packageManager = new PackageManager(); + string packageLocation = PackTestTemplatesNuGetPackage(packageManager); + packageLocation = await packageManager.GetNuGetPackage("Microsoft.Azure.WebJobs.ProjectTemplates"); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v") + .Execute() + .Should() + .ExitWith(0); + + string[] cacheFilePaths = new[] + { + Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfo.json"), + Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfoVer2.json") + }; + var settingsPath = TestUtils.CreateTemporaryFolder(); + + foreach (var cacheFilePath in cacheFilePaths) + { + Assert.True(File.Exists(cacheFilePath)); + new DotnetNewCommand(_log) + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .WithEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE", "true") + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr(); + + new DotnetNewCommand(_log, "func", "--search") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .WithEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE", "true") + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr() + .And.NotHaveStdOutContaining("Exception") + .And.HaveStdOutContaining("Microsoft.Azure.WebJobs.ProjectTemplates"); + + new DotnetNewCommand(_log) + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .WithEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE", "true") + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr(); + + new DotnetNewCommand(_log, "func", "--search") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .WithEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE", "true") + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr() + .And.NotHaveStdOutContaining("Exception") + .And.HaveStdOutContaining("Microsoft.Azure.WebJobs.ProjectTemplates"); + } + } + + [Fact] + public void CanReadAuthor() + { + string testDir = TestUtils.CreateTemporaryFolder(); + using var packageManager = new PackageManager(); + string packageLocation = PackTestTemplatesNuGetPackage(packageManager); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v") + .Execute() + .Should() + .ExitWith(0); + + var jObjectV1 = JsonNode.Parse(File.ReadAllText(Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfo.json")))!.AsObject(); + Assert.Equal("TestAuthor", jObjectV1!["PackToTemplateMap"]!.AsObject().Single(p => p.Key.StartsWith("Microsoft.TemplateEngine.TestTemplates")).Value!["Owners"]!.AsArray().Select(n => n!.GetValue()).Single()); + var jObjectV2 = JsonNode.Parse(File.ReadAllText(Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfoVer2.json")))!.AsObject(); + Assert.Equal("TestAuthor", jObjectV2!["TemplatePackages"]![0]!["Owners"]!.GetValue()); + } + + [Fact] + public void CanReadDescription() + { + string testDir = TestUtils.CreateTemporaryFolder(); + using var packageManager = new PackageManager(); + string packageLocation = PackTestTemplatesNuGetPackage(packageManager); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v") + .Execute() + .Should() + .ExitWith(0); + + var jObjectV2 = JsonNode.Parse(File.ReadAllText(Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfoVer2.json")))!.AsObject(); + Assert.Equal("description", jObjectV2!["TemplatePackages"]![0]!["Description"]!.GetValue()); + } + + [Fact] + public void CanReadIconUrl() + { + string testDir = TestUtils.CreateTemporaryFolder(); + using var packageManager = new PackageManager(); + string packageLocation = PackTestTemplatesNuGetPackage(packageManager); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v") + .Execute() + .Should() + .ExitWith(0); + + var jObjectV2 = JsonNode.Parse(File.ReadAllText(Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfoVer2.json")))!.AsObject(); + Assert.Equal("https://icon", jObjectV2!["TemplatePackages"]![0]!["IconUrl"]!.GetValue()); + } + + [Fact] + public async Task CanDetectNewPackagesInDiffMode() + { + string testDir = TestUtils.CreateTemporaryFolder(); + using var packageManager = new PackageManager(); + string packageLocation = PackTestTemplatesNuGetPackage(packageManager); + + File.Move(packageLocation, Path.Combine(Path.GetDirectoryName(packageLocation)!, "Test.Templates##1.0.0.nupkg")); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v", + "--diff", + "false") + .Execute() + .Should() + .ExitWith(0) + .And.HaveStdOutContaining( +@"Template packages: + new: 1 + Test.Templates@1.0.0 + updated: 0 + removed: 0 + not changed: 0") + .And.HaveStdOutContaining( +@"Non template packages: + new: 0 + updated: 0 + removed: 0 + not changed: 0"); + + string cacheV1Path = Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfo.json"); + string cacheV2Path = Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfoVer2.json"); + string nonTemplatePackagesList = Path.Combine(testDir, "SearchCache", "nonTemplatePacks.json"); + + Assert.True(File.Exists(cacheV1Path)); + Assert.True(File.Exists(cacheV2Path)); + Assert.True(File.Exists(nonTemplatePackagesList)); + + packageLocation = await packageManager.GetNuGetPackage("Microsoft.Azure.WebJobs.ProjectTemplates"); + + File.Move(packageLocation, Path.Combine(Path.GetDirectoryName(packageLocation)!, "Microsoft.Azure.WebJobs.ProjectTemplates##1.0.0.nupkg")); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v", + "--diff", + "true", + "--diff-override-cache", + cacheV2Path) + .Execute() + .Should() + .ExitWith(0) + .And.HaveStdOutContaining( +@"Template packages: + new: 1 + Microsoft.Azure.WebJobs.ProjectTemplates@1.0.0 + updated: 0 + removed: 0 + not changed: 1") + .And.HaveStdOutContaining( +@"Non template packages: + new: 0 + updated: 0 + removed: 0 + not changed: 0"); + + Assert.True(File.Exists(cacheV1Path)); + Assert.True(File.Exists(cacheV2Path)); + Assert.True(File.Exists(nonTemplatePackagesList)); + + var jObjectV1 = JsonNode.Parse(File.ReadAllText(cacheV1Path))!.AsObject(); + Assert.Equal(2, jObjectV1["PackToTemplateMap"]?.AsObject().Count); + var jObjectV2 = JsonNode.Parse(File.ReadAllText(cacheV2Path))!.AsObject(); + Assert.Equal(2, jObjectV2["TemplatePackages"]?.AsArray().Count); + } + + [Fact] + public void CanDetectUpdatedPackagesInDiffMode() + { + string testDir = TestUtils.CreateTemporaryFolder(); + using var packageManager = new PackageManager(); + string packageLocation = PackTestTemplatesNuGetPackage(packageManager); + + string testFileName = Path.Combine(Path.GetDirectoryName(packageLocation)!, "Test.Templates##1.0.0.nupkg"); + File.Move(packageLocation, testFileName); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v", + "--diff", + "false") + .Execute() + .Should() + .ExitWith(0) + .And.HaveStdOutContaining( +@"Template packages: + new: 1 + Test.Templates@1.0.0 + updated: 0 + removed: 0 + not changed: 0") + .And.HaveStdOutContaining( +@"Non template packages: + new: 0 + updated: 0 + removed: 0 + not changed: 0"); + + string cacheV1Path = Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfo.json"); + string cacheV2Path = Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfoVer2.json"); + string nonTemplatePackagesList = Path.Combine(testDir, "SearchCache", "nonTemplatePacks.json"); + + Assert.True(File.Exists(cacheV1Path)); + Assert.True(File.Exists(cacheV2Path)); + Assert.True(File.Exists(nonTemplatePackagesList)); + + File.Move(testFileName, Path.Combine(Path.GetDirectoryName(testFileName)!, "Test.Templates##1.0.1.nupkg")); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v", + "--diff", + "true", + "--diff-override-cache", + cacheV2Path) + .Execute() + .Should() + .ExitWith(0) + .And.HaveStdOutContaining( +@"Template packages: + new: 0 + updated: 1 + Test.Templates, 1.0.0 --> 1.0.1 + removed: 0 + not changed: 0") + .And.HaveStdOutContaining( +@"Non template packages: + new: 0 + updated: 0 + removed: 0 + not changed: 0"); + + Assert.True(File.Exists(cacheV1Path)); + Assert.True(File.Exists(cacheV2Path)); + Assert.True(File.Exists(nonTemplatePackagesList)); + + var jObjectV1 = JsonNode.Parse(File.ReadAllText(cacheV1Path))!.AsObject(); + Assert.Equal(1, jObjectV1["PackToTemplateMap"]?.AsObject().Count); + var jObjectV2 = JsonNode.Parse(File.ReadAllText(cacheV2Path))!.AsObject(); + Assert.Equal(1, jObjectV2["TemplatePackages"]?.AsArray().Count); + Assert.Equal("1.0.1", jObjectV2["TemplatePackages"]?[0]?["Version"]?.GetValue()); + } + + [Fact] + public void CanDetectRemovedPackagesInDiffMode() + { + string testDir = TestUtils.CreateTemporaryFolder(); + using var packageManager = new PackageManager(); + string packageLocation = PackTestTemplatesNuGetPackage(packageManager); + + string testFileName = Path.Combine(Path.GetDirectoryName(packageLocation)!, "Test.Templates##1.0.0.nupkg"); + File.Move(packageLocation, testFileName); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v", + "--diff", + "false") + .Execute() + .Should() + .ExitWith(0) + .And.HaveStdOutContaining( +@"Template packages: + new: 1 + Test.Templates@1.0.0 + updated: 0 + removed: 0 + not changed: 0") + .And.HaveStdOutContaining( +@"Non template packages: + new: 0 + updated: 0 + removed: 0 + not changed: 0"); + + string cacheV1Path = Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfo.json"); + string cacheV2Path = Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfoVer2.json"); + string nonTemplatePackagesList = Path.Combine(testDir, "SearchCache", "nonTemplatePacks.json"); + + Assert.True(File.Exists(cacheV1Path)); + Assert.True(File.Exists(cacheV2Path)); + Assert.True(File.Exists(nonTemplatePackagesList)); + + File.Delete(testFileName); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v", + "--diff", + "true", + "--diff-override-cache", + cacheV2Path) + .Execute() + .Should() + .ExitWith(0) + .And.HaveStdOutContaining( +@"Template packages: + new: 0 + updated: 0 + removed: 1 + Test.Templates@1.0.0 + not changed: 0") + .And.HaveStdOutContaining( +@"Non template packages: + new: 0 + updated: 0 + removed: 0 + not changed: 0") + .And.HaveStdOutContaining( +@"[Error]: the following 1 packages were removed + Test.Templates@1.0.0 +Checking template packages via API: +Package Test.Templates was unlisted."); + + Assert.True(File.Exists(cacheV1Path)); + Assert.True(File.Exists(cacheV2Path)); + Assert.True(File.Exists(nonTemplatePackagesList)); + + var jObjectV1 = JsonNode.Parse(File.ReadAllText(cacheV1Path))!.AsObject(); + Assert.Equal(0, jObjectV1["PackToTemplateMap"]?.AsObject().Count); + var jObjectV2 = JsonNode.Parse(File.ReadAllText(cacheV2Path))!.AsObject(); + Assert.Equal(0, jObjectV2["TemplatePackages"]?.AsArray().Count); + } + +#pragma warning disable xUnit1004 // Test methods should not be skipped + [Fact(Skip = "Template options filtering is not implemented.")] +#pragma warning restore xUnit1004 // Test methods should not be skipped + public void CanReadCliData() + { + string testDir = TestUtils.CreateTemporaryFolder(); + using var packageManager = new PackageManager(); + string packageLocation = PackTestTemplatesNuGetPackage(packageManager); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v") + .Execute() + .Should() + .ExitWith(0); + + string[] cacheFilePaths = new[] + { + Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfo.json"), + Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfoVer2.json") + }; + var settingsPath = TestUtils.CreateTemporaryFolder(); + CheckTemplateOptionsSearch(cacheFilePaths, settingsPath); + } + +#pragma warning disable xUnit1004 // Test methods should not be skipped + [Fact(Skip = "Template options filtering is not implemented.")] +#pragma warning restore xUnit1004 // Test methods should not be skipped + public void CanReadCliDataFromDiff() + { + string testDir = TestUtils.CreateTemporaryFolder(); + using var packageManager = new PackageManager(); + string packageLocation = PackTestTemplatesNuGetPackage(packageManager); + + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v", + "--diff", + "false") + .Execute() + .Should() + .ExitWith(0); + + string[] cacheFilePaths = new[] + { + Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfo.json"), + Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfoVer2.json") + }; + var settingsPath = TestUtils.CreateTemporaryFolder(); + CheckTemplateOptionsSearch(cacheFilePaths, settingsPath); + + string testDir2 = TestUtils.CreateTemporaryFolder(); + new DotnetCommand( + _log, + "Microsoft.TemplateSearch.TemplateDiscovery.dll", + "--basePath", + testDir2, + "--packagesPath", + Path.GetDirectoryName(packageLocation) ?? throw new Exception("Couldn't get package location directory"), + "-v", + "--diff", + "true", + "--diff-override-cache", + Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfoVer2.json")) + .Execute() + .Should() + .ExitWith(0) + .And.HaveStdOutContaining("not changed: 1"); + + string[] updatedCacheFilePaths = new[] + { + Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfo.json"), + Path.Combine(testDir, "SearchCache", "NuGetTemplateSearchInfoVer2.json") + }; + CheckTemplateOptionsSearch(updatedCacheFilePaths, settingsPath); + } + + private void CheckTemplateOptionsSearch(IEnumerable cacheFilePaths, string settingsPath) + { + foreach (var cacheFilePath in cacheFilePaths) + { + Assert.True(File.Exists(cacheFilePath)); + new DotnetNewCommand(_log) + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .WithEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE", "true") + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr(); + + new DotnetNewCommand(_log, "CliHostFile", "--search") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .WithEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE", "true") + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr() + .And.NotHaveStdOutContaining("Exception") + .And.HaveStdOutContaining("TestAssets.TemplateWithCliHostFile") + .And.HaveStdOutContaining("Microsoft.TemplateEngine.TestTemplates"); + + new DotnetNewCommand(_log, "--search", "--param") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .WithEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE", "true") + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr() + .And.NotHaveStdOutContaining("Exception") + .And.HaveStdOutContaining("TestAssets.TemplateWithCliHostFile") + .And.HaveStdOutContaining("Microsoft.TemplateEngine.TestTemplates"); + + new DotnetNewCommand(_log, "--search", "-p") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .WithEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE", "true") + .Execute() + .Should() + .ExitWith(0) + .And.NotHaveStdErr() + .And.NotHaveStdOutContaining("Exception") + .And.HaveStdOutContaining("TestAssets.TemplateWithCliHostFile") + .And.HaveStdOutContaining("Microsoft.TemplateEngine.TestTemplates"); + + new DotnetNewCommand(_log, "--search", "--test-param") + .WithCustomHive(settingsPath) + .WithoutTelemetry() + .WithEnvironmentVariable("DOTNET_NEW_SEARCH_FILE_OVERRIDE", cacheFilePath) + .WithEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE", "true") + .Execute() + .Should().Fail(); + } + } + } +} diff --git a/test/TemplateEngine/Shared/TestBase.cs b/test/TemplateEngine/Shared/TestBase.cs new file mode 100644 index 000000000000..b833b1ab4ea0 --- /dev/null +++ b/test/TemplateEngine/Shared/TestBase.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.TemplateEngine.TestHelper; + +namespace Microsoft.TemplateEngine.Tests +{ + /// + /// The class contains the utils for unit and integration tests. + /// + public abstract class TestBase + { + internal static string CodeBaseRoot { get; } = GetCodeBaseRoot(); + + internal static string ShippingPackagesLocation + { + get + { +#if DEBUG + string configuration = "Debug"; +#elif RELEASE + string configuration = "Release"; +#else + throw new NotSupportedException("The configuration is not supported"); +#endif + + string packagesLocation = Path.Combine(CodeBaseRoot, "artifacts", "packages", configuration, "Shipping"); + + if (!Directory.Exists(packagesLocation)) + { + throw new Exception($"{packagesLocation} does not exist"); + } + return Path.GetFullPath(packagesLocation); + } + } + + internal static string TemplateFeedLocation { get; } = Path.Combine(CodeBaseRoot, "template_feed"); + + internal static string TestTemplatesLocation { get; } = Path.Combine(CodeBaseRoot, "test", "Microsoft.TemplateEngine.TestTemplates", "test_templates"); + + internal static string SampleTemplatesLocation { get; } = Path.Combine(CodeBaseRoot, "dotnet-template-samples"); + + internal static string TestTemplatePackagesLocation { get; } = Path.Combine(CodeBaseRoot, "test", "Microsoft.TemplateEngine.TestTemplates", "nupkg_templates"); + + internal static string TestPackageProjectPath { get; } = Path.Combine(CodeBaseRoot, "test", "Microsoft.TemplateEngine.TestTemplates", "Microsoft.TemplateEngine.TestTemplates.csproj"); + + internal static string PackTestTemplatesNuGetPackage(PackageManager packageManager) + { + return packageManager.PackNuGetPackage(TestPackageProjectPath); + } + + internal static string GetTestTemplateLocation(string templateName) + { + string templateLocation = Path.Combine(TestTemplatesLocation, templateName); + + if (!Directory.Exists(templateLocation)) + { + throw new Exception($"{templateLocation} does not exist"); + } + return Path.GetFullPath(templateLocation); + } + + private static string GetCodeBaseRoot() + { + string codebase = typeof(TestBase).Assembly.Location; + string? codeBaseRoot = new FileInfo(codebase).Directory?.Parent?.Parent?.Parent?.Parent?.Parent?.FullName; + + if (string.IsNullOrEmpty(codeBaseRoot)) + { + throw new InvalidOperationException("The codebase root was not found"); + } + if (!File.Exists(Path.Combine(codeBaseRoot!, "Microsoft.TemplateEngine.sln"))) + { + throw new InvalidOperationException("Microsoft.TemplateEngine.sln was not found in codebase root"); + } + if (!Directory.Exists(Path.Combine(codeBaseRoot!, "test", "Microsoft.TemplateEngine.TestTemplates"))) + { + throw new InvalidOperationException("Microsoft.TemplateEngine.TestTemplates was not found in test/"); + } + return codeBaseRoot!; + } + } +} diff --git a/test/UnitTests.proj b/test/UnitTests.proj index b3037317f63f..2eef751199e5 100644 --- a/test/UnitTests.proj +++ b/test/UnitTests.proj @@ -19,11 +19,11 @@ - + - +