Use OperationGoalState for intermediate steps in Install and GenerateGoalState

This commit is contained in:
Jimmy Lewis 2024-04-16 17:52:07 -07:00
Родитель eb732f03b0
Коммит 4b79145702
8 изменённых файлов: 149 добавлений и 46 удалений

Просмотреть файл

@ -374,7 +374,13 @@ namespace Microsoft.Web.LibraryManager.Contracts
&& normalizedFilePath.StartsWith(normalizedRootDirectory, StringComparison.OrdinalIgnoreCase);
}
internal static string NormalizePath(string path)
/// <summary>
/// Normalizes the path string so it can be easily compared.
/// </summary>
/// <remarks>
/// Result will be lowercase and have any trailing slashes removed.
/// </remarks>
public static string NormalizePath(string path)
{
if (string.IsNullOrEmpty(path))
{

Просмотреть файл

@ -17,11 +17,11 @@ namespace Microsoft.Web.LibraryManager.Contracts
public static class PredefinedErrors
{
/// <summary>
/// Represents an unhandled exception that occured in the provider.
/// Represents an unhandled exception that occurred in the provider.
/// </summary>
/// <remarks>
/// An <see cref="IProvider.InstallAsync"/> should never throw and this error
/// should be used as when catching generic exeptions.
/// should be used as when catching generic exceptions.
/// </remarks>
/// <returns>The error code LIB000</returns>
public static IError UnknownException()
@ -198,6 +198,12 @@ namespace Microsoft.Web.LibraryManager.Contracts
public static IError FileNameMustNotBeEmpty(string libraryId)
=> new Error("LIB020", string.Format(Text.ErrorFilePathIsEmpty, libraryId));
/// <summary>
/// A library mapping does not have a destination specified
/// </summary>
public static IError DestinationNotSpecified(string libraryId)
=> new Error("LIB021", string.Format(Text.ErrorDestinationNotSpecified, libraryId));
/// <summary>
/// The manifest must specify a version
/// </summary>

Просмотреть файл

@ -87,6 +87,15 @@ namespace Microsoft.Web.LibraryManager.Contracts.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to The &quot;{0}&quot; library is missing a destination..
/// </summary>
internal static string ErrorDestinationNotSpecified {
get {
return ResourceManager.GetString("ErrorDestinationNotSpecified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The &quot;{0}&quot; destination file path has invalid characters.
/// </summary>

Просмотреть файл

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -187,6 +187,9 @@ Valid files are {2}</value>
<data name="ErrorFilePathIsEmpty" xml:space="preserve">
<value>The library "{0}" cannot specify a file with an empty name</value>
</data>
<data name="ErrorDestinationNotSpecified" xml:space="preserve">
<value>The "{0}" library is missing a destination.</value>
</data>
<data name="ErrorMissingManifestVersion" xml:space="preserve">
<value>The Library Manager manifest must specify a version.</value>
</data>

Просмотреть файл

@ -60,10 +60,25 @@ namespace Microsoft.Web.LibraryManager.Providers
return LibraryOperationResult.FromCancelled(desiredState);
}
ILibraryCatalog catalog = GetCatalog();
ILibrary library = await catalog.GetLibraryAsync(desiredState.Name, desiredState.Version, cancellationToken).ConfigureAwait(false);
OperationResult<ILibrary> getLibrary = await GetLibraryForInstallationState(desiredState, cancellationToken).ConfigureAwait(false);
if (!getLibrary.Success)
{
return new LibraryOperationResult(desiredState, [.. getLibrary.Errors])
{
Cancelled = getLibrary.Cancelled,
};
}
LibraryInstallationGoalState goalState = GenerateGoalState(desiredState, library);
OperationResult<LibraryInstallationGoalState> getGoalState = GenerateGoalState(desiredState, getLibrary.Result);
if (!getGoalState.Success)
{
return new LibraryOperationResult(desiredState, [.. getGoalState.Errors])
{
Cancelled = getGoalState.Cancelled,
};
}
LibraryInstallationGoalState goalState = getGoalState.Result;
if (!IsSourceCacheReady(goalState))
{
@ -83,8 +98,30 @@ namespace Microsoft.Web.LibraryManager.Providers
}
private async Task<LibraryOperationResult> InstallFiles(LibraryInstallationGoalState goalState, CancellationToken cancellationToken)
private async Task<OperationResult<ILibrary>> GetLibraryForInstallationState(ILibraryInstallationState desiredState, CancellationToken cancellationToken)
{
ILibrary library;
try
{
ILibraryCatalog catalog = GetCatalog();
library = await catalog.GetLibraryAsync(desiredState.Name, desiredState.Version, cancellationToken).ConfigureAwait(false);
}
catch (InvalidLibraryException)
{
string libraryId = LibraryNamingScheme.GetLibraryId(desiredState.Name, desiredState.Version);
return OperationResult<ILibrary>.FromError(PredefinedErrors.UnableToResolveSource(libraryId, desiredState.ProviderId));
}
catch (Exception ex)
{
HostInteraction.Logger.Log(ex.ToString(), LogLevel.Error);
return OperationResult<ILibrary>.FromError(PredefinedErrors.UnknownException());
}
return OperationResult<ILibrary>.FromSuccess(library);
}
private async Task<LibraryOperationResult> InstallFiles(LibraryInstallationGoalState goalState, CancellationToken cancellationToken)
{
try
{
foreach (KeyValuePair<string, string> kvp in goalState.InstalledFiles)
@ -196,9 +233,16 @@ namespace Microsoft.Web.LibraryManager.Providers
#endregion
public LibraryInstallationGoalState GenerateGoalState(ILibraryInstallationState desiredState, ILibrary library)
private OperationResult<LibraryInstallationGoalState> GenerateGoalState(ILibraryInstallationState desiredState, ILibrary library)
{
var goalState = new LibraryInstallationGoalState(desiredState);
List<IError> errors = null;
if (string.IsNullOrEmpty(desiredState.DestinationPath))
{
return OperationResult<LibraryInstallationGoalState>.FromError(PredefinedErrors.DestinationNotSpecified(desiredState.Name));
}
IEnumerable<string> outFiles;
if (desiredState.Files == null || desiredState.Files.Count == 0)
{
@ -209,20 +253,39 @@ namespace Microsoft.Web.LibraryManager.Providers
outFiles = FileGlobbingUtility.ExpandFileGlobs(desiredState.Files, library.Files.Keys);
}
if (library.GetInvalidFiles(outFiles.ToList()) is IReadOnlyList<string> invalidFiles
&& invalidFiles.Count > 0)
{
errors ??= [];
errors.Add(PredefinedErrors.InvalidFilesInLibrary(desiredState.Name, invalidFiles, library.Files.Keys));
}
foreach (string outFile in outFiles)
{
// strip the source prefix
string destinationFile = Path.Combine(HostInteraction.WorkingDirectory, desiredState.DestinationPath, outFile);
if (!FileHelpers.IsUnderRootDirectory(destinationFile, HostInteraction.WorkingDirectory))
{
errors ??= [];
errors.Add(PredefinedErrors.PathOutsideWorkingDirectory());
}
destinationFile = FileHelpers.NormalizePath(destinationFile);
// don't forget to include the cache folder in the path
string sourceFile = GetCachedFileLocalPath(desiredState, outFile);
sourceFile = FileHelpers.NormalizePath(sourceFile);
// TODO: make goalState immutable
// map destination back to the library-relative file it originated from
goalState.InstalledFiles.Add(destinationFile, sourceFile);
}
return goalState;
if (errors is not null)
{
return OperationResult<LibraryInstallationGoalState>.FromErrors([.. errors]);
}
return OperationResult<LibraryInstallationGoalState>.FromSuccess(goalState);
}
public bool IsSourceCacheReady(LibraryInstallationGoalState goalState)

Просмотреть файл

@ -100,8 +100,7 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.Cdnjs
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
Assert.IsFalse(result.Success);
// Unknown exception. We no longer validate ILibraryState at the provider level
Assert.AreEqual("LIB000", result.Errors[0].Code);
Assert.AreEqual("LIB021", result.Errors[0].Code);
}
[TestMethod]
@ -148,11 +147,16 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.Cdnjs
Files = new[] { "*.js", "!*.min.js" },
};
// Verify expansion of Files
OperationResult<LibraryInstallationGoalState> getGoalState = await _provider.GetInstallationGoalStateAsync(desiredState, CancellationToken.None);
Assert.IsTrue(getGoalState.Success);
LibraryInstallationGoalState goalState = getGoalState.Result;
Assert.AreEqual(1, goalState.InstalledFiles.Count);
Assert.AreEqual("jquery.js", Path.GetFileName(goalState.InstalledFiles.Keys.First()));
// Install library
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
Assert.IsTrue(result.Success);
Assert.IsTrue(result.InstallationState.Files.Count == 1); // jquery.min.js file was excluded
Assert.AreEqual("jquery.js", result.InstallationState.Files.First());
}
[TestMethod]

Просмотреть файл

@ -100,8 +100,7 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.JsDelivr
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
Assert.IsFalse(result.Success);
// Unknown exception. We no longer validate ILibraryState at the provider level
Assert.AreEqual("LIB000", result.Errors[0].Code);
Assert.AreEqual("LIB021", result.Errors[0].Code);
}
[TestMethod]
@ -148,10 +147,17 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.JsDelivr
Files = new[] { "dist/*.js", "!dist/*min*" },
};
// Verify expansion of Files
OperationResult<LibraryInstallationGoalState> getGoalState = await _provider.GetInstallationGoalStateAsync(desiredState, CancellationToken.None);
Assert.IsTrue(getGoalState.Success);
LibraryInstallationGoalState goalState = getGoalState.Result;
// Remove the project folder and "/lib/" from the file paths
List<string> installedFiles = goalState.InstalledFiles.Keys.Select(f => f.Substring(_projectFolder.Length + 5).Replace("\\", "/")).ToList();
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, installedFiles);
// Install library
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
Assert.IsTrue(result.Success);
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, result.InstallationState.Files.ToList());
}
[TestMethod]

