Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Validate command to AppWriter #639

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions samples/MSAppGenerator/AppValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions;
using Microsoft.PowerPlatform.PowerApps.Persistence.Models;
using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp;

namespace MSAppGenerator;

public class AppValidator
{
/// <summary>
/// Configures default services for generating the MSApp representation
/// </summary>
private static IServiceProvider ConfigureServiceProvider()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddPowerAppsPersistence(true);
serviceCollection.AddSingleton<IAppGeneratorFactory, AppGeneratorFactory>();
var serviceProvider = serviceCollection.BuildServiceProvider();
return serviceProvider;
}

IServiceProvider _serviceProvider { get; set; }

public AppValidator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public AppValidator()
{
_serviceProvider = ConfigureServiceProvider();
}

private IMsappArchive? GetAppFromFile(string filePath)
{
try
{
var msapp = _serviceProvider.GetRequiredService<IMsappArchiveFactory>().Update(filePath, overwriteOnSave: true);
if (!string.IsNullOrEmpty(msapp.App.Name) && msapp.App.Screens.Count >= 1)

Check warning on line 43 in samples/MSAppGenerator/AppValidator.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Dereference of a possibly null reference.

Check warning on line 43 in samples/MSAppGenerator/AppValidator.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Dereference of a possibly null reference.

Check warning on line 43 in samples/MSAppGenerator/AppValidator.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Dereference of a possibly null reference.

Check warning on line 43 in samples/MSAppGenerator/AppValidator.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest)

Dereference of a possibly null reference.
{
return msapp;
}
}
catch (NullReferenceException ex) {
Console.WriteLine(ex.Message);
}
return null;
}

public bool ValidateMSApp(string filePath, string savePath)
{
var msapp = GetAppFromFile(filePath);

if (msapp == null)
{
return false;
}

msapp.SaveAs(savePath);
return true;
}
}
2 changes: 1 addition & 1 deletion samples/MSAppGenerator/ExampleGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public App GenerateApp(string appName, int numScreens, IList<string>? controls)
}
}

app.Screens.Add(_controlFactory.CreateScreen("Hello from .Net", children: childList.ToArray()));
app.Screens.Add(_controlFactory.CreateScreen("Hello from .Net" + i, children: childList.ToArray()));
}

return app;
Expand Down
87 changes: 83 additions & 4 deletions samples/Test.AppWriter/InputProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ internal class InputProcessor
/// <summary>
/// Validate that the provided filepath is accessible and not an existing file
/// </summary>
private static bool ValidateFilePath(string filePath, out string error)
private static bool ValidateFilePath(string filePath, out string error, bool isFolder = false)
{
error = string.Empty;
if (!File.Exists(filePath))

var dirCheck = false;
if (isFolder)
{
dirCheck &= File.GetAttributes(filePath).HasFlag(FileAttributes.Directory);
}
if (!File.Exists(filePath) || dirCheck)
return true;

Console.WriteLine($"Warning: File '{filePath}' already exists");
Expand Down Expand Up @@ -50,13 +56,39 @@ private static void CreateFunction(bool interactive, string fullPathToMsApp, int
try
{
creator.CreateMSApp(interactive, fullPathToMsApp, numScreens, controlsinfo);

}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

/// <summary>
/// Roundtrips all MSApps in a folder, will generate a report at the outpath
/// </summary>
private static void ValidateFunction(string folderPath, string outPath, int numPasses)
{
var validator = new AppValidator();

// Ensure there are files present in the provided folder
if (Directory.GetFiles(folderPath).Length < 1)
{
Console.WriteLine("No files present in provided file path");
return;
}

for (var i = 0; i < numPasses; i++)
{
foreach (var file in Directory.GetFiles(folderPath))
{
var tempinfo = new FileInfo(file);
var savePath = Path.Combine(outPath, tempinfo.Name);
validator.ValidateMSApp(file, savePath);
}
}
}

/// <summary>
/// Configures and returns the root command to process commandline arguments
/// </summary>
Expand All @@ -69,7 +101,7 @@ public static RootCommand GetRootCommand()
);
var filePathOption = new Option<FileInfo?>(
name: "--filepath",
description: "(string) The path where the msapp file should be generated, including filename and extension",
description: "(string) The desired filepath for the msapp file, including filename and extension",
parseArgument: result =>
{
var filePath = result.Tokens.Single().Value;
Expand All @@ -82,6 +114,35 @@ public static RootCommand GetRootCommand()
}
)
{ IsRequired = true };
var folderPathOption = new Option<FileInfo?>(
name: "--folderpath",
description: "(string) The desired path to folder containing MSAPPs",
parseArgument: result =>
{
var filePath = result.Tokens.Single().Value;

if (ValidateFilePath(filePath, out var error))
return new FileInfo(filePath);

result.ErrorMessage = error;
return null;
}
)
{ IsRequired = true };
var outPathOption = new Option<FileInfo?>(
name: "--outpath",
description: "(string) The path where results of validation should be output",
parseArgument: result =>
{
var filePath = result.Tokens.Single().Value;

if (ValidateFilePath(filePath, out var error))
return new FileInfo(filePath);

result.ErrorMessage = error;
return null;
}
);
var numScreensOption = new Option<int>(
name: "--numscreens",
description: "(integer) The number of screens to generate in the App",
Expand All @@ -98,23 +159,41 @@ public static RootCommand GetRootCommand()
name: "--controls",
description: "(list of string) A list of control templates (i.e. Button Label [Template]...)")
{ AllowMultipleArgumentsPerToken = true };
var numPassesOption = new Option<int>(
name: "--numpasses",
description: "(integer) The number of passes to roundtrip load/save each MSApp",
getDefaultValue: () => 1
);

var rootCommand = new RootCommand("Test Writer for MSApp files.");

var createCommand = new Command("create", "Create a new MSApp at the specified path.")
{
interactiveOption,
filePathOption,
numScreensOption,
controlsOptions
};

createCommand.SetHandler((interactive, filepath, numscreens, controls) =>
{
CreateFunction(interactive, filepath!.FullName, numscreens, controls);
}, interactiveOption, filePathOption, numScreensOption, controlsOptions);

rootCommand.AddCommand(createCommand);

var validateCommand = new Command("validate", "Validate an group of existing MSApps at the specified path.")
{
folderPathOption,
outPathOption,
numPassesOption
};
validateCommand.SetHandler((folderpath, outpath, numpasses) =>
{
ValidateFunction(folderpath!.FullName, outpath!.FullName, numpasses);
}, folderPathOption, outPathOption, numPassesOption);

rootCommand.AddCommand(validateCommand);

return rootCommand;
}
}
34 changes: 34 additions & 0 deletions src/Persistence.Tests/MsApp/MsappArchiveSaveTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.IO.Compression;
using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp;

