From 9816fb6870d29a5b0bfd761b67128092d557ae16 Mon Sep 17 00:00:00 2001 From: Andrew Petrochuk <30735471+petrochuk@users.noreply.github.com> Date: Sun, 28 Jan 2024 17:49:30 -0800 Subject: [PATCH] Moved MsappArchive to Persistence (#529) Co-authored-by: Andrew Petrochuk (from Dev Box) --- .editorconfig | 2 +- .../Exceptions/SerializationException.cs | 24 ---- ...rosoft.PowerPlatform.Formulas.Tools.csproj | 1 - src/PAModel/Model/Control.cs | 46 -------- src/PAModel/Model/ControlEditorState.cs | 21 ---- src/PAModel/Model/RuleEditorState.cs | 17 --- src/PAModel/Model/TaggedRuleEditorState.cs | 9 -- src/PAModel/packages.lock.json | 76 +++++++----- .../Apps/WithYaml/HelloWorld.msapp | Bin 17811 -> 0 bytes src/PAModelTests/DataSourceTests.cs | 2 +- src/PASopa.sln | 2 +- .../Extensions/StringExtensions.cs | 12 ++ ...latform.PowerApps.Persistence.Tests.csproj | 31 ----- .../Model/InvalidControls.cs | 22 ++++ .../MsApp/MsappArchiveTests.cs | 22 ++-- .../Persistence.Tests.csproj | 36 ++++++ .../Yaml/DeserializerInvalidTests.cs | 39 +++++++ ...izerTests.cs => DeserializerValidTests.cs} | 52 ++++++--- src/Persistence.Tests/Yaml/RoundTripTests.cs | 37 ++++++ ...alizerTests.cs => ValidSerializerTests.cs} | 22 ++-- .../_TestData/AppsWithYaml/HelloWorld.msapp | Bin 0 -> 19097 bytes .../_TestData/InvalidYaml/InvalidName.fx.yaml | 2 + .../InvalidYaml/NestedScreen.fx.yaml | 3 + .../_TestData/InvalidYaml/NoScreen.fx.yaml | 3 + .../Control-with-custom-template.yaml | 12 ++ .../Screen-with-controls-as-temples.fx.yaml | 34 ++++++ .../ValidYaml/Screen-with-controls.fx.yaml | 34 ++++++ .../ValidYaml/Screen-with-name.fx.yaml | 2 + .../Attributes/FirstClassAttribute.cs | 3 + .../Exceptions/PersistenceException.cs | 19 +++ .../JsonDoubleToIntConverter.cs | 2 +- ...PowerPlatform.PowerApps.Persistence.csproj | 2 + src/Persistence/Models/BuiltInTemplates.cs | 46 ++++++++ .../Models/BuiltInTemplatesUris.cs | 11 -- src/Persistence/Models/Button.cs | 11 +- src/Persistence/Models/Control.cs | 41 ++++++- src/Persistence/Models/ControlEditorState.cs | 25 ++++ src/Persistence/Models/CustomControl.cs | 14 ++- src/Persistence/Models/Screen.cs | 11 +- src/Persistence/Models/Text.cs | 11 +- .../MsApp/IMsappArchive.cs | 4 +- .../MsApp/MsappArchive.cs | 110 ++++++++---------- .../Yaml/ControlTypeDiscriminator.cs | 38 ++++++ src/Persistence/Yaml/ControlTypeInspector.cs | 38 ++++++ .../Yaml/EmptyPropertyDescriptor.cs | 41 +++++++ .../Yaml/SerializerBuilderExtensions.cs | 63 ++-------- src/Persistence/packages.lock.json | 78 +++++++++++++ 47 files changed, 767 insertions(+), 364 deletions(-) delete mode 100644 src/PAModel/Exceptions/SerializationException.cs delete mode 100644 src/PAModel/Model/Control.cs delete mode 100644 src/PAModel/Model/ControlEditorState.cs delete mode 100644 src/PAModel/Model/RuleEditorState.cs delete mode 100644 src/PAModel/Model/TaggedRuleEditorState.cs delete mode 100644 src/PAModelTests/Apps/WithYaml/HelloWorld.msapp create mode 100644 src/Persistence.Tests/Extensions/StringExtensions.cs delete mode 100644 src/Persistence.Tests/Microsoft.PowerPlatform.PowerApps.Persistence.Tests.csproj create mode 100644 src/Persistence.Tests/Model/InvalidControls.cs rename src/{PAModelTests => Persistence.Tests}/MsApp/MsappArchiveTests.cs (81%) create mode 100644 src/Persistence.Tests/Persistence.Tests.csproj create mode 100644 src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs rename src/Persistence.Tests/Yaml/{YamlDeserializerTests.cs => DeserializerValidTests.cs} (77%) create mode 100644 src/Persistence.Tests/Yaml/RoundTripTests.cs rename src/Persistence.Tests/Yaml/{YamlSerializerTests.cs => ValidSerializerTests.cs} (88%) create mode 100644 src/Persistence.Tests/_TestData/AppsWithYaml/HelloWorld.msapp create mode 100644 src/Persistence.Tests/_TestData/InvalidYaml/InvalidName.fx.yaml create mode 100644 src/Persistence.Tests/_TestData/InvalidYaml/NestedScreen.fx.yaml create mode 100644 src/Persistence.Tests/_TestData/InvalidYaml/NoScreen.fx.yaml create mode 100644 src/Persistence.Tests/_TestData/ValidYaml/Control-with-custom-template.yaml create mode 100644 src/Persistence.Tests/_TestData/ValidYaml/Screen-with-controls-as-temples.fx.yaml create mode 100644 src/Persistence.Tests/_TestData/ValidYaml/Screen-with-controls.fx.yaml create mode 100644 src/Persistence.Tests/_TestData/ValidYaml/Screen-with-name.fx.yaml create mode 100644 src/Persistence/Exceptions/PersistenceException.cs rename src/{PAModel => Persistence}/JsonConverters/JsonDoubleToIntConverter.cs (89%) create mode 100644 src/Persistence/Models/BuiltInTemplates.cs delete mode 100644 src/Persistence/Models/BuiltInTemplatesUris.cs create mode 100644 src/Persistence/Models/ControlEditorState.cs rename src/{PAModel => Persistence}/MsApp/IMsappArchive.cs (88%) rename src/{PAModel => Persistence}/MsApp/MsappArchive.cs (70%) create mode 100644 src/Persistence/Yaml/ControlTypeDiscriminator.cs create mode 100644 src/Persistence/Yaml/ControlTypeInspector.cs create mode 100644 src/Persistence/Yaml/EmptyPropertyDescriptor.cs diff --git a/.editorconfig b/.editorconfig index b922094c..d01072b8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,7 @@ insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true -[*.{json,js}] +[*.{json,js,yaml,yml}] indent_size = 2 tab_width = 2 diff --git a/src/PAModel/Exceptions/SerializationException.cs b/src/PAModel/Exceptions/SerializationException.cs deleted file mode 100644 index 91209645..00000000 --- a/src/PAModel/Exceptions/SerializationException.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.Formulas.Tools; - -internal class SerializationException : Exception -{ - public string FileName { get; init; } - - public SerializationException(string message) - : base(message) - { - } - - public SerializationException(string message, Exception innerException) - : base(message, innerException) - { - } - - public SerializationException(string message, string fileName, Exception innerException) - { - FileName = fileName; - } -} diff --git a/src/PAModel/Microsoft.PowerPlatform.Formulas.Tools.csproj b/src/PAModel/Microsoft.PowerPlatform.Formulas.Tools.csproj index ddb5d0b2..5bbbf5e6 100644 --- a/src/PAModel/Microsoft.PowerPlatform.Formulas.Tools.csproj +++ b/src/PAModel/Microsoft.PowerPlatform.Formulas.Tools.csproj @@ -55,7 +55,6 @@ - diff --git a/src/PAModel/Model/Control.cs b/src/PAModel/Model/Control.cs deleted file mode 100644 index 4b12697d..00000000 --- a/src/PAModel/Model/Control.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.Formulas.Tools.Model; - -[DebuggerDisplay("{Name}")] -public record Control -{ - public Control() - { - - } - - public Control(ControlEditorState editorState) - { - EditorState = editorState ?? throw new ArgumentNullException(nameof(editorState)); - Name = editorState.Name; - Type = editorState.Type; - - if (editorState.Children != null) - { - var childControls = new List(); - foreach (var child in editorState.Children) - { - childControls.Add(new Control(child)); - } - Controls = childControls; - editorState.Children = null; - } - } - - [YamlIgnore] - public ControlEditorState EditorState { get; set; } - - public string Name { get; init; } - - [YamlMember(Alias = "Control")] - public string Type { get; init; } - - public IList Controls { get; init; } - - public IDictionary Properties { get; init; } -} diff --git a/src/PAModel/Model/ControlEditorState.cs b/src/PAModel/Model/ControlEditorState.cs deleted file mode 100644 index 5f02d371..00000000 --- a/src/PAModel/Model/ControlEditorState.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json.Serialization; -using Microsoft.PowerPlatform.Formulas.Tools.JsonConverters; - -namespace Microsoft.PowerPlatform.Formulas.Tools.Model; - -public record ControlEditorState -{ - public string Name { get; init; } - - public string Type { get; set; } - - [JsonConverter(typeof(JsonDoubleToIntConverter))] - public int Index { get; set; } - - public ControlEditorState[] Children { get; set; } - - public IList Rules { get; init; } -} diff --git a/src/PAModel/Model/RuleEditorState.cs b/src/PAModel/Model/RuleEditorState.cs deleted file mode 100644 index 69c42ce1..00000000 --- a/src/PAModel/Model/RuleEditorState.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics; - -namespace Microsoft.PowerPlatform.Formulas.Tools.Model; - -[DebuggerDisplay("{Category} / {Property} / {InvariantScript}")] -public record RuleEditorState -{ - public string Category { get; init; } - public string Property { get; init; } - public string NameMap { get; init; } - public string InvariantScript { get; init; } - public string RuleProviderType { get; init; } - public IList TaggedRuleArray { get; init; } -} diff --git a/src/PAModel/Model/TaggedRuleEditorState.cs b/src/PAModel/Model/TaggedRuleEditorState.cs deleted file mode 100644 index 46ecee27..00000000 --- a/src/PAModel/Model/TaggedRuleEditorState.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.Formulas.Tools.Model; - -public record TaggedRuleEditorState : RuleEditorState -{ - public string Tag { get; init; } -} diff --git a/src/PAModel/packages.lock.json b/src/PAModel/packages.lock.json index d650fb01..ae72a3de 100644 --- a/src/PAModel/packages.lock.json +++ b/src/PAModel/packages.lock.json @@ -2,19 +2,6 @@ "version": 1, "dependencies": { "net7.0": { - "Microsoft.Extensions.Logging": { - "type": "Direct", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "6.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "System.Diagnostics.DiagnosticSource": "6.0.0" - } - }, "Newtonsoft.Json": { "type": "Direct", "requested": "[13.0.1, )", @@ -46,46 +33,73 @@ "resolved": "15.1.0", "contentHash": "fXrqmKkzBtXeJiHEsZOPEWkonHweiwk/l0Hqhz4yMIZPh57kZy03Xbj2/e8HV1QIkTw7yeBe9bbphuE3YiI4vQ==" }, - "Microsoft.Extensions.DependencyInjection": { + "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "6.0.0", - "contentHash": "k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "elNeOmkeX3eDVG6pYVeV82p29hr+UKDaBhrZyWvWLw/EVZSYEkZlQdkp0V39k/Xehs2Qa0mvoCvkVj3eQxNQ1Q==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + "resolved": "7.0.0", + "contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "Nw2muoNrOG5U5qa2ZekXwudUn2BJcD41e65zwmDHb1fQegTX66UokLWZkJRpqSSHXDOWZ5V0iqhbxOEky91atA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "7.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Logging.Abstractions": "7.0.0", + "Microsoft.Extensions.Options": "7.0.0" + } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" + "resolved": "7.0.0", + "contentHash": "kmn78+LPVMOWeITUjIlfxUPDsI0R6G0RkeAMBmQxAJ7vBJn4q2dTva7pWi65ceN5vPGjJ9q/Uae2WKgvfktJAw==" }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "resolved": "7.0.0", + "contentHash": "lP1yBnTTU42cKpMozuafbvNtQ7QcBjr/CcK3bYOGEMH55Fjt+iecXjT6chR7vbgCMqy3PG3aNQSZgo/EuY/9qQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Primitives": "6.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0", + "Microsoft.Extensions.Primitives": "7.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", + "resolved": "7.0.0", + "contentHash": "um1KU5kxcRp3CNuI8o/GrZtD4AIOXDk+RLsytjZ9QPok3ttLUelLKpilVPuaFT3TFjOhSibUAso0odbOaCDj3Q==" + }, + "Microsoft.PowerFx.Core": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "M9DY6FqWUXUPgjbbHsB3vt7+b4QPFaoKBZ7ixSU7cPgHKzo/aNohzYWxYasXwTV6pamv5gX9AqLuF13RimMPdg==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.PowerFx.Transport.Attributes": "1.2.0", + "System.Collections.Immutable": "6.0.0" } }, - "System.Diagnostics.DiagnosticSource": { + "Microsoft.PowerFx.Transport.Attributes": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "zlvi59W/4MdDJeR2rIL8k633V+FIHtxbhyaurNBZevksNWCZ49AV0bUhpB6cUjD/vwci8ZXXWziw0yYnjXIzNg==" + }, + "System.Collections.Immutable": { "type": "Transitive", "resolved": "6.0.0", - "contentHash": "frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==", + "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "6.0.0" } @@ -98,6 +112,8 @@ "microsoft.powerplatform.powerapps.persistence": { "type": "Project", "dependencies": { + "Microsoft.Extensions.Logging": "[7.0.0, )", + "Microsoft.PowerFx.Core": "[1.2.0, )", "YamlDotNet": "[15.1.0, )" } } diff --git a/src/PAModelTests/Apps/WithYaml/HelloWorld.msapp b/src/PAModelTests/Apps/WithYaml/HelloWorld.msapp deleted file mode 100644 index 2eab3da5d933c5f0448678a88e0151e9d39ed153..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17811 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 diff --git a/src/PAModelTests/DataSourceTests.cs b/src/PAModelTests/DataSourceTests.cs index 78e708da..84a2aac7 100644 --- a/src/PAModelTests/DataSourceTests.cs +++ b/src/PAModelTests/DataSourceTests.cs @@ -9,7 +9,7 @@ using Microsoft.PowerPlatform.Formulas.Tools; using Microsoft.PowerPlatform.Formulas.Tools.Extensions; using Microsoft.PowerPlatform.Formulas.Tools.IO; -using Microsoft.PowerPlatform.Formulas.Tools.MsApp; +using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; namespace PAModelTests; diff --git a/src/PASopa.sln b/src/PASopa.sln index 78c6ddc3..35b00095 100644 --- a/src/PASopa.sln +++ b/src/PASopa.sln @@ -18,7 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerPlatform.PowerApps.Persistence", "Persistence\Microsoft.PowerPlatform.PowerApps.Persistence.csproj", "{906B4EA5-F287-4D0E-A511-0D89B2DD9C14}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.PowerPlatform.PowerApps.Persistence.Tests", "Persistence.Tests\Microsoft.PowerPlatform.PowerApps.Persistence.Tests.csproj", "{8AB1C901-FE5E-44BF-AA21-B8F20A9D7CDD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Tests", "Persistence.Tests\Persistence.Tests.csproj", "{8AB1C901-FE5E-44BF-AA21-B8F20A9D7CDD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Persistence.Tests/Extensions/StringExtensions.cs b/src/Persistence.Tests/Extensions/StringExtensions.cs new file mode 100644 index 00000000..fd86147a --- /dev/null +++ b/src/Persistence.Tests/Extensions/StringExtensions.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Persistence.Tests.Extensions; + +internal static class StringExtensions +{ + public static string NormalizeNewlines(this string x) + { + return x.Replace("\r\n", "\n").Replace("\r", "\n"); + } +} diff --git a/src/Persistence.Tests/Microsoft.PowerPlatform.PowerApps.Persistence.Tests.csproj b/src/Persistence.Tests/Microsoft.PowerPlatform.PowerApps.Persistence.Tests.csproj deleted file mode 100644 index adea0f14..00000000 --- a/src/Persistence.Tests/Microsoft.PowerPlatform.PowerApps.Persistence.Tests.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - $(TargetFrameworkVersion) - enable - enable - - false - true - false - - - - - - - - - - - - - - - - - - - - diff --git a/src/Persistence.Tests/Model/InvalidControls.cs b/src/Persistence.Tests/Model/InvalidControls.cs new file mode 100644 index 00000000..0b7069a6 --- /dev/null +++ b/src/Persistence.Tests/Model/InvalidControls.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerPlatform.PowerApps.Persistence.Models; + +namespace Persistence.Tests.Model; + +[TestClass] +public class InvalidControls +{ + [TestMethod] + [DataRow("")] + [DataRow(" ")] + public void Constructor_InvalidControlName_Throws(string controlName) + { + // Act + Action act = () => new Screen(controlName); + + // Assert + act.Should().Throw(); + } +} diff --git a/src/PAModelTests/MsApp/MsappArchiveTests.cs b/src/Persistence.Tests/MsApp/MsappArchiveTests.cs similarity index 81% rename from src/PAModelTests/MsApp/MsappArchiveTests.cs rename to src/Persistence.Tests/MsApp/MsappArchiveTests.cs index 3f5b25b4..0751b182 100644 --- a/src/PAModelTests/MsApp/MsappArchiveTests.cs +++ b/src/Persistence.Tests/MsApp/MsappArchiveTests.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.IO; using System.IO.Compression; -using System.Linq; -using Microsoft.PowerPlatform.Formulas.Tools.MsApp; +using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; +using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; -namespace PAModelTests.MsApp; +namespace Persistence.Tests.MsApp; [TestClass] public class MsappArchiveTests @@ -74,22 +73,19 @@ public void AddEntryTests(string[] entries) } [TestMethod] - [DataRow(@"Apps/WithYaml/HelloWorld.msapp", 14, 2, "HelloScreen", "screen", 8)] - [DataRow(@"Apps/AppWithLabel.msapp", 11, 2, "Screen1", "ControlInfo", 8)] - public void GetTopLevelControlsTests(string testFile, int allEntriesCount, int controlsCount, + [DataRow(@"_TestData/AppsWithYaml/HelloWorld.msapp", 14, 2, "HelloScreen", "screen", 8)] + public void Msapp_ShouldHave_Screens(string testFile, int allEntriesCount, int controlsCount, string topLevelControlName, string topLevelControlType, int topLevelRulesCount) { // Arrange: Create new ZipArchive in memory - using var msappArchive = new MsappArchive(testFile); + using var msappArchive = new MsappArchive(testFile, YamlSerializationFactory.CreateDeserializer()); // Assert msappArchive.CanonicalEntries.Count.Should().Be(allEntriesCount); - msappArchive.TopLevelControls.Count.Should().Be(controlsCount); - msappArchive.TopLevelControls.Should().ContainSingle(c => c.Name == "App"); + msappArchive.Screens.Count.Should().Be(controlsCount); + msappArchive.Screens.Should().ContainSingle(c => c.Name == "App"); - var topLevelControl = msappArchive.TopLevelControls.Single(c => c.Name == topLevelControlName); - topLevelControl.EditorState.Rules.Count.Should().Be(topLevelRulesCount); - topLevelControl.Type.Should().Be(topLevelControlType); + var screen = msappArchive.Screens.Single(c => c.Name == topLevelControlName); } } diff --git a/src/Persistence.Tests/Persistence.Tests.csproj b/src/Persistence.Tests/Persistence.Tests.csproj new file mode 100644 index 00000000..494e525e --- /dev/null +++ b/src/Persistence.Tests/Persistence.Tests.csproj @@ -0,0 +1,36 @@ + + + + $(TargetFrameworkVersion) + enable + enable + + false + true + false + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs b/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs new file mode 100644 index 00000000..3b30b0f7 --- /dev/null +++ b/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; +using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; + +namespace Persistence.Tests.Yaml; + +[TestClass] +public class DeserializerInvalidTests +{ + [TestMethod] + public void Deserialize_ShouldFail() + { + // Arrange + var deserializer = YamlSerializationFactory.CreateDeserializer(); + + var files = Directory.GetFiles(@"_TestData/InvalidYaml", $"*{MsappArchive.YamlFxFileExtension}", SearchOption.AllDirectories); + // Uncomment to test single file + // var files = new string[] { @"_TestData/InvalidYaml/InvalidName.fx.yaml" }; + + Parallel.ForEach(files, file => + { + using var yamlStream = File.OpenRead(file); + using var yamlReader = new StreamReader(yamlStream); + + // Act + try + { + var screen = deserializer.Deserialize(yamlReader); + Assert.Fail($"Expected exception for file {file}"); + } + catch (Exception) + { + // Assert exceptions are thrown + } + }); + } +} diff --git a/src/Persistence.Tests/Yaml/YamlDeserializerTests.cs b/src/Persistence.Tests/Yaml/DeserializerValidTests.cs similarity index 77% rename from src/Persistence.Tests/Yaml/YamlDeserializerTests.cs rename to src/Persistence.Tests/Yaml/DeserializerValidTests.cs index be86a515..f1fde363 100644 --- a/src/Persistence.Tests/Yaml/YamlDeserializerTests.cs +++ b/src/Persistence.Tests/Yaml/DeserializerValidTests.cs @@ -4,10 +4,10 @@ using Microsoft.PowerPlatform.PowerApps.Persistence.Models; using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Tests.Yaml; +namespace Persistence.Tests.Yaml; [TestClass] -public class YamlDeserializerTests +public class DeserializerValidTests { [TestMethod] [DataRow("I am a screen with spaces", "42")] @@ -19,9 +19,8 @@ public class YamlDeserializerTests [DataRow("Cos'è questo?", "---")] public void Deserialize_ShouldParseSimpleStructure(string textValue, string xValue) { - var graph = new Screen() + var graph = new Screen("Screen1") { - Name = "Screen1", Properties = new Dictionary() { { "Text", new() { Value = textValue } }, @@ -38,7 +37,7 @@ public void Deserialize_ShouldParseSimpleStructure(string textValue, string xVal var sut = deserializer.Deserialize(yaml); sut.Should().NotBeNull().And.BeOfType(); sut.Name.Should().Be("Screen1"); - sut.ControlUri.Should().Be(BuiltInTemplatesUris.Screen); + sut.ControlUri.Should().Be(BuiltInTemplates.Screen); sut.Controls.Should().NotBeNull().And.BeEmpty(); sut.Properties.Should().NotBeNull() .And.HaveCount(3) @@ -51,26 +50,23 @@ public void Deserialize_ShouldParseSimpleStructure(string textValue, string xVal [TestMethod] public void Deserialize_ShouldParseYamlWithChildNodes() { - var graph = new Screen() + var graph = new Screen("Screen1") { - Name = "Screen1", Properties = new Dictionary() { { "Text", new() { Value = "I am a screen" } }, }, Controls = new Control[] { - new Text() + new Text("Label1") { - Name = "Label1", Properties = new Dictionary() { { "Text", new() { Value = "lorem ipsum" } }, }, }, - new Button() + new Button("Button1") { - Name = "Button1", Properties = new Dictionary() { { "Text", new() { Value = "click me" } }, @@ -89,7 +85,7 @@ public void Deserialize_ShouldParseYamlWithChildNodes() var sut = deserializer.Deserialize(yaml); sut.Should().NotBeNull().And.BeOfType(); sut.Name.Should().Be("Screen1"); - sut.ControlUri.Should().Be(BuiltInTemplatesUris.Screen); + sut.ControlUri.Should().Be(BuiltInTemplates.Screen); sut.Properties.Should().NotBeNull() .And.HaveCount(1) .And.ContainKey("Text"); @@ -98,7 +94,7 @@ public void Deserialize_ShouldParseYamlWithChildNodes() sut.Controls.Should().NotBeNull().And.HaveCount(2); sut.Controls![0].Should().BeOfType(); sut.Controls![0].Name.Should().Be("Label1"); - sut.Controls![0].ControlUri.Should().Be(BuiltInTemplatesUris.Text); + sut.Controls![0].ControlUri.Should().Be(BuiltInTemplates.Text); sut.Controls![0].Properties.Should().NotBeNull() .And.HaveCount(1) .And.ContainKey("Text"); @@ -106,7 +102,7 @@ public void Deserialize_ShouldParseYamlWithChildNodes() sut.Controls![1].Should().BeOfType