Просмотреть файл

@ -99,8 +99,7 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.Unpkg
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
Assert.IsFalse(result.Success);
// Unknown exception. We no longer validate ILibraryState at the provider level
Assert.AreEqual("LIB000", result.Errors[0].Code);
Assert.AreEqual("LIB021", result.Errors[0].Code);
}
[TestMethod]
@ -147,10 +146,17 @@ namespace Microsoft.Web.LibraryManager.Test.Providers.Unpkg
Files = new[] { "dist/*.js", "!dist/*min*" },
};
// Verify expansion of Files
OperationResult<LibraryInstallationGoalState> getGoalState = await _provider.GetInstallationGoalStateAsync(desiredState, CancellationToken.None);
Assert.IsTrue(getGoalState.Success);
LibraryInstallationGoalState goalState = getGoalState.Result;
// Remove the project folder and "/lib/" from the file paths
List<string> installedFiles = goalState.InstalledFiles.Keys.Select(f => f.Substring(_projectFolder.Length + 5).Replace("\\", "/")).ToList();
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, installedFiles);
// Install library
ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false);
Assert.IsTrue(result.Success);
CollectionAssert.AreEquivalent(new[] { "dist/core.js", "dist/jquery.js", "dist/jquery.slim.js" }, result.InstallationState.Files.ToList());
}
[TestMethod]