namespace Persistence.Tests.MsApp;
Expand Down Expand Up @@ -136,4 +137,37 @@ public void Msapp_ShouldSave_WithUniqueName()
msappArchive.CanonicalEntries.Count.Should().Be(sameNames.Length);
msappArchive.CanonicalEntries.Keys.Should().Contain(MsappArchive.NormalizePath(Path.Combine(MsappArchive.Directories.Src, @$"SameName{sameNames.Length + 1}{MsappArchive.YamlPaFileExtension}")));
}

[TestMethod]
[DataRow(@"_TestData/AppsWithYaml/HelloWorld.msapp", "App", "HelloScreen")]
public void Msapp_ShouldSaveAs_NewFilePath(string testDirectory, string appName, string screenName)
{
// Arrange
var tempFile = Path.Combine(TestContext.DeploymentDirectory!, Path.GetRandomFileName());

// Zip archive in memory from folder
using var stream = new MemoryStream();
using (var zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, true))
{
var files = Directory.GetFiles(testDirectory, "*", SearchOption.AllDirectories);
foreach (var file in files)
{
zipArchive.CreateEntryFromFile(file, file.Substring(testDirectory.Length + 1));
}
}
using var testApp = MsappArchiveFactory.Update(stream, overwriteOnSave: true);

// Save the test app to another file
testApp.SaveAs(tempFile);

// Open the app from the file
using var msappValidation = MsappArchiveFactory.Open(tempFile);

// Assert
msappValidation.App.Should().NotBeNull();
msappValidation.App!.Screens.Count.Should().Be(1);
msappValidation.App.Screens.Single().Name.Should().Be(screenName);
msappValidation.App.Name.Should().Be(appName);
msappValidation.CanonicalEntries.Keys.Should().Contain(MsappArchive.NormalizePath(MsappArchive.HeaderFileName));
}
}
10 changes: 10 additions & 0 deletions src/Persistence/MsApp/IMsappArchive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ public interface IMsappArchive : IDisposable
/// </summary>
void Save();

/// <summary>
/// Saves the archive to the provided file.
/// </summary>
void SaveAs(string filePath);

/// <summary>
/// Saves the archive to the provided stream.
/// </summary>
void SaveAs(Stream stream);

/// <summary>
/// Total sum of decompressed sizes of all entries in the archive.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Persistence/MsApp/IMsappArchiveFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ public interface IMsappArchiveFactory
/// <summary>
/// Opens existing msapp archive for update (read/write).
/// </summary>
IMsappArchive Update(string path);
IMsappArchive Update(string path, bool overwriteOnSave = false);

/// <summary>
/// Opens existing msapp archive for update (read/write).
/// </summary>
IMsappArchive Update(Stream stream, bool leaveOpen = false);
IMsappArchive Update(Stream stream, bool leaveOpen = false, bool overwriteOnSave = false);
}
Loading