From b2a91de679c7466ec4af723c3ddc8a6ffc5077cf Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Sat, 21 Sep 2024 12:36:42 -0700 Subject: [PATCH 01/10] Toml support --- .github/workflows/build.yml | 6 + .gitignore | 2 + README.md | 25 +- src/Directory.Packages.props | 29 +- src/Task/AggregateConfigBuildTask.csproj | 2 + .../FileHandlers/ArmParametersFileHandler.cs | 8 +- src/Task/FileHandlers/FileHandlerFactory.cs | 6 +- src/Task/FileHandlers/FileTypeEnum.cs | 7 +- src/Task/FileHandlers/IFileHandler.cs | 2 +- src/Task/FileHandlers/JsonFileHandler.cs | 8 +- src/Task/FileHandlers/TomlFileHandler.cs | 251 ++++++++++++++++ src/Task/FileHandlers/YamlFileHandler.cs | 8 +- src/Task/FileSystem/FileSystem.cs | 21 +- src/Task/FileSystem/IFileSystem.cs | 12 +- src/ThirdPartyNotices.txt | 29 +- src/UnitTests/Data/DemoData.cs | 98 +++++++ src/UnitTests/TaskTestBase.cs | 274 +++++++++--------- src/UnitTests/UnitTests.csproj | 1 + src/UnitTests/VirtualFileSystem.cs | 23 +- 19 files changed, 601 insertions(+), 211 deletions(-) create mode 100644 src/Task/FileHandlers/TomlFileHandler.cs create mode 100644 src/UnitTests/Data/DemoData.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22456b0..9b13bc7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,6 +54,12 @@ jobs: - name: Run tests for AggregateConfigBuildTask solution run: dotnet test src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ steps.get_version.outputs.VERSION }} -p:CollectCoverage=true + - name: Upload TestResults artifact + uses: actions/upload-artifact@v4 + with: + name: TestResults + path: src\UnitTests\TestResults + - name: Upload NuGetPackage artifact uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index dda79a5..cad43c7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ output*.json _site/ docs/ src/.manifest +src/UnitTests/TestResults +.vscode diff --git a/README.md b/README.md index 34476e6..b31cd6c 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ [![NuGet Version](https://img.shields.io/nuget/v/AggregateConfigBuildTask)](https://www.nuget.org/packages/AggregateConfigBuildTask) [![GitHub Build Status](https://img.shields.io/github/actions/workflow/status/richardsondev/AggregateConfigBuildTask/build.yml?branch=main )](https://github.com/richardsondev/AggregateConfigBuildTask/actions/workflows/build.yml?query=branch%3Amain) -**AggregateConfigBuildTask** is a cross-platform MSBuild task that aggregates and transforms configuration files into more consumable formats like JSON, Azure ARM template parameters, YAML during the build process. +**AggregateConfigBuildTask** is a cross-platform MSBuild task that aggregates and transforms configuration files into more consumable formats like JSON, Azure ARM template parameters, YAML, and TOML during the build process. ## Features -- Merge multiple configuration files into a single output format (JSON, Azure ARM parameters, or YAML). +- Merge multiple configuration files into a single output format (JSON, Azure ARM parameters, YAML, or TOML). - Support for injecting custom metadata (e.g., `ResourceGroup`, `Environment`) into the output. - Optionally include the source file name in each configuration entry. - Embed output files as resources in the assembly for easy inclusion in your project. @@ -39,9 +39,9 @@ Alternatively, add the following line to your `.csproj` file: | Parameter | Description | Supported Values | Default | |----------|----------|----------|----------| | **OutputFile**
*(Required)* | The file path to write output to. Should include the extension. | | | -| **OutputType**
*(Required)* | Specifies the format of the output file. | `Json`, `Arm`, `Yaml` | | +| **OutputType**
*(Required)* | Specifies the format of the output file. | `Json`, `Arm`, `Yaml`, `Toml` | | | **InputDirectory**
*(Required)* | The directory containing the files that need to be aggregated. | | | -| **InputType** | Specifies the format of the input files. Refer to the [File Types](#file-types) table below for the corresponding file extensions that will be searched for. | `Json`, `Arm`, `Yaml` | `Yaml` | +| **InputType** | Specifies the format of the input files. Refer to the [File Types](#file-types) table below for the corresponding file extensions that will be searched for. | `Json`, `Arm`, `Yaml`, `Toml` | `Yaml` | | **AddSourceProperty** | Adds a `source` property to each object in the output, specifying the filename from which the object originated. | `true`, `false` | `false` | | **AdditionalProperties** | A set of custom top-level properties to include in the final output. Use `ItemGroup` syntax to define key-value pairs. See [below](#additional-properties) for usage details. | | | | **IsQuietMode** | When true, only warning and error logs are generated by the task, suppressing standard informational output. | `true`, `false` | `false` | @@ -54,6 +54,7 @@ The `InputDirectory` will be scanned for files based on the specified `InputType |---------------|------------------------| | `Json` | `.json` | | `Arm` | `.json` | +| `Toml` | `.toml` | | `Yaml` | `.yml`, `.yaml` | ## Usage @@ -367,13 +368,17 @@ This project is licensed under the MIT License. See the [LICENSE](https://github This project leverages the following third-party libraries that are bundled with the package: -- **[YamlDotNet](https://github.com/aaubry/YamlDotNet)**\ - __Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Antoine Aubry and contributors__\ - Used for YAML serialization and deserialization. YamlDotNet is distributed under the MIT License. For detailed information, refer to the [YamlDotNet License](https://github.com/aaubry/YamlDotNet/blob/master/LICENSE.txt). +- **[YamlDotNet](https://github.com/aaubry/YamlDotNet)** + _Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Antoine Aubry and contributors_ + Used for YAML serialization and deserialization. YamlDotNet is distributed under the MIT License. For details, refer to the [YamlDotNet License](https://github.com/aaubry/YamlDotNet/blob/master/LICENSE.txt). -- **[YamlDotNet.System.Text.Json](https://github.com/IvanJosipovic/YamlDotNet.System.Text.Json)**\ - __Copyright (c) 2022 Ivan Josipovic__\ - Facilitates type handling for YAML serialization and deserialization, enhancing compatibility with System.Text.Json. This library is also distributed under the MIT License. For more details, see the [YamlDotNet.System.Text.Json License](https://github.com/IvanJosipovic/YamlDotNet.System.Text.Json/blob/main/LICENSE). +- **[YamlDotNet.System.Text.Json](https://github.com/IvanJosipovic/YamlDotNet.System.Text.Json)** + _Copyright (c) 2022 Ivan Josipovic_ + Facilitates type handling for YAML serialization and deserialization with System.Text.Json. Distributed under the MIT License. For more details, see the [YamlDotNet.System.Text.Json License](https://github.com/IvanJosipovic/YamlDotNet.System.Text.Json/blob/main/LICENSE). + +- **[Tommy](https://github.com/dezhidki/Tommy)** + _Copyright (c) 2020 Denis Zhidkikh_ + Tommy provides support for working with TOML files in .NET applications. Distributed under the MIT License. See the full license at the [Tommy License](https://github.com/dezhidki/Tommy/blob/master/LICENSE). ## Contributing diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 87f6689..3bb58aa 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -4,30 +4,31 @@ - - - + + + - - - - + + + + + - - + + - - + + - + - + - + \ No newline at end of file diff --git a/src/Task/AggregateConfigBuildTask.csproj b/src/Task/AggregateConfigBuildTask.csproj index 78079ea..e4f5ee1 100644 --- a/src/Task/AggregateConfigBuildTask.csproj +++ b/src/Task/AggregateConfigBuildTask.csproj @@ -45,6 +45,7 @@ + @@ -52,6 +53,7 @@ + diff --git a/src/Task/FileHandlers/ArmParametersFileHandler.cs b/src/Task/FileHandlers/ArmParametersFileHandler.cs index 48b2077..a565b19 100644 --- a/src/Task/FileHandlers/ArmParametersFileHandler.cs +++ b/src/Task/FileHandlers/ArmParametersFileHandler.cs @@ -6,7 +6,9 @@ namespace AggregateConfigBuildTask.FileHandlers { - /// + /// + /// Handles reading and writing ARM template parameter JSON files. + /// public class ArmParametersFileHandler : IFileHandler { private readonly IFileSystem fileSystem; @@ -51,7 +53,7 @@ public async ValueTask ReadInput(string inputPath) } /// - public void WriteOutput(JsonElement? mergedData, string outputPath) + public async Task WriteOutput(JsonElement? mergedData, string outputPath) { if (mergedData.HasValue && mergedData.Value.ValueKind == JsonValueKind.Object) { @@ -76,7 +78,7 @@ public void WriteOutput(JsonElement? mergedData, string outputPath) ["parameters"] = parameters }; var jsonContent = JsonSerializer.Serialize(armTemplate, jsonOptions); - fileSystem.WriteAllText(outputPath, jsonContent); + await fileSystem.WriteAllTextAsync(outputPath, jsonContent).ConfigureAwait(false); } else { diff --git a/src/Task/FileHandlers/FileHandlerFactory.cs b/src/Task/FileHandlers/FileHandlerFactory.cs index bdc9d4c..e47ae52 100644 --- a/src/Task/FileHandlers/FileHandlerFactory.cs +++ b/src/Task/FileHandlers/FileHandlerFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace AggregateConfigBuildTask.FileHandlers @@ -25,6 +25,8 @@ internal static IFileHandler GetFileHandlerForType(IFileSystem fileSystem, FileT return new YamlFileHandler(fileSystem); case FileType.Arm: return new ArmParametersFileHandler(fileSystem); + case FileType.Toml: + return new TomlFileHandler(fileSystem); default: throw new ArgumentException("Unsupported format"); } @@ -46,6 +48,8 @@ internal static List GetExpectedFileExtensions(FileType inputType) return new List { ".yml", ".yaml" }; case FileType.Arm: return new List { ".json" }; + case FileType.Toml: + return new List { ".toml" }; default: throw new ArgumentException("Unsupported input type"); } diff --git a/src/Task/FileHandlers/FileTypeEnum.cs b/src/Task/FileHandlers/FileTypeEnum.cs index 993eb81..c9bd451 100644 --- a/src/Task/FileHandlers/FileTypeEnum.cs +++ b/src/Task/FileHandlers/FileTypeEnum.cs @@ -1,4 +1,4 @@ -namespace AggregateConfigBuildTask +namespace AggregateConfigBuildTask { /// /// Enum representing different file types supported for merging and processing. @@ -29,5 +29,10 @@ public enum FileType /// Alias for the file type, for files with the .yaml extension. /// Yaml = Yml, + + /// + /// Represents a TOML (Tom's Obvious, Minimal Language) file type. + /// + Toml = 3, } } diff --git a/src/Task/FileHandlers/IFileHandler.cs b/src/Task/FileHandlers/IFileHandler.cs index 8a43edf..8200b61 100644 --- a/src/Task/FileHandlers/IFileHandler.cs +++ b/src/Task/FileHandlers/IFileHandler.cs @@ -21,6 +21,6 @@ public interface IFileHandler /// /// The intermediate data in format. Can be null. /// The path to the output file where the data will be written. - void WriteOutput(JsonElement? mergedData, string outputPath); + Task WriteOutput(JsonElement? mergedData, string outputPath); } } diff --git a/src/Task/FileHandlers/JsonFileHandler.cs b/src/Task/FileHandlers/JsonFileHandler.cs index 6ceb2b3..cef4584 100644 --- a/src/Task/FileHandlers/JsonFileHandler.cs +++ b/src/Task/FileHandlers/JsonFileHandler.cs @@ -3,7 +3,9 @@ namespace AggregateConfigBuildTask.FileHandlers { - /// + /// + /// Handles reading and writing JSON files. + /// public class JsonFileHandler : IFileHandler { readonly IFileSystem fileSystem; @@ -25,10 +27,10 @@ public async ValueTask ReadInput(string inputPath) } /// - public void WriteOutput(JsonElement? mergedData, string outputPath) + public Task WriteOutput(JsonElement? mergedData, string outputPath) { var jsonContent = JsonSerializer.Serialize(mergedData, jsonOptions); - fileSystem.WriteAllText(outputPath, jsonContent); + return fileSystem.WriteAllTextAsync(outputPath, jsonContent); } } } diff --git a/src/Task/FileHandlers/TomlFileHandler.cs b/src/Task/FileHandlers/TomlFileHandler.cs new file mode 100644 index 0000000..0e9d33c --- /dev/null +++ b/src/Task/FileHandlers/TomlFileHandler.cs @@ -0,0 +1,251 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Tommy; +using System.IO; +using System.Collections.Generic; + +namespace AggregateConfigBuildTask.FileHandlers +{ + /// + /// Handles reading and writing TOML files by converting between TOML and JSON structures. + /// + public class TomlFileHandler : IFileHandler + { + private readonly IFileSystem fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The file system abstraction used for reading and writing files. + internal TomlFileHandler(IFileSystem fileSystem) + { + this.fileSystem = fileSystem; + } + + /// + /// Reads the input TOML file, converts its contents to a . + /// + /// The path to the TOML file. + /// A representing the JSON data as a . + /// Thrown when the input file is not found. + public async ValueTask ReadInput(string inputPath) + { + string tomlContent = await fileSystem.ReadAllTextAsync(inputPath).ConfigureAwait(false); + + // Parse the TOML content using Tommy + using (var reader = new StringReader(tomlContent)) + { + TomlTable tomlTable = TOML.Parse(reader); + return ConvertTomlToJsonElement(tomlTable); + } + } + + /// + /// Writes the given to a TOML file at the specified output path. + /// + /// The JSON data to be written as TOML. + /// The path where the TOML file should be written. + /// Thrown when the provided JSON data is null. + public Task WriteOutput(JsonElement? mergedData, string outputPath) + { + if (mergedData == null) + { + throw new ArgumentNullException(nameof(mergedData), "The merged data cannot be null."); + } + + // Convert the JsonElement to a TOML table using Tommy + TomlTable tomlTable = ConvertJsonElementToToml(mergedData.Value); + + string tomlString; + using (var writer = new StringWriter()) + { + tomlTable.WriteTo(writer); + tomlString = writer.ToString(); + } + + return fileSystem.WriteAllTextAsync(outputPath, tomlString); + } + + /// + /// Converts a to a . + /// + /// The TOML table to be converted. + /// A representing the TOML data. + private static JsonElement ConvertTomlToJsonElement(TomlTable tomlTable) + { + using (var stream = new MemoryStream()) + { + using (var writer = new Utf8JsonWriter(stream)) + { + writer.WriteStartObject(); + + foreach (KeyValuePair kvp in tomlTable.RawTable) + { + WriteTomlNodeToJson(writer, kvp.Key, kvp.Value); + } + + writer.WriteEndObject(); + } + + stream.Position = 0; + using (var jsonDoc = JsonDocument.Parse(stream)) + { + return jsonDoc.RootElement.Clone(); + } + } + } + + /// + /// Converts a to a . + /// + /// The JSON element to be converted to TOML. + /// A representing the JSON data. + private TomlTable ConvertJsonElementToToml(JsonElement jsonElement) + { + TomlTable tomlTable = new TomlTable(); + + foreach (JsonProperty property in jsonElement.EnumerateObject()) + { + AddJsonPropertyToTomlTable(tomlTable, property); + } + + return tomlTable; + } + + /// + /// Writes a TOML node to a . + /// + /// The JSON writer to write the TOML node to. + /// The key of the TOML node. + /// The TOML node to be written. + /// Thrown when a TOML types cannot be converted to a JSON type. + private static void WriteTomlNodeToJson(Utf8JsonWriter writer, string key, TomlNode node) + { + switch (node) + { + case TomlTable tableNode: + writer.WriteStartObject(key); + foreach (KeyValuePair kvp in tableNode.RawTable) + { + WriteTomlNodeToJson(writer, kvp.Key, kvp.Value); + } + writer.WriteEndObject(); + break; + case TomlArray arrayNode: + writer.WriteStartArray(key); + foreach (TomlNode item in arrayNode) + { + WriteTomlNodeToJson(writer, item); + } + writer.WriteEndArray(); + break; + case TomlString stringNode: + writer.WriteString(key, stringNode.Value); + break; + case TomlInteger integerNode: + writer.WriteNumber(key, integerNode.Value); + break; + case TomlFloat floatNode: + writer.WriteNumber(key, floatNode.Value); + break; + case TomlBoolean boolNode: + writer.WriteBoolean(key, boolNode.Value); + break; + case TomlDateTime dateTimeNode: + writer.WriteString(key, dateTimeNode.ToString()); + break; + default: + throw new InvalidOperationException($"Unsupported TOML node type: {node.GetType().Name}"); + } + } + + private static void WriteTomlNodeToJson(Utf8JsonWriter writer, TomlNode node) + { + switch (node) + { + case TomlTable tableNode: + writer.WriteStartObject(); + foreach (KeyValuePair kvp in tableNode.RawTable) + { + WriteTomlNodeToJson(writer, kvp.Key, kvp.Value); + } + writer.WriteEndObject(); + break; + case TomlArray arrayNode: + writer.WriteStartArray(); + foreach (TomlNode item in arrayNode) + { + WriteTomlNodeToJson(writer, item); + } + writer.WriteEndArray(); + break; + case TomlString stringNode: + writer.WriteStringValue(stringNode.Value); + break; + case TomlInteger integerNode: + writer.WriteNumberValue(integerNode.Value); + break; + case TomlFloat floatNode: + writer.WriteNumberValue(floatNode.Value); + break; + case TomlBoolean boolNode: + writer.WriteBooleanValue(boolNode.Value); + break; + case TomlDateTime dateTimeNode: + writer.WriteStringValue(dateTimeNode.ToString()); + break; + default: + throw new InvalidOperationException($"Unsupported TOML node type: {node.GetType().Name}"); + } + } + + /// + /// Recursively adds JSON properties to the TOML table. + /// + /// The TOML table to add the JSON properties to. + /// The JSON property being processed. + /// Thrown when a JSON type cannot be converted to TOML. + private void AddJsonPropertyToTomlTable(TomlTable table, JsonProperty property) + { + switch (property.Value.ValueKind) + { + case JsonValueKind.Object: + var subTable = new TomlTable(); + foreach (JsonProperty subProperty in property.Value.EnumerateObject()) + { + AddJsonPropertyToTomlTable(subTable, subProperty); + } + table[property.Name] = subTable; + break; + case JsonValueKind.Array: + var tomlArray = new TomlArray(); + foreach (JsonElement item in property.Value.EnumerateArray()) + { + tomlArray.Add(ConvertJsonElementToToml(item)); + } + table[property.Name] = tomlArray; + break; + case JsonValueKind.String: + table[property.Name] = property.Value.GetString(); + break; + case JsonValueKind.Number: + if (property.Value.TryGetInt64(out long intValue)) + { + table[property.Name] = intValue; + } + else + { + table[property.Name] = property.Value.GetDouble(); + } + break; + case JsonValueKind.True: + case JsonValueKind.False: + table[property.Name] = property.Value.GetBoolean(); + break; + default: + throw new InvalidOperationException($"Unsupported JSON value type: {property.Value.ValueKind}"); + } + } + } +} diff --git a/src/Task/FileHandlers/YamlFileHandler.cs b/src/Task/FileHandlers/YamlFileHandler.cs index 06bf3aa..a140d01 100644 --- a/src/Task/FileHandlers/YamlFileHandler.cs +++ b/src/Task/FileHandlers/YamlFileHandler.cs @@ -7,7 +7,9 @@ namespace AggregateConfigBuildTask.FileHandlers { - /// + /// + /// Handles reading and writing YAML files by converting between YAML and JSON structures. + /// public class YamlFileHandler : IFileHandler { readonly IFileSystem fileSystem; @@ -34,7 +36,7 @@ public async ValueTask ReadInput(string inputPath) } /// - public void WriteOutput(JsonElement? mergedData, string outputPath) + public Task WriteOutput(JsonElement? mergedData, string outputPath) { var serializer = new SerializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) @@ -42,7 +44,7 @@ public void WriteOutput(JsonElement? mergedData, string outputPath) .WithTypeInspector(x => new SystemTextJsonTypeInspector(x)) .Build(); var yamlContent = serializer.Serialize(mergedData); - fileSystem.WriteAllText(outputPath, yamlContent); + return fileSystem.WriteAllTextAsync(outputPath, yamlContent); } } } diff --git a/src/Task/FileSystem/FileSystem.cs b/src/Task/FileSystem/FileSystem.cs index 3fd4c99..8d98bed 100644 --- a/src/Task/FileSystem/FileSystem.cs +++ b/src/Task/FileSystem/FileSystem.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; namespace AggregateConfigBuildTask { @@ -11,21 +12,21 @@ public string[] GetFiles(string path, string searchPattern) } /// - public string[] ReadAllLines(string path) + public async Task ReadAllTextAsync(string path) { - return File.ReadAllLines(path); + using (var reader = new StreamReader(path)) + { + return await reader.ReadToEndAsync().ConfigureAwait(false); + } } /// - public string ReadAllText(string path) + public async Task WriteAllTextAsync(string path, string text) { - return File.ReadAllText(path); - } - - /// - public void WriteAllText(string path, string text) - { - File.WriteAllText(path, text); + using (var writer = new StreamWriter(path)) + { + await writer.WriteAsync(text).ConfigureAwait(false); + } } /// diff --git a/src/Task/FileSystem/IFileSystem.cs b/src/Task/FileSystem/IFileSystem.cs index da2e91b..60fd2c7 100644 --- a/src/Task/FileSystem/IFileSystem.cs +++ b/src/Task/FileSystem/IFileSystem.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; namespace AggregateConfigBuildTask { @@ -15,26 +16,19 @@ internal interface IFileSystem /// An array of file paths that match the specified search pattern. string[] GetFiles(string path, string searchPattern); - /// - /// Reads all lines from the specified file. - /// - /// The path of the file to read. - /// A string containing all the lines from the file. - string[] ReadAllLines(string path); - /// /// Reads all text from the specified file. /// /// The path of the file to read. /// A string containing all the text from the file. - string ReadAllText(string path); + Task ReadAllTextAsync(string path); /// /// Writes the specified text to the specified file, overwriting the file if it already exists. /// /// The path of the file to write to. /// The text to write to the file. - void WriteAllText(string path, string text); + Task WriteAllTextAsync(string path, string text); /// /// Checks if the specified file exists at the given path. diff --git a/src/ThirdPartyNotices.txt b/src/ThirdPartyNotices.txt index b78dc9a..8b9f3c4 100644 --- a/src/ThirdPartyNotices.txt +++ b/src/ThirdPartyNotices.txt @@ -4,7 +4,8 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION AggregateConfigBuildTask is based on or incorporates material from the projects listed below. 1. YamlDotNet (https://github.com/aaubry/YamlDotNet) -2. YamlDotNet.System.Text.Json (https://github.com/adamabdelhamed/PowerArgs) +2. YamlDotNet.System.Text.Json (https://github.com/IvanJosipovic/YamlDotNet.System.Text.Json) +3. Tommy (https://github.com/dezhidki/Tommy) %% YamlDotNet NOTICES AND INFORMATION BEGIN HERE @@ -58,3 +59,29 @@ SOFTWARE. ========================================= END OF YamlDotNet.System.Text.Json NOTICES AND INFORMATION + +%% Tommy NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2020 Denis Zhidkikh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF Tommy NOTICES AND INFORMATION diff --git a/src/UnitTests/Data/DemoData.cs b/src/UnitTests/Data/DemoData.cs new file mode 100644 index 0000000..1634239 --- /dev/null +++ b/src/UnitTests/Data/DemoData.cs @@ -0,0 +1,98 @@ +using System; + +namespace AggregateConfigBuildTask.Tests.Unit +{ + internal static class DemoData + { + public static string GetSampleDataForType(string type) + { + return type switch + { + "JSON" => """ +{ + "options": [ + { + "name": "Option 1", + "description": "First option", + "isTrue": true, + "number": 100, + "nested": [ + { + "name": "Nested option 1", + "description": "Nested first option", + "isTrue": true, + "number": 1001 + }, + { + "name": "Nested option 2", + "description": "Nested second option" + } + ] + } + ] +} +""", + "ARM" => """ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "options": { + "type": "object", + "value": { + "name": "Option 1", + "description": "First option", + "isTrue": true, + "number": 100, + "nested": [ + { + "name": "Nested option 1", + "description": "Nested first option", + "isTrue": true, + "number": 1002 + }, + { + "name": "Nested option 2", + "description": "Nested second option" + } + ] + } + } + } +} +""", + "YML" => @"options: +- name: Option 1 + description: First option + isTrue: true + number: 100 + nested: + - name: Nested option 1 + description: Nested first option + isTrue: true + number: 1003 + - name: Nested option 2 + description: Nested second option +", + "TOML" => """ +[[options]] +name = "Option 1" +description = "First option" +isTrue = true +number = 100 + +[[options.nested]] +name = "Nested option 1" +description = "Nested first option" +isTrue = true +number = 1004 + +[[options.nested]] +name = "Nested option 2" +description = "Nested second option" +""", + _ => throw new InvalidOperationException($"Unknown type: {type}") + }; + } + } +} diff --git a/src/UnitTests/TaskTestBase.cs b/src/UnitTests/TaskTestBase.cs index 6f42c64..92ba1d1 100644 --- a/src/UnitTests/TaskTestBase.cs +++ b/src/UnitTests/TaskTestBase.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; +using System.Threading.Tasks; using Microsoft.Build.Framework; using Moq; using Newtonsoft.Json; @@ -40,17 +42,17 @@ public void Cleanup() [TestMethod] [Description("Test that YAML files are merged into correct JSON output.")] - public void ShouldGenerateJsonOutput() + public async Task ShouldGenerateJsonOutput() { // Arrange: Prepare sample YAML data in the mock file system. - virtualFileSystem.WriteAllText($"{testPath}\\file1.yml", @" + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.yml", @" options: - name: 'Option 1' - description: 'First option'"); - virtualFileSystem.WriteAllText($"{testPath}\\file2.yml", @" + description: 'First option'").ConfigureAwait(false); + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file2.yml", @" options: - name: 'Option 2' - description: 'Second option'"); + description: 'Second option'").ConfigureAwait(false); var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) { @@ -66,7 +68,7 @@ public void ShouldGenerateJsonOutput() // Assert: Check that output was generated correctly. Assert.IsTrue(result); - string output = virtualFileSystem.ReadAllText($"{testPath}\\output.json"); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.json").ConfigureAwait(false); var json = JsonConvert.DeserializeObject>(output); Assert.IsTrue(json.ContainsKey("options")); Assert.AreEqual(2, ((IEnumerable)json.GetValueOrDefault("options")).Count()); @@ -74,17 +76,17 @@ public void ShouldGenerateJsonOutput() [TestMethod] [Description("Test that YAML files are merged into correct ARM parameter output.")] - public void ShouldGenerateArmParameterOutput() + public async Task ShouldGenerateArmParameterOutput() { // Arrange: Prepare sample YAML data in the mock file system. - virtualFileSystem.WriteAllText($"{testPath}\\file1.yml", @" + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.yml", @" options: - name: 'Option 1' - description: 'First option'"); - virtualFileSystem.WriteAllText($"{testPath}\\file2.yml", @" + description: 'First option'").ConfigureAwait(false); + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file2.yml", @" options: - name: 'Option 2' - description: 'Second option'"); + description: 'Second option'").ConfigureAwait(false); // Create the task instance with the mock file system var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) @@ -101,7 +103,7 @@ public void ShouldGenerateArmParameterOutput() // Assert: Check the ARM output structure Assert.IsTrue(result); - string output = virtualFileSystem.ReadAllText($"{testPath}\\output.parameters.json"); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.parameters.json").ConfigureAwait(false); var armTemplate = JsonConvert.DeserializeObject>(output); Assert.IsTrue(armTemplate.ContainsKey("parameters")); @@ -112,13 +114,13 @@ public void ShouldGenerateArmParameterOutput() [TestMethod] [Description("Test that the source property is correctly added when AddSourceProperty is true.")] - public void ShouldAddSourceProperty() + public async Task ShouldAddSourceProperty() { // Arrange: Prepare sample YAML data with source property enabled. - virtualFileSystem.WriteAllText($"{testPath}\\file1.yml", @" + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.yml", @" options: - name: 'Option 1' - description: 'First option'"); + description: 'First option'").ConfigureAwait(false); // Create the task instance with the mock file system var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) @@ -135,7 +137,7 @@ public void ShouldAddSourceProperty() // Assert: Verify that the source property was added Assert.IsTrue(result); - string output = virtualFileSystem.ReadAllText($"{testPath}\\output.json"); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.json").ConfigureAwait(false); var json = JsonConvert.DeserializeObject>>>(output); Assert.IsTrue(json["options"][0].ContainsKey("source")); Assert.AreEqual("file1", json["options"][0]["source"]); @@ -143,16 +145,16 @@ public void ShouldAddSourceProperty() [TestMethod] [Description("Test that the source property is correctly added for multiple files when AddSourceProperty is true.")] - public void ShouldAddSourcePropertyMultipleFiles() + public async Task ShouldAddSourcePropertyMultipleFiles() { // Arrange: Prepare sample YAML data with source property enabled. - virtualFileSystem.WriteAllText($"{testPath}\\file1.yml", @" + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.yml", @" options: - name: 'Option 1' description: 'First option' additionalOptions: - value: 'Good day'"); - virtualFileSystem.WriteAllText($"{testPath}\\file2.yml", @" + value: 'Good day'").ConfigureAwait(false); + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file2.yml", @" options: - name: 'Option 2' description: 'Second option' @@ -162,7 +164,7 @@ public void ShouldAddSourcePropertyMultipleFiles() value: 'Good night' text: - name: 'Text 1' - description: 'Text'"); + description: 'Text'").ConfigureAwait(false); // Create the task instance with the mock file system var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) @@ -179,7 +181,7 @@ public void ShouldAddSourcePropertyMultipleFiles() // Assert: Verify that the source property was added Assert.IsTrue(result); - string output = virtualFileSystem.ReadAllText($"{testPath}\\output.json"); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.json").ConfigureAwait(false); var json = JsonConvert.DeserializeObject>>>(output); Assert.IsTrue(OptionExistsWithSource(json["options"], "Option 1", "file1")); Assert.IsTrue(OptionExistsWithSource(json["options"], "Option 2", "file2")); @@ -191,13 +193,13 @@ public void ShouldAddSourcePropertyMultipleFiles() [Description("Test that additional properties are correctly added to the top level in JSON output.")] [DataRow(true, DisplayName = "Legacy properties")] [DataRow(false, DisplayName = "Modern properties")] - public void ShouldIncludeAdditionalPropertiesInJson(bool useLegacyAdditionalProperties) + public async Task ShouldIncludeAdditionalPropertiesInJson(bool useLegacyAdditionalProperties) { // Arrange: Prepare sample YAML data. - virtualFileSystem.WriteAllText($"{testPath}\\file1.yml", @" + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.yml", @" options: - name: 'Option 1' - description: 'First option'"); + description: 'First option'").ConfigureAwait(false); var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) { @@ -218,7 +220,7 @@ public void ShouldIncludeAdditionalPropertiesInJson(bool useLegacyAdditionalProp // Assert: Verify additional properties are included Assert.IsTrue(result); - string output = virtualFileSystem.ReadAllText($"{testPath}\\output.json"); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.json").ConfigureAwait(false); var json = JsonConvert.DeserializeObject>(output); Assert.AreEqual("TestRG", json["Group"]); @@ -236,13 +238,13 @@ public void ShouldIncludeAdditionalPropertiesInJson(bool useLegacyAdditionalProp [Description("Test that additional properties are correctly added to the ARM parameters output.")] [DataRow(true, DisplayName = "Legacy properties")] [DataRow(false, DisplayName = "Modern properties")] - public void ShouldIncludeAdditionalPropertiesInArmParameters(bool useLegacyAdditionalProperties) + public async Task ShouldIncludeAdditionalPropertiesInArmParameters(bool useLegacyAdditionalProperties) { // Arrange: Prepare sample YAML data. - virtualFileSystem.WriteAllText($"{testPath}\\file1.yml", @" + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.yml", @" options: - name: 'Option 1' - description: 'First option'"); + description: 'First option'").ConfigureAwait(false); var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) { @@ -263,7 +265,7 @@ public void ShouldIncludeAdditionalPropertiesInArmParameters(bool useLegacyAddit // Assert: Verify additional properties are included in ARM output Assert.IsTrue(result); - string output = virtualFileSystem.ReadAllText($"{testPath}\\output.json"); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.json").ConfigureAwait(false); var armTemplate = JsonConvert.DeserializeObject>(output); JObject parameters = (JObject)armTemplate["parameters"]; Assert.AreEqual("array", parameters.GetValue("options", comparison)["type"].ToString()); @@ -295,13 +297,13 @@ public void ShouldHandleEmptyDirectory() [TestMethod] [Description("Test that the task throws an error when it encounters invalid YAML format.")] - public void ShouldHandleInvalidYamlFormat() + public async Task ShouldHandleInvalidYamlFormat() { // Arrange: Add invalid YAML file to the mock file system. - virtualFileSystem.WriteAllText($"{testPath}\\invalid.yml", @" + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\invalid.yml", @" options: - name: 'Option 1' - description: 'Unclosed value"); + description: 'Unclosed value").ConfigureAwait(false); var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) { @@ -320,14 +322,14 @@ public void ShouldHandleInvalidYamlFormat() [TestMethod] [Description("Test that boolean input values are correctly treated as booleans in the output.")] - public void ShouldCorrectlyParseBooleanValues() + public async Task ShouldCorrectlyParseBooleanValues() { // Arrange: Prepare sample YAML data. - virtualFileSystem.WriteAllText($"{testPath}\\file1.yml", @" + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.yml", @" options: - name: 'Option 1' description: 'First option' - isEnabled: true"); + isEnabled: true").ConfigureAwait(false); var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) { @@ -342,7 +344,7 @@ public void ShouldCorrectlyParseBooleanValues() // Assert: Verify additional properties are included in ARM output Assert.IsTrue(result); - string output = virtualFileSystem.ReadAllText($"{testPath}\\output.json"); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.json").ConfigureAwait(false); var armTemplate = JsonConvert.DeserializeObject>(output); JObject parameters = (JObject)armTemplate["parameters"]; Assert.AreEqual("array", parameters.GetValue("options", comparison)["type"].ToString()); @@ -354,10 +356,10 @@ public void ShouldCorrectlyParseBooleanValues() [Description("Test that additional properties are correctly added to the ARM parameters output from JSON input.")] [DataRow(true, DisplayName = "Legacy properties")] [DataRow(false, DisplayName = "Modern properties")] - public void ShouldIncludeAdditionalPropertiesInJsonInput(bool useLegacyAdditionalProperties) + public async Task ShouldIncludeAdditionalPropertiesInJsonInput(bool useLegacyAdditionalProperties) { // Arrange: Prepare sample JSON data. - virtualFileSystem.WriteAllText($"{testPath}\\file1.json", """ + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.json", """ { "options": [ { @@ -367,7 +369,7 @@ public void ShouldIncludeAdditionalPropertiesInJsonInput(bool useLegacyAdditiona } ] } -"""); +""").ConfigureAwait(false); var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) { @@ -389,7 +391,7 @@ public void ShouldIncludeAdditionalPropertiesInJsonInput(bool useLegacyAdditiona // Assert: Verify additional properties are included in ARM output Assert.IsTrue(result); - string output = virtualFileSystem.ReadAllText($"{testPath}\\output.json"); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.json").ConfigureAwait(false); var armTemplate = JsonConvert.DeserializeObject>(output); JObject parameters = (JObject)armTemplate["parameters"]; Assert.AreEqual("TestRG", parameters.GetValue("Group", comparison)["value"].Value()); @@ -400,14 +402,60 @@ public void ShouldIncludeAdditionalPropertiesInJsonInput(bool useLegacyAdditiona Assert.AreEqual(true, parameters.GetValue("options", comparison)["value"].First()["isEnabled"].Value()); } + [TestMethod] + [Description("Test that additional properties are correctly added to the JSON output from TOML input.")] + [DataRow(true, DisplayName = "Legacy properties")] + [DataRow(false, DisplayName = "Modern properties")] + public async Task ShouldIncludeAdditionalPropertiesInTomlInput(bool useLegacyAdditionalProperties) + { + // Arrange: Prepare sample TOML data. + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.toml", @" + [[options]] + name = 'Option 1' + description = 'First option'").ConfigureAwait(false); + + var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) + { + InputDirectory = testPath, + InputType = nameof(FileType.Toml), + OutputFile = testPath + @"\output.json", + OutputType = nameof(FileType.Json), + AddSourceProperty = true, + AdditionalProperties = new Dictionary + { + { "Group", "TestRG" }, + { "Environment\\=Key", "Prod\\=West" } + }.CreateTaskItems(useLegacyAdditionalProperties), + BuildEngine = Mock.Of() + }; + + // Act: Execute the task + bool result = task.Execute(); + + // Assert: Verify additional properties are included + Assert.IsTrue(result); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.json").ConfigureAwait(false); + var json = JsonConvert.DeserializeObject>(output); + Assert.AreEqual("TestRG", json["Group"]); + + if (useLegacyAdditionalProperties) + { + Assert.AreEqual("Prod=West", json["Environment=Key"]); + } + else + { + Assert.AreEqual("Prod\\=West", json["Environment\\=Key"]); + } + } + [TestMethod] [Description("Test that ARM parameters are correctly processed and additional properties are included in the output.")] [DataRow(true, DisplayName = "Legacy properties")] [DataRow(false, DisplayName = "Modern properties")] - public void ShouldIncludeAdditionalPropertiesInArmParameterFile(bool useLegacyAdditionalProperties) + public async Task ShouldIncludeAdditionalPropertiesInArmParameterFile(bool useLegacyAdditionalProperties) { // Arrange: Prepare ARM template parameter file data in 'file1.parameters.json'. - virtualFileSystem.WriteAllText($"{testPath}\\file1.parameters.json", """ + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.parameters.json", """ { "parameters": { "options": { @@ -422,7 +470,7 @@ public void ShouldIncludeAdditionalPropertiesInArmParameterFile(bool useLegacyAd } } } -"""); +""").ConfigureAwait(false); var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) { @@ -444,7 +492,7 @@ public void ShouldIncludeAdditionalPropertiesInArmParameterFile(bool useLegacyAd // Assert: Verify additional properties are included in ARM output Assert.IsTrue(result); - string output = virtualFileSystem.ReadAllText($"{testPath}\\output.parameters.json"); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.parameters.json").ConfigureAwait(false); var armTemplate = JsonConvert.DeserializeObject>(output); JObject parameters = (JObject)armTemplate["parameters"]; Assert.AreEqual("TestRG", parameters.GetValue("Group", comparison)["value"].Value()); @@ -458,7 +506,7 @@ public void ShouldIncludeAdditionalPropertiesInArmParameterFile(bool useLegacyAd [TestMethod] [Description("Stress test to verify the source property is correctly added for 1,000 files with 10 options each.")] [Timeout(60000)] - public void StressTest_ShouldAddSourcePropertyManyFiles() + public async Task StressTest_ShouldAddSourcePropertyManyFiles() { // Arrange: Prepare sample YAML data. const int totalFiles = 1_000; @@ -476,7 +524,7 @@ public void StressTest_ShouldAddSourcePropertyManyFiles() } // Write each YAML file to the mock file system - virtualFileSystem.WriteAllText($"{testPath}\\file{fileIndex}.yml", sb.ToString()); + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file{fileIndex}.yml", sb.ToString()).ConfigureAwait(false); } var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) @@ -493,7 +541,7 @@ public void StressTest_ShouldAddSourcePropertyManyFiles() // Assert: Verify that the source property was added correctly for all files and options Assert.IsTrue(result); - string output = virtualFileSystem.ReadAllText($"{testPath}\\output.json"); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.json").ConfigureAwait(false); var json = JsonConvert.DeserializeObject>>>(output); int optionIndexInTotal = 0; @@ -508,14 +556,9 @@ public void StressTest_ShouldAddSourcePropertyManyFiles() } [TestMethod] - [DataRow("arm", new[] { "json", "yml", "arm" }, DisplayName = "ARM -> JSON -> YAML -> ARM")] - [DataRow("arm", new[] { "yml", "json", "arm" }, DisplayName = "ARM -> YAML -> JSON -> ARM")] - [DataRow("json", new[] { "arm", "yml", "json" }, DisplayName = "JSON -> ARM -> YAML -> JSON")] - [DataRow("json", new[] { "yml", "arm", "json" }, DisplayName = "JSON -> YAML -> ARM -> JSON")] - [DataRow("yml", new[] { "arm", "json", "yml" }, DisplayName = "YAML -> ARM -> JSON -> YAML")] - [DataRow("yml", new[] { "json", "arm", "yml" }, DisplayName = "YAML -> JSON -> ARM -> YAML")] - [Description("Test that files are correctly translated between ARM, JSON, and YAML.")] - public void ShouldTranslateBetweenFormatsAndValidateNoDataLoss(string inputType, string[] steps) + [DynamicData(nameof(GetFileTypeConversions), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetTestDisplayName))] + [Description("Test that files are correctly translated between ARM, JSON, YAML, and TOML.")] + public async Task ShouldTranslateBetweenFormatsAndValidateNoDataLoss(string inputType, string[] steps, string _) { Assert.IsTrue(steps?.Length > 0); @@ -525,7 +568,7 @@ public void ShouldTranslateBetweenFormatsAndValidateNoDataLoss(string inputType, // Write the initial input file var inputFilePath = $"{inputDir}\\input.{(inputType == "arm" ? "json" : inputType)}"; - virtualFileSystem.WriteAllText(inputFilePath, GetSampleDataForType(inputType)); + await virtualFileSystem.WriteAllTextAsync(inputFilePath, DemoData.GetSampleDataForType(inputType)).ConfigureAwait(false); string previousInputPath = inputFilePath; string previousOutputType = inputType; @@ -555,7 +598,40 @@ public void ShouldTranslateBetweenFormatsAndValidateNoDataLoss(string inputType, ExecuteTranslationTask(previousOutputType, inputType, previousInputPath, finalOutputPath); // Assert: Compare final output with original input to check no data loss - AssertNoDataLoss(inputFilePath, finalOutputPath, inputType); + string originalInput = await virtualFileSystem.ReadAllTextAsync(inputFilePath).ConfigureAwait(false); + string finalOutput = await virtualFileSystem.ReadAllTextAsync(finalOutputPath).ConfigureAwait(false); + Assert.AreEqual(originalInput, finalOutput, $"Data mismatch after full conversion cycle for {inputType}"); + } + + public static IEnumerable GetFileTypeConversions() + { + var fileTypes = Enum.GetValues(typeof(FileType)).Cast().Select(ft => ft.ToString().ToUpperInvariant()).ToArray(); + + foreach (var initialFormat in fileTypes) + { + foreach (var permutation in GetPermutations(fileTypes.Where(ft => ft != initialFormat), 3)) + { + var displayName = $"{initialFormat.ToUpperInvariant()} -> {string.Join(" -> ", permutation.Select(p => p.ToUpperInvariant()))} -> {initialFormat.ToUpperInvariant()}"; + yield return new object[] { initialFormat, permutation.ToArray(), displayName }; + } + } + } + + public static string GetTestDisplayName(MethodInfo methodInfo, object[] data) + { + ArgumentNullException.ThrowIfNull(methodInfo); + ArgumentNullException.ThrowIfNull(data); + + // Get the custom display name from the third parameter. + return (string)data[2]; + } + + private static IEnumerable> GetPermutations(IEnumerable list, int length) + { + if (length == 1) return list.Select(t => new[] { t }); + return GetPermutations(list, length - 1) + .SelectMany(t => list.Where(e => !t.Contains(e)), + (t1, t2) => t1.Concat([t2])); } private void ExecuteTranslationTask(string inputType, string outputType, string inputFilePath, string outputFilePath) @@ -572,88 +648,6 @@ private void ExecuteTranslationTask(string inputType, string outputType, string Assert.IsTrue(result, $"Failed translation: {inputType} -> {outputType}"); } - private void AssertNoDataLoss(string originalFilePath, string finalFilePath, string inputType) - { - string originalInput = virtualFileSystem.ReadAllText(originalFilePath); - string finalOutput = virtualFileSystem.ReadAllText(finalFilePath); - Assert.AreEqual(originalInput, finalOutput, $"Data mismatch after full conversion cycle for {inputType}"); - } - - private static string GetSampleDataForType(string type) - { - return type switch - { - "json" => """ -{ - "options": [ - { - "name": "Option 1", - "description": "First option", - "isTrue": true, - "number": 100, - "nested": [ - { - "name": "Nested option 1", - "description": "Nested first option", - "isTrue": true, - "number": 1001 - }, - { - "name": "Nested option 2", - "description": "Nested second option" - } - ] - } - ] -} -""", - "arm" => """ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "options": { - "type": "object", - "value": { - "name": "Option 1", - "description": "First option", - "isTrue": true, - "number": 100, - "nested": [ - { - "name": "Nested option 1", - "description": "Nested first option", - "isTrue": true, - "number": 1002 - }, - { - "name": "Nested option 2", - "description": "Nested second option" - } - ] - } - } - } -} -""", - "yml" => @"options: -- name: Option 1 - description: First option - isTrue: true - number: 100 - nested: - - name: Nested option 1 - description: Nested first option - isTrue: true - number: 1003 - - name: Nested option 2 - description: Nested second option -", - - _ => throw new InvalidOperationException("Unknown type") - }; - } - /// /// Check if an option exists with a given name and source in the top-level of the given root tree. /// diff --git a/src/UnitTests/UnitTests.csproj b/src/UnitTests/UnitTests.csproj index 68205b8..facfbee 100644 --- a/src/UnitTests/UnitTests.csproj +++ b/src/UnitTests/UnitTests.csproj @@ -7,6 +7,7 @@ disable false true + trx CS1591,CA1707,CA5394,CA1305,CA1861 diff --git a/src/UnitTests/VirtualFileSystem.cs b/src/UnitTests/VirtualFileSystem.cs index 2e387b8..06ef3de 100644 --- a/src/UnitTests/VirtualFileSystem.cs +++ b/src/UnitTests/VirtualFileSystem.cs @@ -4,6 +4,7 @@ using System.IO; using System.Text; using System.Text.RegularExpressions; +using System.Threading.Tasks; namespace AggregateConfigBuildTask.Tests.Unit { @@ -15,7 +16,6 @@ internal sealed class VirtualFileSystem(bool isWindowsMode = true) : IFileSystem private RegexOptions RegexOptions => isWindowsMode ? RegexOptions.IgnoreCase : RegexOptions.None; private StringComparison StringComparison => isWindowsMode ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - private string EnvironmentLineBreak => isWindowsMode ? "\r\n" : "\n"; /// public string[] GetFiles(string path, string searchPattern) @@ -42,29 +42,20 @@ public string[] GetFiles(string path, string searchPattern) } /// - public string[] ReadAllLines(string path) - { - path = NormalizePath(path); - - var content = ReadAllText(path); - return content.Split(EnvironmentLineBreak); - } - - /// - public string ReadAllText(string path) + public Task ReadAllTextAsync(string path) { path = NormalizePath(path); if (fileSystem.TryGetValue(path, out var content)) { - return content; + return Task.FromResult(content); } throw new FileNotFoundException($"The file '{path}' was not found in the virtual file system."); } /// - public void WriteAllText(string path, string text) + public Task WriteAllTextAsync(string path, string text) { path = NormalizePath(path); string directoryPath = Path.GetDirectoryName(path); @@ -81,6 +72,8 @@ public void WriteAllText(string path, string text) } fileSystem[path] = text; + + return Task.CompletedTask; } /// @@ -128,13 +121,13 @@ public void CreateDirectory(string directoryPath) /// public TextReader OpenText(string path) { - return new StringReader(ReadAllText(path)); + return new StringReader(ReadAllTextAsync(path).GetAwaiter().GetResult()); } /// public Stream OpenRead(string inputPath) { - var byteArray = Encoding.UTF8.GetBytes(ReadAllText(inputPath)); + var byteArray = Encoding.UTF8.GetBytes(ReadAllTextAsync(inputPath).GetAwaiter().GetResult()); return new MemoryStream(byteArray); } From 1dd81329a4bbabc0b68df2b0d99295bf9a13542e Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Sat, 21 Sep 2024 12:42:38 -0700 Subject: [PATCH 02/10] Always upload failed test results --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b13bc7..ce28326 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,8 +53,10 @@ jobs: - name: Run tests for AggregateConfigBuildTask solution run: dotnet test src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ steps.get_version.outputs.VERSION }} -p:CollectCoverage=true + id: run_tests - name: Upload TestResults artifact + if: ${{ always() && steps.run_tests.conclusion != 'skipped' }} uses: actions/upload-artifact@v4 with: name: TestResults From 5734824f74b8f6f3dd49488f035fdee35a7ac8c5 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Sat, 21 Sep 2024 12:48:17 -0700 Subject: [PATCH 03/10] Emit all mock invocation arguments --- src/Task/ObjectManager.cs | 35 +++++++++++++++++++++++++++-------- src/UnitTests/TaskTestBase.cs | 2 +- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/Task/ObjectManager.cs b/src/Task/ObjectManager.cs index 5fb11cb..f41907f 100644 --- a/src/Task/ObjectManager.cs +++ b/src/Task/ObjectManager.cs @@ -1,12 +1,12 @@ -using AggregateConfigBuildTask.FileHandlers; -using Microsoft.Build.Framework; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using AggregateConfigBuildTask.FileHandlers; +using Microsoft.Build.Framework; namespace AggregateConfigBuildTask { @@ -29,14 +29,33 @@ internal static class ObjectManager ITaskLogger log) { var finalResults = new ConcurrentBag(); + IEnumerable> fileGroups = null; JsonElement? finalResult = null; bool hasError = false; - var expectedExtensions = FileHandlerFactory.GetExpectedFileExtensions(inputType); - var fileGroups = fileSystem.GetFiles(fileObjectDirectoryPath, "*.*") - .Where(file => expectedExtensions.Contains(Path.GetExtension(file), StringComparer.OrdinalIgnoreCase)) - .ToList() - .Chunk(100); + if (fileSystem.DirectoryExists(fileObjectDirectoryPath)) + { + var expectedExtensions = FileHandlerFactory.GetExpectedFileExtensions(inputType); + fileGroups = fileSystem.GetFiles(fileObjectDirectoryPath, "*.*") + .Where(file => expectedExtensions.Contains(Path.GetExtension(file), StringComparer.OrdinalIgnoreCase)) + .ToList() + .Chunk(100); + } + else if (fileSystem.FileExists(fileObjectDirectoryPath)) + { + fileGroups = new List> + { + new List + { + fileObjectDirectoryPath + } + }; + } + else + { + log.LogError("The provided path was not found as a directory or file: {0}", fileObjectDirectoryPath); + return null; + } await fileGroups.ForEachAsync(Environment.ProcessorCount, async (files) => { diff --git a/src/UnitTests/TaskTestBase.cs b/src/UnitTests/TaskTestBase.cs index 92ba1d1..b4413c8 100644 --- a/src/UnitTests/TaskTestBase.cs +++ b/src/UnitTests/TaskTestBase.cs @@ -35,7 +35,7 @@ public void Cleanup() foreach (var invocation in mockLogger.Invocations) { var methodName = invocation.Method.Name; - var arguments = string.Join(", ", invocation.Arguments); + var arguments = string.Join(", ", JsonConvert.SerializeObject(invocation.Arguments)); TestContext.WriteLine($"Logger call: {methodName}({arguments})"); } } From 0cc640587f560361a00109ab520ebdcb3f8aeab1 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Sat, 21 Sep 2024 12:55:32 -0700 Subject: [PATCH 04/10] Update main async --- src/Task/AggregateConfig.cs | 54 ++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/Task/AggregateConfig.cs b/src/Task/AggregateConfig.cs index 0e9f9e1..5912672 100644 --- a/src/Task/AggregateConfig.cs +++ b/src/Task/AggregateConfig.cs @@ -2,6 +2,7 @@ using System.IO; using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using AggregateConfigBuildTask.FileHandlers; using Microsoft.Build.Framework; using Task = Microsoft.Build.Utilities.Task; @@ -109,30 +110,7 @@ public override bool Execute() return false; } - logger.LogMessage(MessageImportance.High, "Aggregating {0} to {1} in folder {2}", inputType, outputType, InputDirectory); - - string directoryPath = Path.GetDirectoryName(OutputFile); - if (!fileSystem.DirectoryExists(directoryPath)) - { - fileSystem.CreateDirectory(directoryPath); - } - - var finalResult = ObjectManager.MergeFileObjects(InputDirectory, inputType, AddSourceProperty, fileSystem, logger).GetAwaiter().GetResult(); - - if (finalResult == null) - { - logger.LogError("No input was found! Check the input directory."); - return false; - } - - var additionalPropertiesDictionary = JsonHelper.ParseAdditionalProperties(AdditionalProperties); - finalResult = ObjectManager.InjectAdditionalProperties(finalResult, additionalPropertiesDictionary, logger).GetAwaiter().GetResult(); - - var writer = FileHandlerFactory.GetFileHandlerForType(fileSystem, outputType); - writer.WriteOutput(finalResult, OutputFile); - logger.LogMessage(MessageImportance.High, "Wrote aggregated configuration file to {0}", OutputFile); - - return true; + return Process(inputType, outputType).GetAwaiter().GetResult(); } catch (Exception ex) { @@ -142,6 +120,34 @@ public override bool Execute() } } + private async Task Process(FileType inputType, FileType outputType) + { + logger.LogMessage(MessageImportance.High, "Aggregating {0} to {1} in folder {2}", inputType, outputType, InputDirectory); + + string directoryPath = Path.GetDirectoryName(OutputFile); + if (!fileSystem.DirectoryExists(directoryPath)) + { + fileSystem.CreateDirectory(directoryPath); + } + + var finalResult = await ObjectManager.MergeFileObjects(InputDirectory, inputType, AddSourceProperty, fileSystem, logger).ConfigureAwait(false); + + if (finalResult == null) + { + logger.LogError("No input was found! Check the input directory."); + return false; + } + + var additionalPropertiesDictionary = JsonHelper.ParseAdditionalProperties(AdditionalProperties); + finalResult = await ObjectManager.InjectAdditionalProperties(finalResult, additionalPropertiesDictionary, logger).ConfigureAwait(false); + + var writer = FileHandlerFactory.GetFileHandlerForType(fileSystem, outputType); + await writer.WriteOutput(finalResult, OutputFile).ConfigureAwait(false); + logger.LogMessage(MessageImportance.High, "Wrote aggregated configuration file to {0}", OutputFile); + + return true; + } + private void EmitHeader() { var assembly = Assembly.GetExecutingAssembly(); From 1749ebc91e98592e62dcc2544c990c978a46c0de Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Sat, 21 Sep 2024 13:04:02 -0700 Subject: [PATCH 05/10] Replace set-output usage --- .github/workflows/build.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce28326..00f7721 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,13 +46,14 @@ jobs: $VERSION = "0.0.1-$COMMIT_HASH" } - Write-Output "::set-output name=VERSION::$VERSION" + echo "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - name: Build AggregateConfigBuildTask solution in Release mode - run: dotnet build src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ steps.get_version.outputs.VERSION }} + run: dotnet build src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ env.VERSION }} - name: Run tests for AggregateConfigBuildTask solution - run: dotnet test src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ steps.get_version.outputs.VERSION }} -p:CollectCoverage=true + run: dotnet test src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ env.VERSION }} -p:CollectCoverage=true id: run_tests - name: Upload TestResults artifact @@ -67,8 +68,8 @@ jobs: with: name: NuGetPackage path: | - src/Task/bin/Release/AggregateConfigBuildTask.${{ steps.get_version.outputs.VERSION }}.nupkg - src/Task/bin/Release/AggregateConfigBuildTask.${{ steps.get_version.outputs.VERSION }}.snupkg + src/Task/bin/Release/AggregateConfigBuildTask.${{ env.VERSION }}.nupkg + src/Task/bin/Release/AggregateConfigBuildTask.${{ env.VERSION }}.snupkg integration_tests: needs: build From 6cd059278b8bc7f63baea2d359a8771c3cb45634 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Sat, 21 Sep 2024 18:08:49 -0700 Subject: [PATCH 06/10] Update toml table array type --- .github/workflows/build.yml | 2 ++ src/Task/FileHandlers/TomlFileHandler.cs | 11 +++++-- src/UnitTests/Data/DemoData.cs | 41 +++++++++++++----------- src/UnitTests/TaskTestBase.cs | 11 +++++-- 4 files changed, 40 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 00f7721..1625df0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,6 +46,8 @@ jobs: $VERSION = "0.0.1-$COMMIT_HASH" } + Write-Output "Version is $VERSION" + echo "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 echo "VERSION=$VERSION" >> $GITHUB_OUTPUT diff --git a/src/Task/FileHandlers/TomlFileHandler.cs b/src/Task/FileHandlers/TomlFileHandler.cs index 0e9d33c..fc5cdf4 100644 --- a/src/Task/FileHandlers/TomlFileHandler.cs +++ b/src/Task/FileHandlers/TomlFileHandler.cs @@ -47,7 +47,7 @@ public async ValueTask ReadInput(string inputPath) /// The JSON data to be written as TOML. /// The path where the TOML file should be written. /// Thrown when the provided JSON data is null. - public Task WriteOutput(JsonElement? mergedData, string outputPath) + public async Task WriteOutput(JsonElement? mergedData, string outputPath) { if (mergedData == null) { @@ -61,10 +61,11 @@ public Task WriteOutput(JsonElement? mergedData, string outputPath) using (var writer = new StringWriter()) { tomlTable.WriteTo(writer); + await writer.FlushAsync().ConfigureAwait(false); tomlString = writer.ToString(); } - return fileSystem.WriteAllTextAsync(outputPath, tomlString); + await fileSystem.WriteAllTextAsync(outputPath, tomlString).ConfigureAwait(false); } /// @@ -219,7 +220,11 @@ private void AddJsonPropertyToTomlTable(TomlTable table, JsonProperty property) table[property.Name] = subTable; break; case JsonValueKind.Array: - var tomlArray = new TomlArray(); + var tomlArray = new TomlArray + { + IsTableArray = true + }; + foreach (JsonElement item in property.Value.EnumerateArray()) { tomlArray.Add(ConvertJsonElementToToml(item)); diff --git a/src/UnitTests/Data/DemoData.cs b/src/UnitTests/Data/DemoData.cs index 1634239..5904cb6 100644 --- a/src/UnitTests/Data/DemoData.cs +++ b/src/UnitTests/Data/DemoData.cs @@ -38,25 +38,27 @@ public static string GetSampleDataForType(string type) "contentVersion": "1.0.0.0", "parameters": { "options": { - "type": "object", - "value": { - "name": "Option 1", - "description": "First option", - "isTrue": true, - "number": 100, - "nested": [ - { - "name": "Nested option 1", - "description": "Nested first option", - "isTrue": true, - "number": 1002 - }, - { - "name": "Nested option 2", - "description": "Nested second option" - } - ] - } + "type": "array", + "value": [ + { + "name": "Option 1", + "description": "First option", + "isTrue": true, + "number": 100, + "nested": [ + { + "name": "Nested option 1", + "description": "Nested first option", + "isTrue": true, + "number": 1002 + }, + { + "name": "Nested option 2", + "description": "Nested second option" + } + ] + } + ] } } } @@ -90,6 +92,7 @@ public static string GetSampleDataForType(string type) [[options.nested]] name = "Nested option 2" description = "Nested second option" + """, _ => throw new InvalidOperationException($"Unknown type: {type}") }; diff --git a/src/UnitTests/TaskTestBase.cs b/src/UnitTests/TaskTestBase.cs index b4413c8..c71d65f 100644 --- a/src/UnitTests/TaskTestBase.cs +++ b/src/UnitTests/TaskTestBase.cs @@ -557,7 +557,7 @@ public async Task StressTest_ShouldAddSourcePropertyManyFiles() [TestMethod] [DynamicData(nameof(GetFileTypeConversions), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetTestDisplayName))] - [Description("Test that files are correctly translated between ARM, JSON, YAML, and TOML.")] + [Description("Test that files are correctly translated between all supported FileTypes.")] public async Task ShouldTranslateBetweenFormatsAndValidateNoDataLoss(string inputType, string[] steps, string _) { Assert.IsTrue(steps?.Length > 0); @@ -606,10 +606,11 @@ public async Task ShouldTranslateBetweenFormatsAndValidateNoDataLoss(string inpu public static IEnumerable GetFileTypeConversions() { var fileTypes = Enum.GetValues(typeof(FileType)).Cast().Select(ft => ft.ToString().ToUpperInvariant()).ToArray(); + var permutationCount = Enum.GetNames(typeof(FileType)).DistinctBy(Enum.Parse).Count() - 1; foreach (var initialFormat in fileTypes) { - foreach (var permutation in GetPermutations(fileTypes.Where(ft => ft != initialFormat), 3)) + foreach (var permutation in GetPermutations(fileTypes.Where(ft => ft != initialFormat), permutationCount)) { var displayName = $"{initialFormat.ToUpperInvariant()} -> {string.Join(" -> ", permutation.Select(p => p.ToUpperInvariant()))} -> {initialFormat.ToUpperInvariant()}"; yield return new object[] { initialFormat, permutation.ToArray(), displayName }; @@ -628,7 +629,11 @@ public static string GetTestDisplayName(MethodInfo methodInfo, object[] data) private static IEnumerable> GetPermutations(IEnumerable list, int length) { - if (length == 1) return list.Select(t => new[] { t }); + if (length == 1) + { + return list.Select(t => new[] { t }); + } + return GetPermutations(list, length - 1) .SelectMany(t => list.Where(e => !t.Contains(e)), (t1, t2) => t1.Concat([t2])); From dc16bd60359deb8f8dc6dc486a1591e411b993d5 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Sat, 21 Sep 2024 18:15:56 -0700 Subject: [PATCH 07/10] Version output fix --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1625df0..71edae3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,14 +49,14 @@ jobs: Write-Output "Version is $VERSION" echo "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "VERSION=$VERSION" >> $env:GITHUB_OUTPUT - name: Build AggregateConfigBuildTask solution in Release mode run: dotnet build src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ env.VERSION }} - name: Run tests for AggregateConfigBuildTask solution - run: dotnet test src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ env.VERSION }} -p:CollectCoverage=true id: run_tests + run: dotnet test src/AggregateConfigBuildTask.sln --configuration Release -warnaserror -p:Version=${{ env.VERSION }} -p:CollectCoverage=true - name: Upload TestResults artifact if: ${{ always() && steps.run_tests.conclusion != 'skipped' }} From 6f5472d7c5c3122c6efc80b9dde352e91470c270 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Sat, 21 Sep 2024 18:22:04 -0700 Subject: [PATCH 08/10] Add toml integration test --- test/IntegrationTests/IntegrationTests.csproj | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/IntegrationTests/IntegrationTests.csproj b/test/IntegrationTests/IntegrationTests.csproj index c0387b9..8819221 100644 --- a/test/IntegrationTests/IntegrationTests.csproj +++ b/test/IntegrationTests/IntegrationTests.csproj @@ -43,6 +43,13 @@ OutputType="Yaml" AddSourceProperty="true" /> + + + From 5d1b2644cfa9a25f230a63c3fa07e7397d3f4103 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Sat, 21 Sep 2024 18:41:24 -0700 Subject: [PATCH 09/10] Remove unused loggers and update source test --- src/Task/Logger/ITaskLogger.cs | 50 ---------------------------- src/Task/Logger/QuietTaskLogger.cs | 52 ------------------------------ src/Task/Logger/TaskLogger.cs | 52 ------------------------------ src/UnitTests/TaskTestBase.cs | 26 +++++++++------ 4 files changed, 17 insertions(+), 163 deletions(-) diff --git a/src/Task/Logger/ITaskLogger.cs b/src/Task/Logger/ITaskLogger.cs index 733c19a..cafd9a1 100644 --- a/src/Task/Logger/ITaskLogger.cs +++ b/src/Task/Logger/ITaskLogger.cs @@ -16,31 +16,6 @@ public interface ITaskLogger /// Optional arguments for formatting the message. void LogError(string message = null, params object[] messageArgs); - /// - /// Logs an error message with additional parameters. - /// - /// The subcategory of the error. - /// The error code. - /// The help keyword associated with the error. - /// The file in which the error occurred. - /// The line number where the error occurred. - /// The column number where the error occurred. - /// The end line number of the error. - /// The end column number of the error. - /// The error message to log. - /// Optional arguments for formatting the message. - void LogError( - string subcategory = null, - string errorCode = null, - string helpKeyword = null, - string file = null, - int lineNumber = 0, - int columnNumber = 0, - int endLineNumber = 0, - int endColumnNumber = 0, - string message = null, - params object[] messageArgs); - /// /// Logs an error message from an exception. /// @@ -60,31 +35,6 @@ void LogErrorFromException(Exception exception, /// Optional arguments for formatting the message. void LogWarning(string message = null, params object[] messageArgs); - /// - /// Logs a warning message with additional parameters. - /// - /// The subcategory of the warning. - /// The warning code. - /// The help keyword associated with the warning. - /// The file in which the warning occurred. - /// The line number where the warning occurred. - /// The column number where the warning occurred. - /// The end line number of the warning. - /// The end column number of the warning. - /// The warning message to log. - /// Optional arguments for formatting the message. - void LogWarning( - string subcategory = null, - string warningCode = null, - string helpKeyword = null, - string file = null, - int lineNumber = 0, - int columnNumber = 0, - int endLineNumber = 0, - int endColumnNumber = 0, - string message = null, - params object[] messageArgs); - /// /// Logs a message with specified importance. /// diff --git a/src/Task/Logger/QuietTaskLogger.cs b/src/Task/Logger/QuietTaskLogger.cs index b0b376f..1c531cd 100644 --- a/src/Task/Logger/QuietTaskLogger.cs +++ b/src/Task/Logger/QuietTaskLogger.cs @@ -29,32 +29,6 @@ public void LogError(string message = null, params object[] messageArgs) Log.LogError(message, messageArgs); } - /// - public void LogError( - string subcategory = null, - string errorCode = null, - string helpKeyword = null, - string file = null, - int lineNumber = 0, - int columnNumber = 0, - int endLineNumber = 0, - int endColumnNumber = 0, - string message = null, - params object[] messageArgs) - { - Log.LogError( - subcategory, - errorCode, - helpKeyword, - file, - lineNumber, - columnNumber, - endLineNumber, - endColumnNumber, - message, - messageArgs); - } - /// public void LogErrorFromException(Exception exception, bool showStackTrace = false, @@ -70,32 +44,6 @@ public void LogWarning(string message = null, params object[] messageArgs) Log.LogWarning(message, messageArgs); } - /// - public void LogWarning( - string subcategory = null, - string warningCode = null, - string helpKeyword = null, - string file = null, - int lineNumber = 0, - int columnNumber = 0, - int endLineNumber = 0, - int endColumnNumber = 0, - string message = null, - params object[] messageArgs) - { - Log.LogWarning( - subcategory, - warningCode, - helpKeyword, - file, - lineNumber, - columnNumber, - endLineNumber, - endColumnNumber, - message, - messageArgs); - } - /// public void LogMessage(MessageImportance importance = MessageImportance.Normal, string message = null, params object[] messageArgs) { diff --git a/src/Task/Logger/TaskLogger.cs b/src/Task/Logger/TaskLogger.cs index b3e4c6d..29db22a 100644 --- a/src/Task/Logger/TaskLogger.cs +++ b/src/Task/Logger/TaskLogger.cs @@ -27,32 +27,6 @@ public void LogError(string message = null, params object[] messageArgs) Log.LogError(message, messageArgs); } - /// - public void LogError( - string subcategory = null, - string errorCode = null, - string helpKeyword = null, - string file = null, - int lineNumber = 0, - int columnNumber = 0, - int endLineNumber = 0, - int endColumnNumber = 0, - string message = null, - params object[] messageArgs) - { - Log.LogError( - subcategory, - errorCode, - helpKeyword, - file, - lineNumber, - columnNumber, - endLineNumber, - endColumnNumber, - message, - messageArgs); - } - /// public void LogErrorFromException(Exception exception, bool showStackTrace = false, @@ -68,32 +42,6 @@ public void LogWarning(string message = null, params object[] messageArgs) Log.LogWarning(message, messageArgs); } - /// - public void LogWarning( - string subcategory = null, - string warningCode = null, - string helpKeyword = null, - string file = null, - int lineNumber = 0, - int columnNumber = 0, - int endLineNumber = 0, - int endColumnNumber = 0, - string message = null, - params object[] messageArgs) - { - Log.LogWarning( - subcategory, - warningCode, - helpKeyword, - file, - lineNumber, - columnNumber, - endLineNumber, - endColumnNumber, - message, - messageArgs); - } - /// public void LogMessage(MessageImportance importance = MessageImportance.Normal, string message = null, params object[] messageArgs) { diff --git a/src/UnitTests/TaskTestBase.cs b/src/UnitTests/TaskTestBase.cs index c71d65f..70cbf8c 100644 --- a/src/UnitTests/TaskTestBase.cs +++ b/src/UnitTests/TaskTestBase.cs @@ -114,10 +114,12 @@ await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file2.yml", @" [TestMethod] [Description("Test that the source property is correctly added when AddSourceProperty is true.")] - public async Task ShouldAddSourceProperty() + [DynamicData(nameof(GetFileTypes), DynamicDataSourceType.Method)] + public async Task ShouldAddSourceProperty(FileType outputType) { // Arrange: Prepare sample YAML data with source property enabled. - await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.yml", @" + Guid outputFileName = Guid.NewGuid(); + await virtualFileSystem.WriteAllTextAsync($"{testPath}\\{outputFileName}.yml", @" options: - name: 'Option 1' description: 'First option'").ConfigureAwait(false); @@ -126,8 +128,8 @@ await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.yml", @" var task = new AggregateConfig(virtualFileSystem, mockLogger.Object) { InputDirectory = testPath, - OutputFile = testPath + @"\output.json", - OutputType = nameof(FileType.Json), + OutputFile = testPath + $@"\output.{(outputType == FileType.Arm ? "json" : outputType)}", + OutputType = outputType.ToString().ToUpperInvariant(), AddSourceProperty = true, BuildEngine = Mock.Of() }; @@ -137,10 +139,8 @@ await virtualFileSystem.WriteAllTextAsync($"{testPath}\\file1.yml", @" // Assert: Verify that the source property was added Assert.IsTrue(result); - string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.json").ConfigureAwait(false); - var json = JsonConvert.DeserializeObject>>>(output); - Assert.IsTrue(json["options"][0].ContainsKey("source")); - Assert.AreEqual("file1", json["options"][0]["source"]); + string output = await virtualFileSystem.ReadAllTextAsync($"{testPath}\\output.{(outputType == FileType.Arm ? "json" : outputType)}").ConfigureAwait(false); + StringAssert.Contains(output, outputFileName.ToString(), StringComparison.InvariantCulture); } [TestMethod] @@ -556,8 +556,8 @@ public async Task StressTest_ShouldAddSourcePropertyManyFiles() } [TestMethod] - [DynamicData(nameof(GetFileTypeConversions), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetTestDisplayName))] [Description("Test that files are correctly translated between all supported FileTypes.")] + [DynamicData(nameof(GetFileTypeConversions), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetTestDisplayName))] public async Task ShouldTranslateBetweenFormatsAndValidateNoDataLoss(string inputType, string[] steps, string _) { Assert.IsTrue(steps?.Length > 0); @@ -603,6 +603,14 @@ public async Task ShouldTranslateBetweenFormatsAndValidateNoDataLoss(string inpu Assert.AreEqual(originalInput, finalOutput, $"Data mismatch after full conversion cycle for {inputType}"); } + public static IEnumerable GetFileTypes() + { + foreach (var type in Enum.GetValues(typeof(FileType)).Cast()) + { + yield return new object[] { type }; + } + } + public static IEnumerable GetFileTypeConversions() { var fileTypes = Enum.GetValues(typeof(FileType)).Cast().Select(ft => ft.ToString().ToUpperInvariant()).ToArray(); From cefab6a4ab072903fe0e20be9a0bb12f7e944004 Mon Sep 17 00:00:00 2001 From: Billy Richardson Date: Sat, 21 Sep 2024 19:18:24 -0700 Subject: [PATCH 10/10] Update handling of simple types --- src/Task/FileHandlers/TomlFileHandler.cs | 63 +++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/Task/FileHandlers/TomlFileHandler.cs b/src/Task/FileHandlers/TomlFileHandler.cs index fc5cdf4..d0c5bf4 100644 --- a/src/Task/FileHandlers/TomlFileHandler.cs +++ b/src/Task/FileHandlers/TomlFileHandler.cs @@ -106,9 +106,28 @@ private TomlTable ConvertJsonElementToToml(JsonElement jsonElement) { TomlTable tomlTable = new TomlTable(); - foreach (JsonProperty property in jsonElement.EnumerateObject()) + // Check if the JsonElement is an object + if (jsonElement.ValueKind == JsonValueKind.Object) { - AddJsonPropertyToTomlTable(tomlTable, property); + foreach (JsonProperty property in jsonElement.EnumerateObject()) + { + AddJsonPropertyToTomlTable(tomlTable, property); + } + } + // Handle array case + else if (jsonElement.ValueKind == JsonValueKind.Array) + { + var tomlArray = new TomlArray(); + foreach (JsonElement element in jsonElement.EnumerateArray()) + { + tomlArray.Add(ConvertJsonElementToTomlValue(element)); + } + return new TomlTable { ["array"] = tomlArray }; + } + else + { + // For simple types, add them as a direct value + tomlTable["value"] = ConvertJsonElementToTomlValue(jsonElement); } return tomlTable; @@ -252,5 +271,45 @@ private void AddJsonPropertyToTomlTable(TomlTable table, JsonProperty property) throw new InvalidOperationException($"Unsupported JSON value type: {property.Value.ValueKind}"); } } + + /// + /// Converts a value to the appropriate TOML node. + /// + /// The JSON element to be converted. + /// A TOML node. + /// Thrown when the JSON element contains unsupported number format or type. + private TomlNode ConvertJsonElementToTomlValue(JsonElement jsonElement) + { + switch (jsonElement.ValueKind) + { + case JsonValueKind.String: + return new TomlString { Value = jsonElement.GetString() }; + case JsonValueKind.Number: + if (jsonElement.TryGetInt32(out int intValue)) + { + return new TomlInteger { Value = intValue }; + } + if (jsonElement.TryGetDouble(out double doubleValue)) + { + return new TomlFloat { Value = doubleValue }; + } + throw new InvalidOperationException("Unsupported number format."); + case JsonValueKind.True: + return new TomlBoolean { Value = true }; + case JsonValueKind.False: + return new TomlBoolean { Value = false }; + case JsonValueKind.Object: + return ConvertJsonElementToToml(jsonElement); + case JsonValueKind.Array: + var tomlArray = new TomlArray(); + foreach (JsonElement element in jsonElement.EnumerateArray()) + { + tomlArray.Add(ConvertJsonElementToTomlValue(element)); + } + return tomlArray; + default: + throw new InvalidOperationException($"Unsupported JsonElement type: {jsonElement.ValueKind}"); + } + } } }