winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 6f0bf7b624769768093504b00591e7e2d6b378a3
parent 3dac77fb9ad2675f6c7bdf4e8f8fcae2bc8f1c92
Author: Ruben Guerrero <rubengu@microsoft.com>
Date:   Mon, 10 Apr 2023 16:27:37 -0700

Add support for bool, strings and arrays in Configuration settings (#3135)


Diffstat:
Msrc/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp | 107++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 3+++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 3+++
Asrc/AppInstallerCLITests/TestData/Node-Types.yaml | 12++++++++++++
Msrc/AppInstallerCLITests/YamlManifest.cpp | 29+++++++++++++++++++++++++++++
Msrc/AppInstallerSharedLib/AppInstallerStrings.cpp | 26++++++++++++++++++++++++++
Msrc/AppInstallerSharedLib/Public/AppInstallerStrings.h | 3+++
Msrc/AppInstallerSharedLib/Public/winget/Yaml.h | 42++++++++++++++++++++++++++++++++++++++++--
Msrc/AppInstallerSharedLib/Yaml.cpp | 133++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/AppInstallerSharedLib/YamlWrapper.cpp | 5++++-
Msrc/Microsoft.Management.Configuration.Processor/Extensions/PowerShellExtensions.cs | 3---
Msrc/Microsoft.Management.Configuration.Processor/Extensions/ValueSetExtensions.cs | 58+++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj | 10++++------
Msrc/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs | 47+++++++++++++++++++++++++++++++++++++++++++++--
Asrc/Microsoft.Management.Configuration.UnitTests/Tests/ValueSetExtensionsTests.cs | 344+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp | 21+++++++++++++++++++--
16 files changed, 811 insertions(+), 35 deletions(-)

diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -96,6 +96,93 @@ namespace AppInstaller::CLI::Workflow return {}; } + void OutputPropertyValue(OutputStream& out, const IPropertyValue property) + { + switch (property.Type()) + { + case PropertyType::String: + out << ' ' << Utility::ConvertToUTF8(property.GetString()) << '\n'; + break; + case PropertyType::Boolean: + out << ' ' << (property.GetBoolean() ? Utility::LocIndView("true") : Utility::LocIndView("false")) << '\n'; + break; + case PropertyType::Int64: + out << ' ' << property.GetInt64() << '\n'; + break; + default: + out << " [Debug:PropertyType="_liv << property.Type() << "]\n"_liv; + break; + } + } + + void OutputValueSet(OutputStream& out, const ValueSet& valueSet, size_t indent); + + void OutputValueSetAsArray(OutputStream& out, const ValueSet& valueSetArray, size_t indent) + { + Utility::LocIndString indentString{ std::string(indent, ' ') }; + + std::vector<std::pair<int, winrt::Windows::Foundation::IInspectable>> arrayValues; + for (const auto& arrayValue : valueSetArray) + { + if (arrayValue.Key() != L"treatAsArray") + { + arrayValues.emplace_back(std::make_pair(std::stoi(arrayValue.Key().c_str()), arrayValue.Value())); + } + } + + std::sort( + arrayValues.begin(), + arrayValues.end(), + [](const std::pair<int, winrt::Windows::Foundation::IInspectable>& a, const std::pair<int, winrt::Windows::Foundation::IInspectable>& b) + { + return a.first < b.first; + }); + + for (const auto& arrayValue : arrayValues) + { + auto arrayObject = arrayValue.second; + IPropertyValue arrayProperty = arrayObject.try_as<IPropertyValue>(); + + out << indentString << "-"; + if (arrayProperty) + { + OutputPropertyValue(out, arrayProperty); + } + else + { + ValueSet arraySubset = arrayObject.as<ValueSet>(); + auto size = arraySubset.Size(); + if (size > 0) + { + // First one is special. + auto first = arraySubset.First().Current(); + out << ' ' << Utility::ConvertToUTF8(first.Key()) << ':'; + + auto object = first.Value(); + IPropertyValue property = object.try_as<IPropertyValue>(); + if (property) + { + OutputPropertyValue(out, property); + } + else + { + // If not an IPropertyValue, it must be a ValueSet + ValueSet subset = object.as<ValueSet>(); + out << '\n'; + OutputValueSet(out, subset, indent + 4); + } + + if (size > 1) + { + arraySubset.Remove(first.Key()); + OutputValueSet(out, arraySubset, indent + 2); + arraySubset.Insert(first.Key(), first.Value()); + } + } + } + } + } + void OutputValueSet(OutputStream& out, const ValueSet& valueSet, size_t indent) { Utility::LocIndString indentString{ std::string(indent, ' ') }; @@ -109,23 +196,21 @@ namespace AppInstaller::CLI::Workflow IPropertyValue property = object.try_as<IPropertyValue>(); if (property) { - switch (property.Type()) - { - case PropertyType::String: - out << ' ' << Utility::ConvertToUTF8(property.GetString()) << '\n'; - break; - default: - // TODO: Sort out how we actually want to handle this given that we don't expect anything but strings - out << " [Debug:PropertyType="_liv << property.Type() << "]\n"_liv; - break; - } + OutputPropertyValue(out, property); } else { // If not an IPropertyValue, it must be a ValueSet ValueSet subset = object.as<ValueSet>(); out << '\n'; - OutputValueSet(out, subset, indent + 2); + if (subset.HasKey(L"treatAsArray")) + { + OutputValueSetAsArray(out, subset, indent + 2); + } + else + { + OutputValueSet(out, subset, indent + 2); + } } } } diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -810,6 +810,9 @@ <CopyFileToFolders Include="TestData\Manifest-Bad-MsixInstaller-PackageVersion.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Node-Types.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> </ItemGroup> <ItemGroup> <ProjectReference Include="..\AppInstallerCLICore\AppInstallerCLICore.vcxproj"> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -834,5 +834,8 @@ <CopyFileToFolders Include="TestData\Manifest-Bad-InconsistentSignedMsixBundleInstallerFields.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\Node-Types.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestData/Node-Types.yaml b/src/AppInstallerCLITests/TestData/Node-Types.yaml @@ -0,0 +1,11 @@ +IntegerUnquoted: 12345 +IntegerSingleQuoted: '12345' +IntegerDoubleQuoted: "12345" + +BooleanTrue: true +StringTrue: 'true' + +BooleanFalse: false +StringFalse: 'false' + +LocalTag: !myTag value+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/YamlManifest.cpp b/src/AppInstallerCLITests/YamlManifest.cpp @@ -1162,3 +1162,32 @@ TEST_CASE("ManifestArpVersionRange", "[ManifestValidation]") REQUIRE(arpRangeMultiArp.GetMinVersion().ToString() == "12.0"); REQUIRE(arpRangeMultiArp.GetMaxVersion().ToString() == "13.0"); } + +TEST_CASE("YamlParserTypes", "[YAML]") +{ + auto document = AppInstaller::YAML::Load(TestDataFile("Node-Types.yaml")); + + auto intUnquoted = document["IntegerUnquoted"]; + CHECK(intUnquoted.GetTagType() == Node::TagType::Int); + + auto intSingleQuoted = document["IntegerSingleQuoted"]; + CHECK(intSingleQuoted.GetTagType() == Node::TagType::Str); + + auto intDoubleQuoted = document["IntegerDoubleQuoted"]; + CHECK(intDoubleQuoted.GetTagType() == Node::TagType::Str); + + auto boolTrue = document["BooleanTrue"]; + CHECK(boolTrue.GetTagType() == Node::TagType::Bool); + + auto strTrue = document["StringTrue"]; + CHECK(strTrue.GetTagType() == Node::TagType::Str); + + auto boolFalse = document["BooleanFalse"]; + CHECK(boolFalse.GetTagType() == Node::TagType::Bool); + + auto strFalse = document["StringFalse"]; + CHECK(strFalse.GetTagType() == Node::TagType::Str); + + auto localTag = document["LocalTag"]; + CHECK(localTag.GetTagType() == Node::TagType::Unknown); +} diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -175,6 +175,32 @@ namespace AppInstaller::Utility return result; } + std::optional<std::wstring> TryConvertToUTF16(std::string_view input, UINT codePage) + { + if (input.empty()) + { + return std::wstring{}; + } + + int utf16CharCount = MultiByteToWideChar(codePage, 0, input.data(), wil::safe_cast<int>(input.length()), nullptr, 0); + if (utf16CharCount == 0) + { + return {}; + } + + // Since the string view should not contain the null char, the result won't either. + // This allows us to use the resulting size value directly in the string constructor. + std::wstring result(wil::safe_cast<size_t>(utf16CharCount), L'\0'); + + int utf16CharsWritten = MultiByteToWideChar(codePage, 0, input.data(), wil::safe_cast<int>(input.length()), &result[0], wil::safe_cast<int>(result.size())); + if (utf16CharCount != utf16CharsWritten) + { + return {}; + } + + return std::optional{ result }; + } + std::u32string ConvertToUTF32(std::string_view input) { if (input.empty()) diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -16,6 +16,9 @@ namespace AppInstaller::Utility // Converts the given UTF8 string to UTF16 std::wstring ConvertToUTF16(std::string_view input, UINT codePage = CP_UTF8); + // Tries to convert the given UTF8 string to UTF16 + std::optional<std::wstring> TryConvertToUTF16(std::string_view input, UINT codePage = CP_UTF8); + // Converts the given UTF8 string to UTF32 std::u32string ConvertToUTF32(std::string_view input); diff --git a/src/AppInstallerSharedLib/Public/winget/Yaml.h b/src/AppInstallerSharedLib/Public/winget/Yaml.h @@ -72,11 +72,26 @@ namespace AppInstaller::YAML Mapping }; - Node() : m_type(Type::Invalid) {} + // The node's tag + enum class TagType + { + Unknown, + Null, + Bool, + Str, + Int, + Float, + Timestamp, + Seq, + Map, + }; + + Node() : m_type(Type::Invalid), m_tagType(TagType::Unknown) {} Node(Type type, std::string tag, const Mark& mark); // Sets the scalar value of the node. void SetScalar(std::string value); + void SetScalar(std::string value, bool isQuoted); // Adds a child node to the sequence. template <typename... Args> @@ -100,6 +115,7 @@ namespace AppInstaller::YAML bool IsSequence() const { return m_type == Type::Sequence; } bool IsMap() const { return m_type == Type::Mapping; } Type GetType() const { return m_type; } + TagType GetTagType() const { return m_tagType; } explicit operator bool() const { return IsDefined(); } @@ -112,6 +128,18 @@ namespace AppInstaller::YAML return as_dispatch(t); } + template <typename T> + std::optional<T> try_as() const + { + if (m_type != Type::Scalar) + { + return {}; + } + + T* t = nullptr; + return try_as_dispatch(t); + } + bool operator<(const Node& other) const; // Gets a child node from the mapping by its name. @@ -135,20 +163,30 @@ namespace AppInstaller::YAML const std::multimap<Node, Node>& Mapping() const; private: - Node(std::string_view key) : m_type(Type::Scalar), m_scalar(key) {} + Node(std::string_view key) : m_type(Type::Scalar), m_scalar(key), m_tagType(TagType::Str) {} // Require certain node types to; throwing if the requirement is not met. void Require(Type type) const; // The workers for the as function. std::string as_dispatch(std::string*) const; + std::optional<std::string> try_as_dispatch(std::string*) const; + std::wstring as_dispatch(std::wstring*) const; + std::optional<std::wstring> try_as_dispatch(std::wstring*) const; + int64_t as_dispatch(int64_t*) const; + std::optional<int64_t> try_as_dispatch(int64_t*) const; + int as_dispatch(int*) const; + std::optional<int> try_as_dispatch(int*) const; + bool as_dispatch(bool*) const; + std::optional<bool> try_as_dispatch(bool*) const; Type m_type; std::string m_tag; + TagType m_tagType; YAML::Mark m_mark; std::string m_scalar; std::optional<std::vector<Node>> m_sequence; diff --git a/src/AppInstallerSharedLib/Yaml.cpp b/src/AppInstallerSharedLib/Yaml.cpp @@ -16,6 +16,15 @@ namespace AppInstaller::YAML { Node s_globalInvalidNode; + static constexpr std::string_view s_nullTag = "tag:yaml.org,2002:null"sv; + static constexpr std::string_view s_boolTag = "tag:yaml.org,2002:bool"sv; + static constexpr std::string_view s_strTag = "tag:yaml.org,2002:str"sv; + static constexpr std::string_view s_intTag = "tag:yaml.org,2002:int"sv; + static constexpr std::string_view s_floatTag = "tag:yaml.org,2002:float"sv; + static constexpr std::string_view s_timestampTag = "tag:yaml.org,2002:timestamp"sv; + static constexpr std::string_view s_seqTag = "tag:yaml.org,2002:seq"sv; + static constexpr std::string_view s_mapTag = "tag:yaml.org,2002:map"sv; + std::string_view GetExceptionTypeStringView(Exception::Type type) { switch (type) @@ -50,6 +59,44 @@ namespace AppInstaller::YAML { out << "[line " << mark.line << "; col " << mark.column << ']'; } + + Node::TagType ConvertToTagType(const std::string& tag) + { + if (tag == s_strTag) + { + return Node::TagType::Str; + } + else if (tag == s_seqTag) + { + return Node::TagType::Seq; + } + else if (tag == s_mapTag) + { + return Node::TagType::Map; + } + else if (tag == s_boolTag) + { + return Node::TagType::Bool; + } + else if (tag == s_intTag) + { + return Node::TagType::Int; + } + else if (tag == s_floatTag) + { + return Node::TagType::Float; + } + else if (tag == s_timestampTag) + { + return Node::TagType::Timestamp; + } + else if (tag == s_nullTag) + { + return Node::TagType::Null; + } + + return Node::TagType::Unknown; + } } Exception::Exception(Type type) : @@ -138,6 +185,8 @@ namespace AppInstaller::YAML { m_mapping = decltype(m_mapping)::value_type{}; } + + m_tagType = ConvertToTagType(m_tag); } void Node::SetScalar(std::string value) @@ -146,6 +195,34 @@ namespace AppInstaller::YAML m_scalar = std::move(value); } + void Node::SetScalar(std::string value, bool isQuoted) + { + this->SetScalar(value); + + // For untagged scalar nodes, libyaml always assigns the generic string + // tag. Here we just try our best and assume that if the value is unquoted + // then is not necessarily a string. + // TODO: handle float and timestamps + if (!isQuoted && this->GetTagType() == TagType::Str) + { + // Integer + // 0 | -? [1-9] [0-9]* + auto tryInt = this->try_as<int64_t>(); + if (tryInt.has_value()) + { + m_tagType = TagType::Int; + return; + } + + // Boolean. Either 'true' or 'false' + auto tryBool = this->try_as<bool>(); + if (tryBool.has_value()) + { + m_tagType = TagType::Bool; + } + } + } + bool Node::operator<(const Node& other) const { Require(Type::Scalar); @@ -238,31 +315,63 @@ namespace AppInstaller::YAML return m_scalar; } + std::optional<std::string> Node::try_as_dispatch(std::string*) const + { + return std::optional{ m_scalar }; + } + std::wstring Node::as_dispatch(std::wstring*) const { return Utility::ConvertToUTF16(m_scalar); } + std::optional<std::wstring> Node::try_as_dispatch(std::wstring*) const + { + return Utility::TryConvertToUTF16(m_scalar); + } + int64_t Node::as_dispatch(int64_t*) const { return std::stoll(m_scalar); } + std::optional<int64_t> Node::try_as_dispatch(int64_t*) const + { + try + { + return std::optional{ std::stoll(m_scalar) }; + } + catch(...) + { + return {}; + } + } + int Node::as_dispatch(int*) const { // To allow HResult representation return static_cast<int>(std::stoll(m_scalar, 0, 0)); } - bool Node::as_dispatch(bool*) const + std::optional<int> Node::try_as_dispatch(int*) const { - if (Utility::CaseInsensitiveEquals(m_scalar, "true")) + try { - return true; + return std::optional{ static_cast<int>(std::stoll(m_scalar, 0, 0)) }; } - else if (Utility::CaseInsensitiveEquals(m_scalar, "false")) + catch (...) { - return false; + return {}; + } + } + + bool Node::as_dispatch(bool*) const + { + bool* t = nullptr; + auto tryToBool = this->try_as_dispatch(t); + if (tryToBool.has_value()) + { + return tryToBool.value(); } else { @@ -270,6 +379,20 @@ namespace AppInstaller::YAML } } + std::optional<bool> Node::try_as_dispatch(bool*) const + { + if (Utility::CaseInsensitiveEquals(m_scalar, "true")) + { + return std::optional{ true }; + } + else if (Utility::CaseInsensitiveEquals(m_scalar, "false")) + { + return std::optional{ false }; + } + + return {}; + } + Node Load(std::string_view input) { Wrapper::Parser parser(input); diff --git a/src/AppInstallerSharedLib/YamlWrapper.cpp b/src/AppInstallerSharedLib/YamlWrapper.cpp @@ -141,7 +141,10 @@ namespace AppInstaller::YAML::Wrapper pop = true; break; case YAML_SCALAR_NODE: - stackItem.node->SetScalar(ConvertScalarToString(stackItem.yamlNode)); + stackItem.node->SetScalar( + ConvertScalarToString(stackItem.yamlNode), + stackItem.yamlNode->data.scalar.style == YAML_SINGLE_QUOTED_SCALAR_STYLE || + stackItem.yamlNode->data.scalar.style == YAML_DOUBLE_QUOTED_SCALAR_STYLE); pop = true; break; case YAML_SEQUENCE_NODE: diff --git a/src/Microsoft.Management.Configuration.Processor/Extensions/PowerShellExtensions.cs b/src/Microsoft.Management.Configuration.Processor/Extensions/PowerShellExtensions.cs @@ -9,9 +9,6 @@ namespace Microsoft.Management.Configuration.Processor.Extensions using System.Collections.ObjectModel; using System.Management.Automation; using System.Text; - using Microsoft.Management.Configuration.Processor.Exceptions; - using System.Xml.Linq; - using Microsoft.PowerShell.Commands; /// <summary> /// Extensions methods for <see cref="PowerShell"/> class. diff --git a/src/Microsoft.Management.Configuration.Processor/Extensions/ValueSetExtensions.cs b/src/Microsoft.Management.Configuration.Processor/Extensions/ValueSetExtensions.cs @@ -6,14 +6,19 @@ namespace Microsoft.Management.Configuration.Processor.Extensions { + using System; using System.Collections; + using System.Collections.Generic; using Windows.Foundation.Collections; + using WinRT; /// <summary> /// Extensions for ValueSet. /// </summary> internal static class ValueSetExtensions { + private const string TreatAsArray = "treatAsArray"; + /// <summary> /// Extension method to transform a ValueSet to a Hashtable. /// </summary> @@ -25,10 +30,16 @@ namespace Microsoft.Management.Configuration.Processor.Extensions foreach (var keyValuePair in valueSet) { - if (keyValuePair.Value is ValueSet) + if (keyValuePair.Value is ValueSet innerValueSet) { - ValueSet innerValueSet = (ValueSet)keyValuePair.Value; - hashtable.Add(keyValuePair.Key, innerValueSet.ToHashtable()); + if (innerValueSet.ContainsKey(TreatAsArray)) + { + hashtable.Add(keyValuePair.Key, innerValueSet.ToArray()); + } + else + { + hashtable.Add(keyValuePair.Key, innerValueSet.ToHashtable()); + } } else { @@ -38,5 +49,46 @@ namespace Microsoft.Management.Configuration.Processor.Extensions return hashtable; } + + /// <summary> + /// Gets ordered list from a ValueSet that is threated as an array. + /// </summary> + /// <param name="valueSet">ValueSet.</param> + /// <returns>Ordered list.</returns> + public static IList<object> ToArray(this ValueSet valueSet) + { + if (!valueSet.ContainsKey(TreatAsArray)) + { + throw new InvalidOperationException(); + } + + var sortedList = new SortedList<int, object>(); + + foreach (var keyValuePair in valueSet) + { + if (keyValuePair.Key == TreatAsArray) + { + continue; + } + + if (int.TryParse(keyValuePair.Key, out int key)) + { + if (keyValuePair.Value is ValueSet innerValueSet) + { + sortedList.Add(key, innerValueSet.ToHashtable()); + } + else + { + sortedList.Add(key, keyValuePair.Value); + } + } + else + { + throw new InvalidOperationException($"Invalid key for ValueSet to array {keyValuePair.Key}"); + } + } + + return sortedList.Values; + } } } diff --git a/src/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj b/src/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj @@ -11,14 +11,12 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> <!-- Workaround for MSB3271 error on processor architecture mismatch --> <ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch> + <!-- Disable warning for CS1591 for cswinrt auto-generated files --> + <NoWarn>1591</NoWarn> </PropertyGroup> - <PropertyGroup Condition="'$(Configuration)'=='Release'"> - <!-- TODO: cswinrt generate source files that will fail with warning CS1591. - We need to somehow skip these warning via a .editorconfig or something similar. - Ideally cswinrt would fix this in the future. - --> - <!--<TreatWarningsAsErrors>true</TreatWarningsAsErrors>--> + <PropertyGroup Condition="'$(Configuration)'=='Debug'"> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> <PropertyGroup> diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs @@ -232,7 +232,7 @@ properties: Directive1: A Directive2: B settings: - Setting1: 1 + Setting1: '1' Setting2: 2 ")); Assert.NotNull(result.Set); @@ -272,12 +272,55 @@ properties: Assert.Contains("Setting1", settings); Assert.Equal("1", settings["Setting1"]); Assert.Contains("Setting2", settings); - Assert.Equal("2", settings["Setting2"]); + Assert.Equal(2L, settings["Setting2"]); Assert.Null(unit.Details); Assert.Equal(ConfigurationUnitState.Unknown, unit.State); Assert.Null(unit.ResultInformation); Assert.True(unit.ShouldApply); } + + /// <summary> + /// Test type of scalar nodes. + /// </summary> + [Fact] + public void CheckUnitScalarTypes() + { + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(); + + OpenConfigurationSetResult result = processor.OpenConfigurationSet(this.CreateStream(@" +properties: + configurationVersion: 0.1 + resources: + - resource: Resource + id: Identifier + settings: + SettingInt: 1 + SettingString: '1' + SettingBool: false + SettingStringBool: 'false' +")); + Assert.NotNull(result.Set); + Assert.Null(result.ResultCode); + + var units = result.Set.ConfigurationUnits; + Assert.NotNull(units); + Assert.Equal(1, units.Count); + + ConfigurationUnit unit = units[0]; + Assert.NotNull(unit); + + var settings = unit.Settings; + Assert.NotNull(settings); + Assert.Equal(4, settings.Count); + Assert.Contains("SettingInt", settings); + Assert.Equal(1L, settings["SettingInt"]); + Assert.Contains("SettingString", settings); + Assert.Equal("1", settings["SettingString"]); + Assert.Contains("SettingBool", settings); + Assert.Equal(false, settings["SettingBool"]); + Assert.Contains("SettingStringBool", settings); + Assert.Equal("false", settings["SettingStringBool"]); + } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ValueSetExtensionsTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ValueSetExtensionsTests.cs @@ -0,0 +1,344 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ValueSetExtensionsTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System; + using System.Collections; + using System.Collections.Generic; + using Microsoft.Management.Configuration.Processor.Extensions; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Windows.Foundation.Collections; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// ValueSet extension tests. + /// </summary> + [Collection("UnitTestCollection")] + public class ValueSetExtensionsTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="ValueSetExtensionsTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public ValueSetExtensionsTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Tests PropertyExists. + /// </summary> + [Fact] + public void ValueSet_SimpleTypes() + { + string stringProperty = "stringProperty"; + string stringPropertyValue = "string"; + + string intProperty = "intProperty"; + long intPropertyValue = 64; + + string boolProperty = "boolProperty"; + bool boolPropertyValue = true; + + var valueSet = new ValueSet + { + { stringProperty, stringPropertyValue }, + { intProperty, intPropertyValue }, + { boolProperty, boolPropertyValue }, + }; + + var resultHashtable = valueSet.ToHashtable(); + + Assert.NotNull(resultHashtable); + + Assert.True(resultHashtable.ContainsKey(stringProperty)); + Assert.Equal(stringPropertyValue, resultHashtable[stringProperty]); + + Assert.True(resultHashtable.ContainsKey(intProperty)); + Assert.Equal(intPropertyValue, resultHashtable[intProperty]); + + Assert.True(resultHashtable.ContainsKey(boolProperty)); + Assert.Equal(boolPropertyValue, resultHashtable[boolProperty]); + } + + /// <summary> + /// Tests when a ValueSet has inner value sets. + /// </summary> + [Fact] + public void ValueSet_NestedValueSets() + { + string boolPropertyInner = "boolPropertyInner"; + bool boolPropertyValueInner = true; + var valueSetInner = new ValueSet() + { + { boolPropertyInner, boolPropertyValueInner }, + }; + + string stringPropertyInnerInner = "stringPropertyInnerInner"; + string stringPropertyValueInnerInner = "stringInnerInner"; + var valueSetInnerInner = new ValueSet() + { + { stringPropertyInnerInner, stringPropertyValueInnerInner }, + }; + + string inner2Key = "InnerKey2"; + var valueSetInner2 = new ValueSet() + { + { inner2Key, valueSetInnerInner }, + }; + + string key1 = "key1"; + string key2 = "key2"; + var valueSet = new ValueSet() + { + { key1, valueSetInner }, + { key2, valueSetInner2 }, + }; + + var hashtable = valueSet.ToHashtable(); + Assert.NotNull(hashtable); + Assert.Equal(2, hashtable.Count); + Assert.True(hashtable.ContainsKey(key1)); + Assert.True(hashtable.ContainsKey(key2)); + + var key1Result = hashtable[key1] as Hashtable; + Assert.NotNull(key1Result); + Assert.Single(key1Result); + Assert.True(key1Result.ContainsKey(boolPropertyInner)); + Assert.Equal(boolPropertyValueInner, key1Result[boolPropertyInner]); + + var key2Result = hashtable[key2] as Hashtable; + Assert.NotNull(key2Result); + Assert.Single(key2Result); + Assert.True(key2Result.ContainsKey(inner2Key)); + var key2ResultInner = key2Result[inner2Key] as Hashtable; + Assert.NotNull(key2ResultInner); + Assert.True(key2ResultInner.ContainsKey(stringPropertyInnerInner)); + Assert.Equal(stringPropertyValueInnerInner, key2ResultInner[stringPropertyInnerInner]); + } + + /// <summary> + /// Test when ValueSet contains a ValueSet that is threated as an. + /// </summary> + [Fact] + public void ValueSet_ArraySimpleTypes() + { + var valueSetArray = new ValueSet() + { + { "treatAsArray", true }, + { "0", "value1" }, + { "1", "value1" }, + { "2", "value1" }, + { "3", "value1" }, + }; + + string arrayKey = "arrayKey"; + var valueSet = new ValueSet() + { + { arrayKey, valueSetArray }, + }; + + var hashtable = valueSet.ToHashtable(); + Assert.NotNull(hashtable); + Assert.True(hashtable.ContainsKey(arrayKey)); + + var expectedList = hashtable[arrayKey] as IList<object>; + Assert.NotNull(expectedList); + Assert.Equal(4, expectedList.Count); + foreach (var element in expectedList) + { + Assert.IsType<string>(element); + } + } + + /// <summary> + /// Test ValueSet with an array of value sets. + /// </summary> + [Fact] + public void ValueSet_ArrayHashtable() + { + var arrayValue1 = new ValueSet() + { + { "key11", "value11" }, + { "key12", "value12" }, + }; + + var arrayValue2 = new ValueSet() + { + { "key21", "value21" }, + }; + + var valueSetArray = new ValueSet() + { + { "treatAsArray", true }, + { "0", arrayValue1 }, + { "1", arrayValue2 }, + }; + + string arrayKey = "arrayKey"; + var valueSet = new ValueSet() + { + { arrayKey, valueSetArray }, + }; + + var hashtable = valueSet.ToHashtable(); + Assert.NotNull(hashtable); + Assert.True(hashtable.ContainsKey(arrayKey)); + + var expectedList = hashtable[arrayKey] as IList<object>; + Assert.NotNull(expectedList); + Assert.Equal(2, expectedList.Count); + + var resultValue1 = expectedList[0] as Hashtable; + Assert.NotNull(resultValue1); + Assert.Equal(2, resultValue1.Count); + + var resultValue2 = expectedList[1] as Hashtable; + Assert.NotNull(resultValue2); + Assert.Single(resultValue2); + } + + /// <summary> + /// Tests ConvertValueSetToArray. + /// </summary> + [Fact] + public void ValueSet_ArrayOrder() + { + string arrayValue0 = "arrayValue0"; + string arrayValue1 = "arrayValue1"; + string arrayValue2 = "arrayValue2"; + string arrayValue3 = "arrayValue3"; + string arrayValue4 = "arrayValue4"; + string arrayValue5 = "arrayValue5"; + string arrayValue6 = "arrayValue6"; + string arrayValue7 = "arrayValue7"; + string arrayValue8 = "arrayValue8"; + string arrayValue9 = "arrayValue9"; + string arrayValue10 = "arrayValue10"; + string arrayValue11 = "arrayValue11"; + string arrayValue12 = "arrayValue12"; + var valueSetArray = new ValueSet() + { + { "10", arrayValue10 }, + { "7", arrayValue7 }, + { "2", arrayValue2 }, + { "12", arrayValue12 }, + { "6", arrayValue6 }, + { "treatAsArray", true }, + { "3", arrayValue3 }, + { "1", arrayValue1 }, + { "9", arrayValue9 }, + { "0", arrayValue0 }, + { "4", arrayValue4 }, + { "11", arrayValue11 }, + { "8", arrayValue8 }, + { "5", arrayValue5 }, + }; + + var result = valueSetArray.ToArray(); + Assert.NotNull(result); + Assert.Equal(valueSetArray.Count - 1, result.Count); + Assert.Equal(arrayValue0, result[0]); + Assert.Equal(arrayValue1, result[1]); + Assert.Equal(arrayValue2, result[2]); + Assert.Equal(arrayValue3, result[3]); + Assert.Equal(arrayValue4, result[4]); + Assert.Equal(arrayValue5, result[5]); + Assert.Equal(arrayValue6, result[6]); + Assert.Equal(arrayValue7, result[7]); + Assert.Equal(arrayValue8, result[8]); + Assert.Equal(arrayValue9, result[9]); + Assert.Equal(arrayValue10, result[10]); + Assert.Equal(arrayValue11, result[11]); + Assert.Equal(arrayValue12, result[12]); + } + + /// <summary> + /// Tests ConvertValueSetToArray. + /// </summary> + [Fact] + public void ValueSet_InvalidArray() + { + string arrayValue0 = "arrayValue0"; + string arrayValue1 = "arrayValue1"; + string arrayValue2 = "arrayValue2"; + string arrayValue3 = "arrayValue3"; + string arrayValue4 = "arrayValue4"; + string arrayValue5 = "arrayValue5"; + string arrayValue6 = "arrayValue6"; + string arrayValue7 = "arrayValue7"; + string arrayValue8 = "arrayValue8"; + string arrayValue9 = "arrayValue9"; + string arrayValue10 = "arrayValue10"; + string arrayValue11 = "arrayValue11"; + string arrayValue12 = "arrayValue12"; + var valueSetArray = new ValueSet() + { + { "10", arrayValue10 }, + { "7", arrayValue7 }, + { "2", arrayValue2 }, + { "12", arrayValue12 }, + { "6", arrayValue6 }, + { "3", arrayValue3 }, + { "1", arrayValue1 }, + { "9", arrayValue9 }, + { "0", arrayValue0 }, + { "4", arrayValue4 }, + { "11", arrayValue11 }, + { "8", arrayValue8 }, + { "5", arrayValue5 }, + }; + + Assert.Throws<InvalidOperationException>(() => valueSetArray.ToArray()); + } + + /// <summary> + /// Tests ConvertValueSetToArray. + /// </summary> + [Fact] + public void ValueSet_InvalidArrayKey() + { + string arrayValue0 = "arrayValue0"; + string arrayValue1 = "arrayValue1"; + string arrayValue2 = "arrayValue2"; + string arrayValue3 = "arrayValue3"; + string arrayValue4 = "arrayValue4"; + string arrayValue5 = "arrayValue5"; + string arrayValue6 = "arrayValue6"; + string arrayValue7 = "arrayValue7"; + string arrayValue8 = "arrayValue8"; + string arrayValue9 = "arrayValue9"; + string arrayValue10 = "arrayValue10"; + string arrayValue11 = "arrayValue11"; + string arrayValue12 = "arrayValue12"; + var valueSetArray = new ValueSet() + { + { "10", arrayValue10 }, + { "7", arrayValue7 }, + { "2", arrayValue2 }, + { "a", arrayValue12 }, + { "6", arrayValue6 }, + { "3", arrayValue3 }, + { "1", arrayValue1 }, + { "9", arrayValue9 }, + { "0", arrayValue0 }, + { "4", arrayValue4 }, + { "11", arrayValue11 }, + { "8", arrayValue8 }, + { "5", arrayValue5 }, + }; + + Assert.Throws<InvalidOperationException>(() => valueSetArray.ToArray()); + } + } +} diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp @@ -23,8 +23,25 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Returns the appropriate IPropertyValue for the given node, which is assumed to be a scalar. Windows::Foundation::IInspectable GetPropertyValueFromScalar(const Node& node) { - // TODO: Use the tag to determine the type of property to create. - return Windows::Foundation::PropertyValue::CreateString(node.as<std::wstring>()); + ::winrt::Windows::Foundation::IInspectable result; + + switch (node.GetTagType()) + { + case Node::TagType::Null: + return Windows::Foundation::PropertyValue::CreateEmpty(); + case Node::TagType::Bool: + return Windows::Foundation::PropertyValue::CreateBoolean(node.as<bool>()); + case Node::TagType::Str: + return Windows::Foundation::PropertyValue::CreateString(node.as<std::wstring>()); + case Node::TagType::Int: + return Windows::Foundation::PropertyValue::CreateInt64(node.as<int64_t>()); + case Node::TagType::Float: + THROW_HR(E_NOTIMPL); + case Node::TagType::Timestamp: + THROW_HR(E_NOTIMPL); + default: + THROW_HR(E_UNEXPECTED); + } } // Returns the appropriate IPropertyValue for the given node, which is assumed to be a scalar.