From 135723834bf41643529c91e9ea89edcca1278815 Mon Sep 17 00:00:00 2001 From: Jimmy Lewis Date: Sat, 29 Mar 2025 12:53:24 -0700 Subject: [PATCH 01/11] Replace ILibraryOperationResult with OperationResult --- src/LibraryManager.Build/RestoreTask.cs | 19 +- .../ILibraryOperationResult.cs | 44 ---- src/LibraryManager.Contracts/IProvider.cs | 13 +- .../InvalidLibraryException.cs | 2 +- src/LibraryManager.Vsix/Contracts/Logger.cs | 14 +- .../ErrorList/ErrorListPropagator.cs | 10 +- .../Json/TextviewCreationListener.cs | 6 +- .../Shared/LibraryCommandService.cs | 32 ++- src/LibraryManager.Vsix/Shared/Telemetry.cs | 26 +-- .../UI/Models/InstallDialogViewModel.cs | 2 +- src/LibraryManager/Helpers/Extensions.cs | 30 +-- src/LibraryManager/LibrariesValidator.cs | 94 ++++----- src/LibraryManager/LibraryOperationResult.cs | 99 --------- .../Logging/LogMessageGenerator.cs | 14 +- src/LibraryManager/Manifest.cs | 92 ++++----- src/LibraryManager/Providers/BaseProvider.cs | 188 +++--------------- .../FileSystem/FileSystemProvider.cs | 36 ++-- src/libman/Commands/BaseCommand.cs | 2 +- src/libman/Commands/CleanCommand.cs | 4 +- src/libman/Commands/InstallCommand.cs | 2 +- src/libman/Commands/RestoreCommand.cs | 4 +- src/libman/Commands/UninstallCommand.cs | 2 +- src/libman/Commands/UpdateCommand.cs | 15 +- src/libman/ManifestRestorer.cs | 12 +- .../LibraryOperationResult.cs | 101 ---------- test/LibraryManager.Mocks/Provider.cs | 6 +- .../LibrariesValidatorTest.cs | 16 +- .../LibraryInstallationResultTest.cs | 67 ------- .../LibraryInstallationStateTest.cs | 20 +- test/LibraryManager.Test/ManifestTest.cs | 53 ++--- .../Providers/Cdnjs/CdnjsProviderTest.cs | 12 +- .../FileSystem/FileSystemProviderTest.cs | 22 +- .../JsDelivr/JsDelivrProviderTest.cs | 12 +- .../Providers/Unpkg/UnpkgProviderTest.cs | 12 +- .../Shared/LibraryCommandServiceTest.cs | 4 +- 35 files changed, 295 insertions(+), 792 deletions(-) delete mode 100644 src/LibraryManager.Contracts/ILibraryOperationResult.cs delete mode 100644 src/LibraryManager/LibraryOperationResult.cs delete mode 100644 test/LibraryManager.Mocks/LibraryOperationResult.cs delete mode 100644 test/LibraryManager.Test/LibraryInstallationResultTest.cs diff --git a/src/LibraryManager.Build/RestoreTask.cs b/src/LibraryManager.Build/RestoreTask.cs index ebe461fd..d7af6147 100644 --- a/src/LibraryManager.Build/RestoreTask.cs +++ b/src/LibraryManager.Build/RestoreTask.cs @@ -69,7 +69,7 @@ public override bool Execute() return false; } - IEnumerable validationResults = manifest.GetValidationResultsAsync(token).Result; + IEnumerable> validationResults = manifest.GetValidationResultsAsync(token).Result; if (!validationResults.All(r => r.Success)) { sw.Stop(); @@ -78,7 +78,7 @@ public override bool Execute() return false; } - IEnumerable results = manifest.RestoreAsync(token).Result; + IList> results = manifest.RestoreAsync(token).Result; sw.Stop(); FlushLogger(logger); @@ -103,7 +103,7 @@ private void FlushLogger(Logger logger) } } - private void LogResults(Stopwatch sw, IEnumerable results) + private void LogResults(Stopwatch sw, IEnumerable> results) { bool hasErrors = results.Any(r => !r.Success); @@ -136,19 +136,14 @@ private void LogErrors(IEnumerable errors) Log.LogMessage(MessageImportance.High, Environment.NewLine + text + Environment.NewLine); } - private void PopulateFilesWritten(IEnumerable results, Dependencies dependencies) + private void PopulateFilesWritten(IEnumerable> results, Dependencies dependencies) { - IEnumerable states = results.Where(r => r.Success).Select(r => r.InstallationState); + IEnumerable goalStates = results.Where(r => r.Success).Select(r => r.Result); var list = new List(); - foreach (ILibraryInstallationState state in states) + foreach (LibraryInstallationGoalState goalState in goalStates) { - IProvider provider = dependencies.GetProvider(state.ProviderId); - OperationResult goalStateResult = provider.GetInstallationGoalStateAsync(state, CancellationToken.None).Result; - if (goalStateResult.Success) - { - list.AddRange(goalStateResult.Result.InstalledFiles.Select(f => new TaskItem(f.Key))); - } + list.AddRange(goalState.InstalledFiles.Select(f => new TaskItem(f.Key))); } FilesWritten = list.ToArray(); diff --git a/src/LibraryManager.Contracts/ILibraryOperationResult.cs b/src/LibraryManager.Contracts/ILibraryOperationResult.cs deleted file mode 100644 index 74e43196..00000000 --- a/src/LibraryManager.Contracts/ILibraryOperationResult.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; - -namespace Microsoft.Web.LibraryManager.Contracts -{ - /// - /// Represents the result of . - /// - public interface ILibraryOperationResult - { - /// - /// True if the installation was cancelled; otherwise false; - /// - bool Cancelled { get; } - - /// - /// True if the install was successfull; otherwise False. - /// - /// - /// The value is usually True if the list is empty. - /// - bool Success { get; } - - /// - /// True if the library is up to date; otherwise False. - /// - /// - /// - bool UpToDate { get; } - - /// - /// A list of errors that occured during library installation. - /// - IList Errors { get; } - - /// - /// The object passed to the - /// for installation. - /// - ILibraryInstallationState InstallationState { get; } - } -} diff --git a/src/LibraryManager.Contracts/IProvider.cs b/src/LibraryManager.Contracts/IProvider.cs index 52ec3bc5..976a742d 100644 --- a/src/LibraryManager.Contracts/IProvider.cs +++ b/src/LibraryManager.Contracts/IProvider.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Threading; using System.Threading.Tasks; @@ -44,16 +45,8 @@ public interface IProvider /// /// The details about the library to install. /// A token that allows for the operation to be cancelled. - /// The from the installation process. - Task InstallAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken); - - /// - /// Updates library state using catalog if needed - /// - /// - /// - /// - Task UpdateStateAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken); + /// The from the installation process. + Task> InstallAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken); /// /// Gets the for the . May be null if no catalog is supported. diff --git a/src/LibraryManager.Contracts/InvalidLibraryException.cs b/src/LibraryManager.Contracts/InvalidLibraryException.cs index f877eeec..82d67590 100644 --- a/src/LibraryManager.Contracts/InvalidLibraryException.cs +++ b/src/LibraryManager.Contracts/InvalidLibraryException.cs @@ -15,7 +15,7 @@ namespace Microsoft.Web.LibraryManager.Contracts /// For instance, if a with an id that isn't /// recognized by the is being passed to , /// this exception could be thrown so it can be handled inside - /// and an added to the collection. + /// and an added to the collection. /// [Serializable] public class InvalidLibraryException : Exception diff --git a/src/LibraryManager.Vsix/Contracts/Logger.cs b/src/LibraryManager.Vsix/Contracts/Logger.cs index b9e9de5d..d7acbd98 100644 --- a/src/LibraryManager.Vsix/Contracts/Logger.cs +++ b/src/LibraryManager.Vsix/Contracts/Logger.cs @@ -77,7 +77,7 @@ public static void LogEventsFooter(OperationType operationType, TimeSpan elapsed /// /// /// - public static void LogEventsSummary(IEnumerable totalResults, OperationType operationType, TimeSpan elapsedTime, bool endOfMessage = true) + public static void LogEventsSummary(IEnumerable> totalResults, OperationType operationType, TimeSpan elapsedTime, bool endOfMessage = true) { LogErrors(totalResults); LogEvent(LogMessageGenerator.GetSummaryHeaderString(operationType), LogLevel.Task); @@ -116,7 +116,7 @@ public static void LogErrorsSummary(IEnumerable errorMessages, Operation /// Operation results /// /// Whether or not to log end of message lines - public static void LogErrorsSummary(IEnumerable results, OperationType operationType, bool endOfMessage = true) + public static void LogErrorsSummary(IEnumerable> results, OperationType operationType, bool endOfMessage = true) { List errorStrings = GetErrorStrings(results); LogErrorsSummary(errorStrings, operationType, endOfMessage); @@ -271,7 +271,7 @@ private static bool EnsurePane() return OutputWindowPaneValue != null; } - private static void LogOperationSummary(IEnumerable totalResults, OperationType operation, TimeSpan elapsedTime) + private static void LogOperationSummary(IEnumerable> totalResults, OperationType operation, TimeSpan elapsedTime) { string messageText = LogMessageGenerator.GetOperationSummaryString(totalResults, operation, elapsedTime); @@ -281,9 +281,9 @@ private static void LogOperationSummary(IEnumerable tot } } - private static void LogErrors(IEnumerable results) + private static void LogErrors(IEnumerable> results) { - foreach (ILibraryOperationResult result in results) + foreach (OperationResult result in results) { foreach (IError error in result.Errors) { @@ -292,11 +292,11 @@ private static void LogErrors(IEnumerable results) } } - private static List GetErrorStrings(IEnumerable results) + private static List GetErrorStrings(IEnumerable> results) { List errorStrings = new List(); - foreach (ILibraryOperationResult result in results) + foreach (OperationResult result in results) { foreach (IError error in result.Errors) { diff --git a/src/LibraryManager.Vsix/ErrorList/ErrorListPropagator.cs b/src/LibraryManager.Vsix/ErrorList/ErrorListPropagator.cs index f93f0010..34a99ad3 100644 --- a/src/LibraryManager.Vsix/ErrorList/ErrorListPropagator.cs +++ b/src/LibraryManager.Vsix/ErrorList/ErrorListPropagator.cs @@ -23,16 +23,16 @@ public ErrorListPropagator(string projectName, string configFileName) public string ConfigFileName { get; set; } public List Errors { get; } - public bool HandleErrors(IEnumerable results) + public bool HandleErrors(IEnumerable> results) { string[] jsonLines = File.Exists(ConfigFileName) ? File.ReadLines(ConfigFileName).ToArray() : Array.Empty(); - foreach (ILibraryOperationResult result in results) + foreach (OperationResult goalStateResult in results) { - if (!result.Success) + if (!goalStateResult.Success) { - DisplayError[] displayErrors = result.Errors.Select(error => new DisplayError(error)).ToArray(); - AddLineAndColumn(jsonLines, result.InstallationState, displayErrors); + DisplayError[] displayErrors = goalStateResult.Errors.Select(error => new DisplayError(error)).ToArray(); + AddLineAndColumn(jsonLines, goalStateResult.Result?.InstallationState, displayErrors); Errors.AddRange(displayErrors); } diff --git a/src/LibraryManager.Vsix/Json/TextviewCreationListener.cs b/src/LibraryManager.Vsix/Json/TextviewCreationListener.cs index f1833185..8c402c4e 100644 --- a/src/LibraryManager.Vsix/Json/TextviewCreationListener.cs +++ b/src/LibraryManager.Vsix/Json/TextviewCreationListener.cs @@ -80,7 +80,7 @@ public void VsTextViewCreated(IVsTextView textViewAdapter) _ = Task.Run(async () => { - IEnumerable results = await LibrariesValidator.GetManifestErrorsAsync(_manifest, dependencies, CancellationToken.None).ConfigureAwait(false); + IEnumerable> results = await LibrariesValidator.GetManifestErrorsAsync(_manifest, dependencies, CancellationToken.None).ConfigureAwait(false); if (!results.All(r => r.Success)) { AddErrorsToList(results); @@ -108,7 +108,7 @@ async Task DoRestoreOnSaveAsync() { IDependencies dependencies = DependenciesFactory.FromConfigFile(textDocument.FilePath); var newManifest = Manifest.FromJson(textDocument.TextBuffer.CurrentSnapshot.GetText(), dependencies); - IEnumerable results = await LibrariesValidator.GetManifestErrorsAsync(newManifest, dependencies, CancellationToken.None).ConfigureAwait(false); + IEnumerable> results = await LibrariesValidator.GetManifestErrorsAsync(newManifest, dependencies, CancellationToken.None).ConfigureAwait(false); if (!results.All(r => r.Success)) { @@ -170,7 +170,7 @@ private void OnViewClosed(object sender, EventArgs e) _errorList?.ClearErrors(); } - private void AddErrorsToList(IEnumerable errors) + private void AddErrorsToList(IEnumerable> errors) { _errorList = new ErrorListPropagator(_project?.Name, _manifestPath); _errorList.HandleErrors(errors); diff --git a/src/LibraryManager.Vsix/Shared/LibraryCommandService.cs b/src/LibraryManager.Vsix/Shared/LibraryCommandService.cs index 40e3710b..f91e8f67 100644 --- a/src/LibraryManager.Vsix/Shared/LibraryCommandService.cs +++ b/src/LibraryManager.Vsix/Shared/LibraryCommandService.cs @@ -182,11 +182,11 @@ private async Task CleanLibrariesAsync(ProjectItem configProjectItem, Cancellati Project project = VsHelpers.GetDTEProjectFromConfig(configFileName); Manifest manifest = await Manifest.FromFileAsync(configFileName, dependencies, CancellationToken.None).ConfigureAwait(false); - IEnumerable results = new List(); + IEnumerable> results = new List>(); if (manifest != null) { - IEnumerable validationResults = await LibrariesValidator.GetManifestErrorsAsync(manifest, dependencies, cancellationToken).ConfigureAwait(false); + IEnumerable> validationResults = await LibrariesValidator.GetManifestErrorsAsync(manifest, dependencies, cancellationToken).ConfigureAwait(false); if (!validationResults.All(r => r.Success)) { @@ -234,7 +234,7 @@ private async Task RestoreInternalAsync(IDictionary manifests, Logger.LogEvent(string.Format(LibraryManager.Resources.Text.Restore_LibrariesForProject, project?.Name), LogLevel.Operation); - IEnumerable validationResults = await LibrariesValidator.GetManifestErrorsAsync(manifest.Value, dependencies, cancellationToken).ConfigureAwait(false); + IEnumerable> validationResults = await LibrariesValidator.GetManifestErrorsAsync(manifest.Value, dependencies, cancellationToken).ConfigureAwait(false); if (!validationResults.All(r => r.Success)) { swLocal.Stop(); @@ -244,7 +244,7 @@ private async Task RestoreInternalAsync(IDictionary manifests, } else { - IEnumerable results = await RestoreLibrariesAsync(manifest.Value, cancellationToken).ConfigureAwait(false); + var results = await RestoreLibrariesAsync(manifest.Value, cancellationToken).ConfigureAwait(false); await AddFilesToProjectAsync(manifest.Key, project, results.Where(r =>r.Success && !r.UpToDate), cancellationToken).ConfigureAwait(false); swLocal.Stop(); @@ -264,7 +264,7 @@ private async Task RestoreInternalAsync(IDictionary manifests, } } - private async Task> RestoreLibrariesAsync(Manifest manifest, CancellationToken cancellationToken) + private async Task>> RestoreLibrariesAsync(Manifest manifest, CancellationToken cancellationToken) { return await manifest.RestoreAsync(cancellationToken).ConfigureAwait(false); } @@ -281,11 +281,11 @@ private async Task UninstallLibraryAsync(string configFilePath, string libraryNa var dependencies = _dependenciesFactory.FromConfigFile(configFilePath); Manifest manifest = await Manifest.FromFileAsync(configFilePath, dependencies, cancellationToken).ConfigureAwait(false); - ILibraryOperationResult result = null; + OperationResult result = null; if (manifest == null) { - result = LibraryOperationResult.FromError(PredefinedErrors.ManifestMalformed()); + result = OperationResult.FromError(PredefinedErrors.ManifestMalformed()); } else { @@ -297,14 +297,14 @@ private async Task UninstallLibraryAsync(string configFilePath, string libraryNa if (result.Errors.Any()) { - Logger.LogErrorsSummary(new List { result }, OperationType.Uninstall); + Logger.LogErrorsSummary(new List> { result }, OperationType.Uninstall); } else { - Logger.LogEventsSummary(new List { result }, OperationType.Uninstall, sw.Elapsed); + Logger.LogEventsSummary(new List> { result }, OperationType.Uninstall, sw.Elapsed); } - Telemetry.LogEventsSummary(new List { result }, OperationType.Uninstall, sw.Elapsed); + Telemetry.LogEventsSummary(new List> { result }, OperationType.Uninstall, sw.Elapsed); } catch (OperationCanceledException ex) { @@ -333,26 +333,24 @@ private string GetTaskTitle(OperationType operation, string libraryId) return string.Empty; } - private void AddErrorsToErrorList(string projectName, string configFile, IEnumerable results) + private void AddErrorsToErrorList(string projectName, string configFile, IEnumerable> results) { var errorList = new ErrorListPropagator(projectName, configFile); errorList.HandleErrors(results); } - private async Task AddFilesToProjectAsync(string configFilePath, Project project, IEnumerable results, CancellationToken cancellationToken) + private async Task AddFilesToProjectAsync(string configFilePath, Project project, IEnumerable> results, CancellationToken cancellationToken) { string workingDirectory = Path.GetDirectoryName(configFilePath); var files = new List(); if (project != null) { - foreach (ILibraryOperationResult state in results) + foreach (OperationResult goalStateResult in results) { - if (state.Success && !state.UpToDate && state.InstallationState.Files != null) + if (goalStateResult.Success && !goalStateResult.UpToDate && goalStateResult.Result.InstallationState.Files != null) { - IEnumerable absoluteFiles = state.InstallationState.Files - .Select(file => Path.Combine(workingDirectory, state.InstallationState.DestinationPath, file) - .Replace('/', Path.DirectorySeparatorChar)); + IEnumerable absoluteFiles = goalStateResult.Result.InstalledFiles.Keys; files.AddRange(absoluteFiles); } } diff --git a/src/LibraryManager.Vsix/Shared/Telemetry.cs b/src/LibraryManager.Vsix/Shared/Telemetry.cs index de2bd4da..da0c12e3 100644 --- a/src/LibraryManager.Vsix/Shared/Telemetry.cs +++ b/src/LibraryManager.Vsix/Shared/Telemetry.cs @@ -49,13 +49,13 @@ public static void TrackException(string name, Exception exception) TelemetryService.DefaultSession.PostFault(Namespace + actualName, exception.Message, exception); } - internal static void LogEventsSummary(IEnumerable results, OperationType operation, TimeSpan elapsedTime) + internal static void LogEventsSummary(IEnumerable> results, OperationType operation, TimeSpan elapsedTime) { Dictionary telResult = new Dictionary(); double elapsedTimeRounded = Math.Round(elapsedTime.TotalSeconds, 2); string elapsedTimeStr = elapsedTimeRounded.ToString(System.Globalization.CultureInfo.InvariantCulture); - List generalErrorCodes = GetErrorCodes(results.Where(r => r.InstallationState == null && r.Errors.Any())); - IEnumerable providers = results.Select(r => r.InstallationState?.ProviderId).Distinct(StringComparer.OrdinalIgnoreCase); + List generalErrorCodes = GetErrorCodes(results.Where(r => r.Result?.InstallationState == null && r.Errors.Any())); + IEnumerable providers = results.Select(r => r.Result?.InstallationState?.ProviderId).Distinct(StringComparer.OrdinalIgnoreCase); telResult.Add("LibrariesCount", results.Count()); telResult.Add($"{operation}_time", elapsedTimeStr); @@ -67,15 +67,15 @@ internal static void LogEventsSummary(IEnumerable resul foreach (string provider in providers) { - List successfulProviderResults = new List(); - List failedProviderResults = new List(); - List cancelledProviderResults = new List(); - List uptodateProviderResults = new List(); + List> successfulProviderResults = new(); + List> failedProviderResults = new(); + List> cancelledProviderResults = new(); + List> uptodateProviderResults = new(); - foreach (ILibraryOperationResult result in results) + foreach (OperationResult result in results) { - if (result.InstallationState != null && - result.InstallationState.ProviderId.Equals(provider, StringComparison.OrdinalIgnoreCase)) + if (result.Result?.InstallationState != null && + result.Result.InstallationState.ProviderId.Equals(provider, StringComparison.OrdinalIgnoreCase)) { if (result.Success && !result.UpToDate) { @@ -123,17 +123,17 @@ internal static void LogEventsSummary(IEnumerable resul TrackUserTask($@"{operation}_Operation", TelemetryResult.None, telResult.Select(i => new KeyValuePair(i.Key, i.Value)).ToArray()); } - internal static void LogErrors(string eventName, IEnumerable results) + internal static void LogErrors(string eventName, IEnumerable> results) { List errorCodes = GetErrorCodes(results.Where(r => r.Errors.Any())); TrackUserTask(eventName, TelemetryResult.Failure, new KeyValuePair("Errorcode", string.Join(":", errorCodes))); } - private static List GetErrorCodes(IEnumerable results) + private static List GetErrorCodes(IEnumerable> results) { List errorCodes = new List(); - foreach (ILibraryOperationResult result in results) + foreach (OperationResult result in results) { errorCodes.AddRange(result.Errors.Select(e => e.Code)); } diff --git a/src/LibraryManager.Vsix/UI/Models/InstallDialogViewModel.cs b/src/LibraryManager.Vsix/UI/Models/InstallDialogViewModel.cs index a70974ab..64b56af1 100644 --- a/src/LibraryManager.Vsix/UI/Models/InstallDialogViewModel.cs +++ b/src/LibraryManager.Vsix/UI/Models/InstallDialogViewModel.cs @@ -419,7 +419,7 @@ public async Task IsLibraryInstallationStateValidAsync() Files = SelectedFiles?.ToList() }; - ILibraryOperationResult libraryOperationResult = await libraryInstallationState.IsValidAsync(SelectedProvider).ConfigureAwait(false); + OperationResult libraryOperationResult = await libraryInstallationState.IsValidAsync(SelectedProvider).ConfigureAwait(false); IList errors = libraryOperationResult.Errors; ErrorMessage = string.Empty; diff --git a/src/LibraryManager/Helpers/Extensions.cs b/src/LibraryManager/Helpers/Extensions.cs index 53d8faf1..96b74396 100644 --- a/src/LibraryManager/Helpers/Extensions.cs +++ b/src/LibraryManager/Helpers/Extensions.cs @@ -23,23 +23,23 @@ public static class Extensions /// /// The to validate. /// The used to validate - /// with the result of the validation - public static async Task IsValidAsync(this ILibraryInstallationState state, IDependencies dependencies) + /// with the result of the validation + public static async Task> IsValidAsync(this ILibraryInstallationState state, IDependencies dependencies) { if (state == null) { - return new LibraryOperationResult(state, new[] { PredefinedErrors.UnknownError() }); + return OperationResult.FromError(PredefinedErrors.UnknownError()); } if (string.IsNullOrEmpty(state.ProviderId)) { - return new LibraryOperationResult(state, new[] { PredefinedErrors.ProviderIsUndefined() }); + return OperationResult.FromError(PredefinedErrors.ProviderIsUndefined()); } IProvider provider = dependencies?.GetProvider(state.ProviderId); if (provider == null) { - return new LibraryOperationResult(state, new[] { PredefinedErrors.ProviderUnknown(state.ProviderId) }); + return OperationResult.FromError(PredefinedErrors.ProviderUnknown(state.ProviderId)); } return await IsValidAsync(state, provider).ConfigureAwait(false); @@ -50,22 +50,22 @@ public static async Task IsValidAsync(this ILibraryInst /// /// The to validate. /// The used to validate - /// with the result of the validation - public static async Task IsValidAsync(this ILibraryInstallationState state, IProvider provider) + /// with the result of the validation + public static async Task> IsValidAsync(this ILibraryInstallationState state, IProvider provider) { if (state == null) { - return new LibraryOperationResult(state, new[] { PredefinedErrors.UnknownError() }); + return OperationResult.FromError(PredefinedErrors.UnknownError()); } if (provider == null) { - return new LibraryOperationResult(state, new[] { PredefinedErrors.ProviderUnknown(string.Empty) }); + return OperationResult.FromError(PredefinedErrors.ProviderUnknown(string.Empty)); } if (string.IsNullOrEmpty(state.Name)) { - return new LibraryOperationResult(state, new[] { PredefinedErrors.LibraryIdIsUndefined() }); + return OperationResult.FromError(PredefinedErrors.LibraryIdIsUndefined()); } ILibraryCatalog catalog = provider.GetCatalog(); @@ -75,20 +75,22 @@ public static async Task IsValidAsync(this ILibraryInst } catch { - return new LibraryOperationResult(state, new[] { PredefinedErrors.UnableToResolveSource(state.Name, state.Version, provider.Id) }); + return OperationResult.FromError(PredefinedErrors.UnableToResolveSource(state.Name, state.Version, provider.Id)); } if (string.IsNullOrEmpty(state.DestinationPath)) { - return new LibraryOperationResult(state, new[] { PredefinedErrors.PathIsUndefined() }); + return OperationResult.FromError(PredefinedErrors.PathIsUndefined()); } if (state.DestinationPath.IndexOfAny(Path.GetInvalidPathChars()) >= 0) { - return new LibraryOperationResult(state, new[] { PredefinedErrors.DestinationPathHasInvalidCharacters(state.DestinationPath) }); + return OperationResult.FromError(PredefinedErrors.DestinationPathHasInvalidCharacters(state.DestinationPath)); } - return LibraryOperationResult.FromSuccess(state); + OperationResult goalStateResult = await provider.GetInstallationGoalStateAsync(state, CancellationToken.None); + + return goalStateResult; } /// diff --git a/src/LibraryManager/LibrariesValidator.cs b/src/LibraryManager/LibrariesValidator.cs index 8325cd59..79dbe8dc 100644 --- a/src/LibraryManager/LibrariesValidator.cs +++ b/src/LibraryManager/LibrariesValidator.cs @@ -18,7 +18,7 @@ namespace Microsoft.Web.LibraryManager internal static class LibrariesValidator { /// - /// Returns a collection of that represents the status for validation of each + /// Returns a collection of that represents the status for validation of each /// library /// /// Set of libraries to be validated @@ -27,7 +27,7 @@ internal static class LibrariesValidator /// DefaultProvider used to validate the libraries /// /// - public static async Task> GetLibrariesErrorsAsync( + public static async Task>> GetLibrariesErrorsAsync( IEnumerable libraries, IDependencies dependencies, string defaultDestination, @@ -37,41 +37,44 @@ public static async Task> GetLibrariesError cancellationToken.ThrowIfCancellationRequested(); // Check for valid libraries - IEnumerable validateLibraries = await ValidatePropertiesAsync(libraries, dependencies, cancellationToken).ConfigureAwait(false); + IEnumerable> validateLibraries = await ValidatePropertiesAsync(libraries, dependencies, cancellationToken).ConfigureAwait(false); if (!validateLibraries.All(t => t.Success)) { return validateLibraries; } // Check for duplicate libraries - IEnumerable duplicateLibraries = GetDuplicateLibrariesErrors(libraries); + IEnumerable> duplicateLibraries = GetDuplicateLibrariesErrors(libraries); if (!duplicateLibraries.All(t => t.Success)) { return duplicateLibraries; } // Check for files conflicts - IEnumerable expandLibraries = await ExpandLibrariesAsync(libraries, dependencies, defaultDestination, defaultProvider, cancellationToken).ConfigureAwait(false); - if (!expandLibraries.All(t => t.Success)) + IEnumerable> result = await ExpandLibrariesAsync(libraries, dependencies, defaultDestination, defaultProvider, cancellationToken).ConfigureAwait(false); + if (!result.All(t => t.Success)) { - return expandLibraries; + return result; } - libraries = expandLibraries.Select(l => l.InstallationState); - IEnumerable fileConflicts = GetFilesConflicts(libraries); - ILibraryOperationResult conflictErrors = GetConflictErrors(fileConflicts); + IEnumerable goalStates = result.Select(l => l.Result); + IEnumerable fileConflicts = GetFilesConflicts(goalStates); + if (fileConflicts.Any()) + { + result = [GetConflictErrors(fileConflicts)]; + } - return new[] { conflictErrors }; + return result; } /// - /// Returns a collection of that represents the status for validation of the Manifest and its libraries + /// Returns a collection of that represents the status for validation of the Manifest and its libraries /// /// The to be validated /// used to validate the libraries /// /// - public static async Task> GetManifestErrorsAsync( + public static async Task>> GetManifestErrorsAsync( Manifest manifest, IDependencies dependencies, CancellationToken cancellationToken) @@ -80,17 +83,17 @@ public static async Task> GetManifestErrors if (manifest == null) { - return [LibraryOperationResult.FromError(PredefinedErrors.ManifestMalformed())]; + return [OperationResult.FromError(PredefinedErrors.ManifestMalformed())]; } if (string.IsNullOrEmpty(manifest.Version)) { - return [LibraryOperationResult.FromError(PredefinedErrors.MissingManifestVersion())]; + return [OperationResult.FromError(PredefinedErrors.MissingManifestVersion())]; } if (!IsValidManifestVersion(manifest.Version)) { - return [LibraryOperationResult.FromError(PredefinedErrors.VersionIsNotSupported(manifest.Version))]; + return [OperationResult.FromError(PredefinedErrors.VersionIsNotSupported(manifest.Version))]; } return await GetLibrariesErrorsAsync(manifest.Libraries, dependencies, manifest.DefaultDestination, manifest.DefaultProvider, cancellationToken); @@ -114,38 +117,31 @@ private static bool IsValidManifestVersion(string version) /// /// /// - private static async Task> ValidatePropertiesAsync(IEnumerable libraries, IDependencies dependencies, CancellationToken cancellationToken) + private static async Task>> ValidatePropertiesAsync(IEnumerable libraries, IDependencies dependencies, CancellationToken cancellationToken) { - var validationStatus = new List(); + var validationStatus = new List>(); foreach (ILibraryInstallationState library in libraries) { cancellationToken.ThrowIfCancellationRequested(); - ILibraryOperationResult result = await library.IsValidAsync(dependencies).ConfigureAwait(false); - if (!result.Success) - { - validationStatus.Add(result); - } - else - { - validationStatus.Add(LibraryOperationResult.FromSuccess(library)); - } + OperationResult result = await library.IsValidAsync(dependencies).ConfigureAwait(false); + validationStatus.Add(result); } return validationStatus; } - private static IEnumerable GetDuplicateLibrariesErrors(IEnumerable libraries) + private static IEnumerable> GetDuplicateLibrariesErrors(IEnumerable libraries) { - var errors = new List(); + var errors = new List>(); HashSet duplicateLibraries = GetDuplicateLibraries(libraries); if (duplicateLibraries.Count > 0) { foreach (string libraryName in duplicateLibraries) { - errors.Add(LibraryOperationResult.FromError(PredefinedErrors.DuplicateLibrariesInManifest(libraryName))); + errors.Add(OperationResult.FromError(PredefinedErrors.DuplicateLibrariesInManifest(libraryName))); } } @@ -176,14 +172,14 @@ private static HashSet GetDuplicateLibraries(IEnumerable /// /// - private static async Task> ExpandLibrariesAsync( + private static async Task>> ExpandLibrariesAsync( IEnumerable libraries, IDependencies dependencies, string defaultDestination, string defaultProvider, CancellationToken cancellationToken) { - List expandedLibraries = new List(); + List> expandedLibraries = []; foreach (ILibraryInstallationState library in libraries) { @@ -195,13 +191,13 @@ private static async Task> ExpandLibrariesA IProvider provider = dependencies.GetProvider(providerId); if (provider == null) { - return new[] { LibraryOperationResult.FromError(PredefinedErrors.ProviderIsUndefined()) }; + return [OperationResult.FromError(PredefinedErrors.ProviderIsUndefined())]; } - ILibraryOperationResult desiredState = await provider.UpdateStateAsync(library, cancellationToken); + OperationResult desiredState = await provider.GetInstallationGoalStateAsync(library, cancellationToken); if (!desiredState.Success) { - return new[] { desiredState }; + return [desiredState]; } expandedLibraries.Add(desiredState); @@ -213,32 +209,30 @@ private static async Task> ExpandLibrariesA /// /// Detects files conflicts in between libraries in the given collection /// - /// + /// /// A collection of for each library conflict - private static IEnumerable GetFilesConflicts(IEnumerable libraries) + private static IEnumerable GetFilesConflicts(IEnumerable goalStates) { - Dictionary> _fileToLibraryMap = new Dictionary>(RelativePathEqualityComparer.Instance); + Dictionary> fileToLibraryMap = new(RelativePathEqualityComparer.Instance); - foreach (ILibraryInstallationState library in libraries) + foreach (LibraryInstallationGoalState goalState in goalStates) { - string destinationPath = library.DestinationPath; - - IEnumerable files = library.Files.Select(f => Path.Combine(destinationPath, f)); + IEnumerable files = goalState.InstalledFiles.Keys; foreach (string file in files) { - if (!_fileToLibraryMap.ContainsKey(file)) + if (!fileToLibraryMap.ContainsKey(file)) { - _fileToLibraryMap[file] = new List(); + fileToLibraryMap[file] = new List(); } - _fileToLibraryMap[file].Add(library); + fileToLibraryMap[file].Add(goalState); } } - return _fileToLibraryMap + return fileToLibraryMap .Where(f => f.Value.Count > 1) - .Select(f => new FileConflict(f.Key, f.Value)); + .Select(f => new FileConflict(f.Key, f.Value.Select(gs => gs.InstallationState).ToList())); } @@ -247,7 +241,7 @@ private static IEnumerable GetFilesConflicts(IEnumerable /// /// - private static ILibraryOperationResult GetConflictErrors(IEnumerable fileConflicts) + private static OperationResult GetConflictErrors(IEnumerable fileConflicts) { if (fileConflicts.Any()) { @@ -257,10 +251,10 @@ private static ILibraryOperationResult GetConflictErrors(IEnumerable l.Name).ToList())); } - return new LibraryOperationResult(errors.ToArray()); + return OperationResult.FromErrors([..errors]); } - return LibraryOperationResult.FromSuccess(null); + return OperationResult.FromSuccess(null); } } } diff --git a/src/LibraryManager/LibraryOperationResult.cs b/src/LibraryManager/LibraryOperationResult.cs deleted file mode 100644 index 6efa81c0..00000000 --- a/src/LibraryManager/LibraryOperationResult.cs +++ /dev/null @@ -1,99 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable enable - -using System; -using System.Collections.Generic; -using Microsoft.Web.LibraryManager.Contracts; - -namespace Microsoft.Web.LibraryManager -{ - /// Internal use only - internal class LibraryOperationResult : ILibraryOperationResult - { - /// Internal use only - public LibraryOperationResult(ILibraryInstallationState installationState) - { - Errors = new List(); - InstallationState = installationState; - } - - /// Internal use only - public LibraryOperationResult(ILibraryInstallationState installationState, params IError[] error) - { - var list = new List(); - list.AddRange(error); - Errors = list; - InstallationState = installationState; - } - - /// Internal use only - public LibraryOperationResult(params IError[] error) - { - Errors = new List(error); - } - - /// - /// True if the installation was cancelled; otherwise false; - /// - public bool Cancelled { get; set; } - - /// - /// True if the library is up to date; otherwise false; - /// - public bool UpToDate { get; set; } - - /// - /// True if the install was successful; otherwise False. - /// - /// - /// The value is usually True if the list is empty. - /// - public bool Success - { - get { return !Cancelled && Errors.Count == 0; } - } - - /// - /// A list of errors that occured during library installation. - /// - public IList Errors { get; set; } - - /// - /// The object passed to the - /// for installation. - /// - public ILibraryInstallationState? InstallationState { get; set; } - - /// Internal use only - public static LibraryOperationResult FromSuccess(ILibraryInstallationState installationState) - { - return new LibraryOperationResult(installationState); - } - - /// Internal use only - public static LibraryOperationResult FromCancelled(ILibraryInstallationState installationState) - { - return new LibraryOperationResult(installationState) - { - Cancelled = true - }; - } - - /// Internal use only - public static LibraryOperationResult FromError(IError error) - { - return new LibraryOperationResult(error); - } - - /// Internal use only - public static ILibraryOperationResult FromUpToDate(ILibraryInstallationState installationState) - { - return new LibraryOperationResult(installationState) - { - UpToDate = true - }; - } - } -} diff --git a/src/LibraryManager/Logging/LogMessageGenerator.cs b/src/LibraryManager/Logging/LogMessageGenerator.cs index b298b603..c1547300 100644 --- a/src/LibraryManager/Logging/LogMessageGenerator.cs +++ b/src/LibraryManager/Logging/LogMessageGenerator.cs @@ -195,15 +195,15 @@ public static string GetErrorsHeaderString(OperationType operationType) /// /// Gets the operation summary string based on number of successful and failure operations. /// - public static string GetOperationSummaryString(IEnumerable results, OperationType operation, TimeSpan elapsedTime) + public static string GetOperationSummaryString(IEnumerable> results, OperationType operation, TimeSpan elapsedTime) { if (results != null && results.Any()) { int totalResultsCounts = results.Count(); - IEnumerable successfulResults = results.Where(r => r.Success && !r.UpToDate); - IEnumerable failedResults = results.Where(r => r.Errors.Any()); - IEnumerable cancelledRessults = results.Where(r => r.Cancelled); - IEnumerable upToDateResults = results.Where(r => r.UpToDate); + IEnumerable> successfulResults = results.Where(r => r.Success && !r.UpToDate); + IEnumerable> failedResults = results.Where(r => r.Errors.Any()); + IEnumerable> cancelledRessults = results.Where(r => r.Cancelled); + IEnumerable> upToDateResults = results.Where(r => r.UpToDate); bool allSuccess = successfulResults.Count() == totalResultsCounts; bool allFailed = failedResults.Count() == totalResultsCounts; @@ -244,13 +244,13 @@ public static string GetOperationSummaryString(IEnumerable totalResults, OperationType operation) + private static string GetLibraryId(IEnumerable> totalResults, OperationType operation) { if (operation == OperationType.Uninstall || operation == OperationType.Upgrade) { if (totalResults != null && totalResults.Count() == 1) { - ILibraryInstallationState state = totalResults.First().InstallationState; + ILibraryInstallationState state = totalResults.First().Result.InstallationState; return LibraryIdToNameAndVersionConverter.Instance.GetLibraryId(state.Name, state.Version, state.ProviderId); } } diff --git a/src/LibraryManager/Manifest.cs b/src/LibraryManager/Manifest.cs index 22db3f8b..6a08006b 100644 --- a/src/LibraryManager/Manifest.cs +++ b/src/LibraryManager/Manifest.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -196,7 +195,7 @@ public Manifest Clone() /// /// /// - public async Task InstallLibraryAsync( + public async Task> InstallLibraryAsync( string libraryName, string version, string providerId, @@ -215,7 +214,7 @@ public async Task InstallLibraryAsync( UpdateLibraryProviderAndDestination(desiredState, DefaultProvider, DefaultDestination); - ILibraryOperationResult validationResult = await desiredState.IsValidAsync(_dependencies); + OperationResult validationResult = await desiredState.IsValidAsync(_dependencies); if (!validationResult.Success) { return validationResult; @@ -224,17 +223,16 @@ public async Task InstallLibraryAsync( IProvider provider = _dependencies.GetProvider(desiredState.ProviderId); if (provider == null) { - return new LibraryOperationResult(desiredState, new IError[] { PredefinedErrors.ProviderUnknown(desiredState.ProviderId) }); + return new OperationResult(new IError[] { PredefinedErrors.ProviderUnknown(desiredState.ProviderId) }); } - ILibraryOperationResult conflictResults = await CheckLibraryForConflictsAsync(desiredState, cancellationToken).ConfigureAwait(false); - + OperationResult conflictResults = await CheckLibraryForConflictsAsync(validationResult.Result, cancellationToken).ConfigureAwait(false); if (!conflictResults.Success) { return conflictResults; } - ILibraryOperationResult installResult = await provider.InstallAsync(desiredState, cancellationToken); + OperationResult installResult = await provider.InstallAsync(desiredState, cancellationToken); if (installResult.Success) { @@ -259,15 +257,15 @@ private ILibraryInstallationState SetDefaultProviderIfNeeded(LibraryInstallation return desiredState; } - private async Task CheckLibraryForConflictsAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken) + private async Task> CheckLibraryForConflictsAsync(LibraryInstallationGoalState desiredGoalState, CancellationToken cancellationToken) { var libraries = new List(Libraries); - libraries.Add(desiredState); + libraries.Add(desiredGoalState.InstallationState); - IEnumerable fileConflicts = await LibrariesValidator.GetLibrariesErrorsAsync(libraries, _dependencies, DefaultDestination, DefaultProvider, cancellationToken).ConfigureAwait(false); + IEnumerable> fileConflicts = await LibrariesValidator.GetLibrariesErrorsAsync(libraries, _dependencies, DefaultDestination, DefaultProvider, cancellationToken).ConfigureAwait(false); // consolidate all the errors in one result - return new LibraryOperationResult(desiredState, fileConflicts.SelectMany(r => r.Errors).ToArray()); + return new OperationResult(desiredGoalState, fileConflicts.SelectMany(r => r.Errors).ToArray()); } /// @@ -307,10 +305,10 @@ public void AddVersion(string version) /// Restores all libraries in the collection. /// /// A token that allows for cancellation of the operation. - public async Task> RestoreAsync(CancellationToken cancellationToken) + public async Task>> RestoreAsync(CancellationToken cancellationToken) { //TODO: This should have an "undo scope" - var results = new List(); + var results = new List>(); foreach (ILibraryInstallationState state in Libraries) { @@ -321,30 +319,30 @@ public async Task> RestoreAsync(CancellationToken } /// - /// Returns a collection of that represents the status for validation of the Manifest and its libraries + /// Returns a collection of that represents the status for validation of the Manifest and its libraries /// /// A token that allows for cancellation of the operation. - public async Task> GetValidationResultsAsync(CancellationToken cancellationToken) + public async Task>> GetValidationResultsAsync(CancellationToken cancellationToken) { - IEnumerable validationResults = await LibrariesValidator.GetManifestErrorsAsync(this, _dependencies, cancellationToken).ConfigureAwait(false); + IEnumerable> validationResults = await LibrariesValidator.GetManifestErrorsAsync(this, _dependencies, cancellationToken).ConfigureAwait(false); return validationResults; } - private async Task RestoreLibraryAsync(ILibraryInstallationState libraryState, CancellationToken cancellationToken) + private async Task> RestoreLibraryAsync(ILibraryInstallationState libraryState, CancellationToken cancellationToken) { string libraryId = LibraryIdToNameAndVersionConverter.Instance.GetLibraryId(libraryState.Name, libraryState.Version, libraryState.ProviderId); _hostInteraction.Logger.Log(string.Format(Resources.Text.Restore_RestoreOfLibraryStarted, libraryId, libraryState.DestinationPath), LogLevel.Operation); if (cancellationToken.IsCancellationRequested) { - return LibraryOperationResult.FromCancelled(libraryState); + return OperationResult.FromCancelled(null); } IProvider provider = _dependencies.GetProvider(libraryState.ProviderId); if (provider == null) { - return new LibraryOperationResult(libraryState, new IError[] { PredefinedErrors.ProviderUnknown(libraryState.ProviderId) }); + return OperationResult.FromError(PredefinedErrors.ProviderUnknown(libraryState.ProviderId)); } try @@ -353,7 +351,7 @@ private async Task RestoreLibraryAsync(ILibraryInstalla } catch (OperationCanceledException) { - return LibraryOperationResult.FromCancelled(libraryState); + return OperationResult.FromCancelled(null); } } @@ -364,18 +362,18 @@ private async Task RestoreLibraryAsync(ILibraryInstalla /// Version of the library to uninstall. /// /// - public async Task UninstallAsync(string libraryName, string version, Func, Task> deleteFilesFunction, CancellationToken cancellationToken) + public async Task> UninstallAsync(string libraryName, string version, Func, Task> deleteFilesFunction, CancellationToken cancellationToken) { ILibraryInstallationState library = Libraries.FirstOrDefault(l => l.Name == libraryName && l.Version == version); if (cancellationToken.IsCancellationRequested) { - return LibraryOperationResult.FromCancelled(library); + return OperationResult.FromCancelled(null); } if (library != null) { - ILibraryOperationResult validationResult = await library.IsValidAsync(_dependencies); + OperationResult validationResult = await library.IsValidAsync(_dependencies); if (!validationResult.Success) { return validationResult; @@ -383,7 +381,7 @@ public async Task UninstallAsync(string libraryName, st try { - ILibraryOperationResult result = await DeleteLibraryFilesAsync(library, deleteFilesFunction, cancellationToken).ConfigureAwait(false); + OperationResult result = await DeleteLibraryFilesAsync(library, deleteFilesFunction, cancellationToken).ConfigureAwait(false); if (result.Success) { @@ -394,11 +392,11 @@ public async Task UninstallAsync(string libraryName, st } catch (OperationCanceledException) { - return LibraryOperationResult.FromCancelled(library); + return OperationResult.FromCancelled(validationResult.Result); } } - return LibraryOperationResult.FromError(PredefinedErrors.CouldNotDeleteLibrary(libraryName)); + return OperationResult.FromError(PredefinedErrors.CouldNotDeleteLibrary(libraryName)); } /// @@ -454,9 +452,9 @@ public async Task SaveAsync(string fileName, CancellationToken cancellationToken /// >An action to delete the files. /// /// - public async Task> CleanAsync(Func, Task> deleteFileAction, CancellationToken cancellationToken) + public async Task>> CleanAsync(Func, Task> deleteFileAction, CancellationToken cancellationToken) { - var results = new List(); + var results = new List>(); foreach (ILibraryInstallationState state in Libraries) { @@ -521,29 +519,10 @@ private async Task> GetFilesWithVersionsAsync(ILibra return filesWithVersions; } - ILibraryOperationResult validationResult = await state.IsValidAsync(_dependencies).ConfigureAwait(false); + OperationResult validationResult = await state.IsValidAsync(_dependencies).ConfigureAwait(false); if (validationResult.Success) { - IProvider provider = _dependencies.GetProvider(state.ProviderId); - - if (provider != null) - { - ILibraryOperationResult updatedStateResult = await provider.UpdateStateAsync(state, CancellationToken.None).ConfigureAwait(false); - - if (updatedStateResult.Success) - { - ILibrary library = await catalog.GetLibraryAsync(state.Name, state.Version, CancellationToken.None).ConfigureAwait(false); - - if (library != null && library.Files != null) - { - IEnumerable desiredStateFiles = updatedStateResult.InstallationState.Files; - if (desiredStateFiles != null && desiredStateFiles.Any()) - { - filesWithVersions = desiredStateFiles.Select(f => new FileIdentifier(Path.Combine(state.DestinationPath, f), library.Version)); - } - } - } - } + filesWithVersions = validationResult.Result.InstalledFiles.Select(f => new FileIdentifier(f.Key, state.Version)); } else { @@ -554,7 +533,7 @@ private async Task> GetFilesWithVersionsAsync(ILibra return filesWithVersions; } - private async Task DeleteLibraryFilesAsync(ILibraryInstallationState state, + private async Task> DeleteLibraryFilesAsync(ILibraryInstallationState state, Func, Task> deleteFilesFunction, CancellationToken cancellationToken) { @@ -568,10 +547,7 @@ private async Task DeleteLibraryFilesAsync(ILibraryInst OperationResult getGoalState = await provider.GetInstallationGoalStateAsync(state, cancellationToken).ConfigureAwait(false); if (!getGoalState.Success) { - return new LibraryOperationResult(state, [.. getGoalState.Errors]) - { - Cancelled = getGoalState.Cancelled, - }; + return getGoalState; } LibraryInstallationGoalState goalState = getGoalState.Result; @@ -584,20 +560,20 @@ private async Task DeleteLibraryFilesAsync(ILibraryInst if (success) { - return LibraryOperationResult.FromSuccess(goalState.InstallationState); + return OperationResult.FromSuccess(goalState); } else { - return LibraryOperationResult.FromError(PredefinedErrors.CouldNotDeleteLibrary(libraryId)); + return OperationResult.FromError(PredefinedErrors.CouldNotDeleteLibrary(libraryId)); } } catch (OperationCanceledException) { - return LibraryOperationResult.FromCancelled(state); + return OperationResult.FromCancelled(null); } catch (Exception) { - return LibraryOperationResult.FromError(PredefinedErrors.CouldNotDeleteLibrary(libraryId)); + return OperationResult.FromError(PredefinedErrors.CouldNotDeleteLibrary(libraryId)); } } } diff --git a/src/LibraryManager/Providers/BaseProvider.cs b/src/LibraryManager/Providers/BaseProvider.cs index ae1c149d..2178c582 100644 --- a/src/LibraryManager/Providers/BaseProvider.cs +++ b/src/LibraryManager/Providers/BaseProvider.cs @@ -53,17 +53,17 @@ public BaseProvider(IHostInteraction hostInteraction, CacheService cacheService) public abstract string GetSuggestedDestination(ILibrary library); /// - public virtual async Task InstallAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken) + public virtual async Task> InstallAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { - return LibraryOperationResult.FromCancelled(desiredState); + return OperationResult.FromCancelled(null); } OperationResult getLibrary = await GetLibraryForInstallationState(desiredState, cancellationToken).ConfigureAwait(false); if (!getLibrary.Success) { - return new LibraryOperationResult(desiredState, [.. getLibrary.Errors]) + return new OperationResult([.. getLibrary.Errors]) { Cancelled = getLibrary.Cancelled, }; @@ -72,17 +72,14 @@ public virtual async Task InstallAsync(ILibraryInstalla OperationResult getGoalState = GenerateGoalState(desiredState, getLibrary.Result); if (!getGoalState.Success) { - return new LibraryOperationResult(desiredState, [.. getGoalState.Errors]) - { - Cancelled = getGoalState.Cancelled, - }; + return getGoalState; } LibraryInstallationGoalState goalState = getGoalState.Result; if (!IsSourceCacheReady(goalState)) { - ILibraryOperationResult updateCacheResult = await RefreshCacheAsync(desiredState, getLibrary.Result, cancellationToken); + OperationResult updateCacheResult = await RefreshCacheAsync(goalState, getLibrary.Result, cancellationToken); if (!updateCacheResult.Success) { return updateCacheResult; @@ -91,7 +88,7 @@ public virtual async Task InstallAsync(ILibraryInstalla if (goalState.IsAchieved()) { - return LibraryOperationResult.FromUpToDate(desiredState); + return OperationResult.FromUpToDate(goalState); } return await InstallFiles(goalState, cancellationToken); @@ -120,7 +117,7 @@ private async Task> GetLibraryForInstallationState(ILi return OperationResult.FromSuccess(library); } - private async Task InstallFiles(LibraryInstallationGoalState goalState, CancellationToken cancellationToken) + private async Task> InstallFiles(LibraryInstallationGoalState goalState, CancellationToken cancellationToken) { try { @@ -128,7 +125,7 @@ private async Task InstallFiles(LibraryInstallationGoalS { if (cancellationToken.IsCancellationRequested) { - return LibraryOperationResult.FromCancelled(goalState.InstallationState); + return OperationResult.FromCancelled(goalState); } string sourcePath = kvp.Value; @@ -137,98 +134,21 @@ private async Task InstallFiles(LibraryInstallationGoalS if (!writeOk) { - return new LibraryOperationResult(goalState.InstallationState, PredefinedErrors.CouldNotWriteFile(destinationPath)); + return new OperationResult(goalState, PredefinedErrors.CouldNotWriteFile(destinationPath)); } } } catch (UnauthorizedAccessException) { - return new LibraryOperationResult(goalState.InstallationState, PredefinedErrors.PathOutsideWorkingDirectory()); - } - catch (Exception ex) - { - HostInteraction.Logger.Log(ex.ToString(), LogLevel.Error); - return new LibraryOperationResult(goalState.InstallationState, PredefinedErrors.UnknownException()); - } - - return LibraryOperationResult.FromSuccess(goalState.InstallationState); - } - - /// - public virtual async Task UpdateStateAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return LibraryOperationResult.FromCancelled(desiredState); - } - - string libraryId = LibraryNamingScheme.GetLibraryId(desiredState.Name, desiredState.Version); - try - { - ILibraryCatalog catalog = GetCatalog(); - - if (string.Equals(desiredState.Version, ManifestConstants.LatestVersion, StringComparison.Ordinal)) - { - // replace the @latest version with the latest version from the catalog. This redirect - // ensures that as new versions are released, we will not reuse stale "latest" assets - // from the cache. - string latestVersion = await catalog.GetLatestVersion(libraryId, includePreReleases: false, cancellationToken).ConfigureAwait(false); - LibraryInstallationState newState = LibraryInstallationState.FromInterface(desiredState); - newState.Version = latestVersion; - desiredState = newState; - } - - ILibrary library = await catalog.GetLibraryAsync(desiredState.Name, desiredState.Version, cancellationToken).ConfigureAwait(false); - - if (library == null) - { - return new LibraryOperationResult(desiredState, PredefinedErrors.UnableToResolveSource(desiredState.Name, desiredState.ProviderId)); - } - - if (desiredState.Files != null && desiredState.Files.Count > 0) - { - // expand any potential file patterns - IEnumerable updatedFiles = FileGlobbingUtility.ExpandFileGlobs(desiredState.Files, library.Files.Keys); - var processedState = new LibraryInstallationState - { - Name = desiredState.Name, - Version = desiredState.Version, - ProviderId = desiredState.ProviderId, - DestinationPath = desiredState.DestinationPath, - IsUsingDefaultDestination = desiredState.IsUsingDefaultDestination, - IsUsingDefaultProvider = desiredState.IsUsingDefaultProvider, - Files = updatedFiles.ToList(), - }; - - return CheckForInvalidFiles(processedState, libraryId, library); - } - - desiredState = new LibraryInstallationState - { - ProviderId = Id, - Name = desiredState.Name, - Version = desiredState.Version, - DestinationPath = desiredState.DestinationPath, - Files = library.Files.Keys.ToList(), - IsUsingDefaultDestination = desiredState.IsUsingDefaultDestination, - IsUsingDefaultProvider = desiredState.IsUsingDefaultProvider - }; - } - catch (InvalidLibraryException) - { - return new LibraryOperationResult(desiredState, PredefinedErrors.UnableToResolveSource(libraryId, desiredState.ProviderId)); - } - catch (UnauthorizedAccessException) - { - return new LibraryOperationResult(desiredState, PredefinedErrors.PathOutsideWorkingDirectory()); + return new OperationResult(goalState, PredefinedErrors.PathOutsideWorkingDirectory()); } catch (Exception ex) { HostInteraction.Logger.Log(ex.ToString(), LogLevel.Error); - return new LibraryOperationResult(desiredState, PredefinedErrors.UnknownException()); + return new OperationResult(goalState, PredefinedErrors.UnknownException()); } - return LibraryOperationResult.FromSuccess(desiredState); + return OperationResult.FromSuccess(goalState); } public async Task> GetInstallationGoalStateAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken) @@ -359,20 +279,6 @@ public bool IsSourceCacheReady(LibraryInstallationGoalState goalState) return true; } - protected virtual ILibraryOperationResult CheckForInvalidFiles(ILibraryInstallationState desiredState, string libraryId, ILibrary library) - { - IReadOnlyList invalidFiles = library.GetInvalidFiles(desiredState.Files); - if (invalidFiles.Count > 0) - { - IError invalidFilesError = PredefinedErrors.InvalidFilesInLibrary(libraryId, invalidFiles, library.Files.Keys); - return new LibraryOperationResult(desiredState, invalidFilesError); - } - else - { - return LibraryOperationResult.FromSuccess(desiredState); - } - } - protected virtual ILibraryNamingScheme LibraryNamingScheme { get; } = new VersionedLibraryNamingScheme(); public string CacheFolder @@ -380,53 +286,6 @@ public string CacheFolder get { return _cacheFolder ?? (_cacheFolder = Path.Combine(HostInteraction.CacheDirectory, Id)); } } - /// - /// Copy files from the download cache to the desired installation state - /// - /// Precondition: all files must already exist in the cache - protected async Task WriteToFilesAsync(ILibraryInstallationState state, CancellationToken cancellationToken) - { - if (state.Files != null) - { - try - { - foreach (string file in state.Files) - { - if (cancellationToken.IsCancellationRequested) - { - return LibraryOperationResult.FromCancelled(state); - } - - if (string.IsNullOrEmpty(file)) - { - string id = LibraryNamingScheme.GetLibraryId(state.Name, state.Version); - return new LibraryOperationResult(state, PredefinedErrors.FileNameMustNotBeEmpty(id)); - } - - string sourcePath = GetCachedFileLocalPath(state, file); - string destinationPath = Path.Combine(state.DestinationPath, file); - bool writeOk = await HostInteraction.CopyFileAsync(sourcePath, destinationPath, cancellationToken); - - if (!writeOk) - { - return new LibraryOperationResult(state, PredefinedErrors.CouldNotWriteFile(file)); - } - } - } - catch (UnauthorizedAccessException) - { - return new LibraryOperationResult(state, PredefinedErrors.PathOutsideWorkingDirectory()); - } - catch (Exception ex) - { - HostInteraction.Logger.Log(ex.ToString(), LogLevel.Error); - return new LibraryOperationResult(state, PredefinedErrors.UnknownException()); - } - } - - return LibraryOperationResult.FromSuccess(state); - } - /// /// Gets the expected local path for a file from the file cache /// @@ -443,33 +302,36 @@ protected virtual string GetCachedFileLocalPath(ILibraryInstallationState state, /// Library resolved from provider /// /// - private async Task RefreshCacheAsync(ILibraryInstallationState state, ILibrary library, CancellationToken cancellationToken) + private async Task> RefreshCacheAsync(LibraryInstallationGoalState goalState, ILibrary library, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { - return LibraryOperationResult.FromCancelled(state); + return OperationResult.FromCancelled(goalState); } - string libraryDir = Path.Combine(CacheFolder, state.Name, state.Version); + string libraryDir = Path.Combine(CacheFolder, goalState.InstallationState.Name, goalState.InstallationState.Version); try { IEnumerable filesToCache; // expand "files" to concrete files in the library - if (state.Files == null || state.Files.Count == 0) + + + // TODO: where do we do FileMappings? + if (goalState.InstallationState.Files == null || goalState.InstallationState.Files.Count == 0) { filesToCache = library.Files.Keys; } else { - filesToCache = FileGlobbingUtility.ExpandFileGlobs(state.Files, library.Files.Keys); + filesToCache = FileGlobbingUtility.ExpandFileGlobs(goalState.InstallationState.Files, library.Files.Keys); } var librariesMetadata = new HashSet(); foreach (string sourceFile in filesToCache) { string cacheFile = Path.Combine(libraryDir, sourceFile); - string url = GetDownloadUrl(state, sourceFile); + string url = GetDownloadUrl(goalState.InstallationState, sourceFile); var newEntry = new CacheFileMetadata(url, cacheFile); librariesMetadata.Add(newEntry); @@ -479,19 +341,19 @@ private async Task RefreshCacheAsync(ILibraryInstallati catch (ResourceDownloadException ex) { HostInteraction.Logger.Log(ex.ToString(), LogLevel.Error); - return new LibraryOperationResult(state, PredefinedErrors.FailedToDownloadResource(ex.Url)); + return new OperationResult(goalState, PredefinedErrors.FailedToDownloadResource(ex.Url)); } catch (OperationCanceledException) { - return LibraryOperationResult.FromCancelled(state); + return OperationResult.FromCancelled(goalState); } catch (Exception ex) { HostInteraction.Logger.Log(ex.InnerException.ToString(), LogLevel.Error); - return new LibraryOperationResult(state, PredefinedErrors.UnknownException()); + return new OperationResult(goalState, PredefinedErrors.UnknownException()); } - return LibraryOperationResult.FromSuccess(state); + return OperationResult.FromSuccess(goalState); } protected abstract string GetDownloadUrl(ILibraryInstallationState state, string sourceFile); diff --git a/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs b/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs index 9101767b..2084e7cf 100644 --- a/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs +++ b/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs @@ -54,36 +54,33 @@ public override ILibraryCatalog GetCatalog() /// The details about the library to install. /// A token that allows for the operation to be cancelled. /// - /// The from the installation process. + /// The from the installation process. /// - public override async Task InstallAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken) + public override async Task> InstallAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { - return LibraryOperationResult.FromCancelled(desiredState); + return OperationResult.FromCancelled(null); } try { - ILibraryOperationResult result = await UpdateStateAsync(desiredState, cancellationToken); - - if (!result.Success) + OperationResult goalStateResult = await GetInstallationGoalStateAsync(desiredState, cancellationToken).ConfigureAwait(false); + if (!goalStateResult.Success) { - return result; + return goalStateResult; } - desiredState = result.InstallationState; - foreach (string file in desiredState.Files) { if (cancellationToken.IsCancellationRequested) { - return LibraryOperationResult.FromCancelled(desiredState); + return OperationResult.FromCancelled(goalStateResult.Result); } if (string.IsNullOrEmpty(file)) { - return new LibraryOperationResult(desiredState, PredefinedErrors.CouldNotWriteFile(file)); + return OperationResult.FromError(PredefinedErrors.CouldNotWriteFile(file)); } string path = Path.Combine(desiredState.DestinationPath, file); @@ -92,30 +89,25 @@ public override async Task InstallAsync(ILibraryInstall if (!writeOk) { - return new LibraryOperationResult(desiredState, PredefinedErrors.CouldNotWriteFile(file)); + return OperationResult.FromError(PredefinedErrors.CouldNotWriteFile(file)); } } + + return OperationResult.FromSuccess(goalStateResult.Result); } catch (UnauthorizedAccessException) { - return new LibraryOperationResult(desiredState, PredefinedErrors.PathOutsideWorkingDirectory()); + return OperationResult.FromError(PredefinedErrors.PathOutsideWorkingDirectory()); } catch (ResourceDownloadException ex) { - return new LibraryOperationResult(desiredState, PredefinedErrors.FailedToDownloadResource(ex.Url)); + return OperationResult.FromError(PredefinedErrors.FailedToDownloadResource(ex.Url)); } catch (Exception ex) { HostInteraction.Logger.Log(ex.ToString(), LogLevel.Error); - return new LibraryOperationResult(desiredState, PredefinedErrors.UnknownException()); + return OperationResult.FromError(PredefinedErrors.UnknownException()); } - - return LibraryOperationResult.FromSuccess(desiredState); - } - - protected override ILibraryOperationResult CheckForInvalidFiles(ILibraryInstallationState desiredState, string libraryId, ILibrary library) - { - return LibraryOperationResult.FromSuccess(desiredState); } /// diff --git a/src/libman/Commands/BaseCommand.cs b/src/libman/Commands/BaseCommand.cs index aaa9092d..456384f7 100644 --- a/src/libman/Commands/BaseCommand.cs +++ b/src/libman/Commands/BaseCommand.cs @@ -204,7 +204,7 @@ protected async Task CreateManifestAsync(string defaultProvider, return manifest; } - protected void LogResultsSummary(IEnumerable results, OperationType operation, TimeSpan elapsedTime) + protected void LogResultsSummary(IEnumerable> results, OperationType operation, TimeSpan elapsedTime) { string messageText = LogMessageGenerator.GetOperationSummaryString(results, operation, elapsedTime); diff --git a/src/libman/Commands/CleanCommand.cs b/src/libman/Commands/CleanCommand.cs index d1085fbb..752f3c0b 100644 --- a/src/libman/Commands/CleanCommand.cs +++ b/src/libman/Commands/CleanCommand.cs @@ -30,7 +30,7 @@ protected override async Task ExecuteInternalAsync() Manifest manifest = await GetManifestAsync(); Task deleteFileAction(IEnumerable s) => HostInteractions.DeleteFilesAsync(s, CancellationToken.None); - IEnumerable validationResults = await manifest.GetValidationResultsAsync(CancellationToken.None); + IEnumerable> validationResults = await manifest.GetValidationResultsAsync(CancellationToken.None); if (!validationResults.All(r => r.Success)) { @@ -40,7 +40,7 @@ protected override async Task ExecuteInternalAsync() return 0; } - IEnumerable results = await manifest.CleanAsync(deleteFileAction, CancellationToken.None); + IEnumerable> results = await manifest.CleanAsync(deleteFileAction, CancellationToken.None); sw.Stop(); LogResultsSummary(results, OperationType.Clean, sw.Elapsed); diff --git a/src/libman/Commands/InstallCommand.cs b/src/libman/Commands/InstallCommand.cs index 563effeb..c03d8547 100644 --- a/src/libman/Commands/InstallCommand.cs +++ b/src/libman/Commands/InstallCommand.cs @@ -138,7 +138,7 @@ protected override async Task ExecuteInternalAsync() destinationToUse = destinationToUse.Replace('\\', '/'); } - ILibraryOperationResult result = await _manifest.InstallLibraryAsync( + OperationResult result = await _manifest.InstallLibraryAsync( library.Name, library.Version, providerIdToUse, diff --git a/src/libman/Commands/RestoreCommand.cs b/src/libman/Commands/RestoreCommand.cs index fcd318af..9df1218c 100644 --- a/src/libman/Commands/RestoreCommand.cs +++ b/src/libman/Commands/RestoreCommand.cs @@ -28,7 +28,7 @@ protected override async Task ExecuteInternalAsync() sw.Start(); Manifest manifest = await GetManifestAsync(); - IEnumerable validationResults = await manifest.GetValidationResultsAsync(CancellationToken.None); + IEnumerable> validationResults = await manifest.GetValidationResultsAsync(CancellationToken.None); if (!validationResults.All(r => r.Success)) { @@ -38,7 +38,7 @@ protected override async Task ExecuteInternalAsync() return (int)ExitCode.Failure; } - IList results = await ManifestRestorer.RestoreManifestAsync(manifest, Logger, CancellationToken.None); + IList> results = await ManifestRestorer.RestoreManifestAsync(manifest, Logger, CancellationToken.None); sw.Stop(); LogResultsSummary(results, OperationType.Restore, sw.Elapsed); diff --git a/src/libman/Commands/UninstallCommand.cs b/src/libman/Commands/UninstallCommand.cs index 9e8d3f2e..9b544c17 100644 --- a/src/libman/Commands/UninstallCommand.cs +++ b/src/libman/Commands/UninstallCommand.cs @@ -75,7 +75,7 @@ protected async override Task ExecuteInternalAsync() string libraryId = LibraryIdToNameAndVersionConverter.Instance.GetLibraryId(libraryToUninstall.Name, libraryToUninstall.Version, libraryToUninstall.ProviderId); - ILibraryOperationResult result = await manifest.UninstallAsync(libraryToUninstall.Name, libraryToUninstall.Version, deleteFileAction, CancellationToken.None); + OperationResult result = await manifest.UninstallAsync(libraryToUninstall.Name, libraryToUninstall.Version, deleteFileAction, CancellationToken.None); if (result.Success) { diff --git a/src/libman/Commands/UpdateCommand.cs b/src/libman/Commands/UpdateCommand.cs index e70333bc..87e75b07 100644 --- a/src/libman/Commands/UpdateCommand.cs +++ b/src/libman/Commands/UpdateCommand.cs @@ -72,7 +72,7 @@ public override BaseCommand Configure(CommandLineApplication parent = null) protected override async Task ExecuteInternalAsync() { Manifest manifest = await GetManifestAsync(); - IEnumerable validationResults = await manifest.GetValidationResultsAsync(CancellationToken.None); + IEnumerable> validationResults = await manifest.GetValidationResultsAsync(CancellationToken.None); if (!validationResults.All(r => r.Success)) { @@ -125,11 +125,11 @@ protected override async Task ExecuteInternalAsync() // Delete files from old version of the library. await backup.RemoveUnwantedFilesAsync(manifest, CancellationToken.None); - IEnumerable results = await manifest.RestoreAsync(CancellationToken.None); + IEnumerable> results = await manifest.RestoreAsync(CancellationToken.None); - ILibraryOperationResult result = null; + OperationResult result = null; - foreach (ILibraryOperationResult r in results) + foreach (OperationResult r in results) { if (!r.Success && r.Errors.Any(e => e.Message.Contains(libraryToUpdate.Name, StringComparison.OrdinalIgnoreCase))) { @@ -137,9 +137,10 @@ protected override async Task ExecuteInternalAsync() break; } else if (r.Success - && r.InstallationState.Name == libraryToUpdate.Name - && r.InstallationState.ProviderId == libraryToUpdate.ProviderId - && r.InstallationState.DestinationPath == libraryToUpdate.DestinationPath) + && r.Result != null + && r.Result.InstallationState.Name == libraryToUpdate.Name + && r.Result.InstallationState.ProviderId == libraryToUpdate.ProviderId + && r.Result.InstallationState.DestinationPath == libraryToUpdate.DestinationPath) { result = r; break; diff --git a/src/libman/ManifestRestorer.cs b/src/libman/ManifestRestorer.cs index 0ddd3272..28d51e83 100644 --- a/src/libman/ManifestRestorer.cs +++ b/src/libman/ManifestRestorer.cs @@ -22,20 +22,20 @@ internal static class ManifestRestorer /// /// /// - public static async Task> RestoreManifestAsync(Manifest manifest, ILogger logger, CancellationToken cancelToken) + public static async Task>> RestoreManifestAsync(Manifest manifest, ILogger logger, CancellationToken cancelToken) { - IList results = await manifest.RestoreAsync(cancelToken); + IList> results = await manifest.RestoreAsync(cancelToken); - IList failures = results.Where(r => r.Errors.Any()).ToList(); + IList> failures = results.Where(r => r.Errors.Any()).ToList(); if (failures.Any()) { var librarySpecificErrors = new StringBuilder(); var otherErrors = new StringBuilder(); - foreach (ILibraryOperationResult f in failures) + foreach (OperationResult f in failures) { - if (f.InstallationState != null) + if (f.Result?.InstallationState is ILibraryInstallationState installState) { - librarySpecificErrors.AppendLine(string.Format(Resources.Text.FailedToRestoreLibraryMessage, f.InstallationState.ToConsoleDisplayString())); + librarySpecificErrors.AppendLine(string.Format(Resources.Text.FailedToRestoreLibraryMessage, installState.ToConsoleDisplayString())); foreach (IError e in f.Errors) { librarySpecificErrors.AppendLine($" - [{e.Code}]: {e.Message}"); diff --git a/test/LibraryManager.Mocks/LibraryOperationResult.cs b/test/LibraryManager.Mocks/LibraryOperationResult.cs deleted file mode 100644 index 184b6276..00000000 --- a/test/LibraryManager.Mocks/LibraryOperationResult.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Web.LibraryManager.Contracts; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.Web.LibraryManager.Mocks -{ - /// - /// A mock class. - /// - /// - public class LibraryOperationResult : ILibraryOperationResult - { - /// - /// Initializes a new instance of the class. - /// - public LibraryOperationResult() - { - Errors = new List(); - } - - /// - /// Initializes a new instance of the class. - /// - /// The errors. - public LibraryOperationResult(params IError[] errors) - { - var list = new List(); - list.AddRange(errors); - Errors = list; - } - - /// - /// True if the installation was cancelled; otherwise false; - /// - public virtual bool Cancelled - { - get; - set; - } - - /// - /// True if the install was successfull; otherwise False. - /// - /// - /// The value is usually True if the list is empty. - /// - public virtual bool Success - { - get { return !Errors.Any() && !Cancelled && !UpToDate; } - } - - /// - /// A list of errors that occured during library installation. - /// - public virtual IList Errors - { - get; - set; - } - - /// - /// The object passed to the - /// for installation. - /// - public virtual ILibraryInstallationState InstallationState - { - get; - set; - } - - /// - /// - public virtual bool UpToDate - { - get; - set; - } - - /// - /// Creates a new that is in a successfull state. - /// - public static LibraryOperationResult FromSuccess() - { - return new LibraryOperationResult(); - } - - /// - /// Creates a new that is in a cancelled state. - /// - public static LibraryOperationResult FromCancelled() - { - return new LibraryOperationResult - { - Cancelled = true - }; - } - } -} diff --git a/test/LibraryManager.Mocks/Provider.cs b/test/LibraryManager.Mocks/Provider.cs index c745d5ec..d706e2f3 100644 --- a/test/LibraryManager.Mocks/Provider.cs +++ b/test/LibraryManager.Mocks/Provider.cs @@ -56,7 +56,7 @@ public Provider(IHostInteraction hostInteraction) /// /// Gets or sets the result to return from the method. /// - public virtual ILibraryOperationResult Result { get; set; } + public virtual OperationResult Result { get; set; } /// /// Gets or sets the goal state to return from @@ -85,7 +85,7 @@ public virtual ILibraryCatalog GetCatalog() /// /// The from the installation process. /// - public virtual Task InstallAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken) + public virtual Task> InstallAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken) { return Task.FromResult(Result); } @@ -96,7 +96,7 @@ public virtual Task InstallAsync(ILibraryInstallationSt /// /// /// - public virtual Task UpdateStateAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken) + public virtual Task> UpdateStateAsync(ILibraryInstallationState desiredState, CancellationToken cancellationToken) { return Task.FromResult(Result); } diff --git a/test/LibraryManager.Test/LibrariesValidatorTest.cs b/test/LibraryManager.Test/LibrariesValidatorTest.cs index 6513680e..e9437028 100644 --- a/test/LibraryManager.Test/LibrariesValidatorTest.cs +++ b/test/LibraryManager.Test/LibrariesValidatorTest.cs @@ -48,7 +48,7 @@ public async Task DetectConflictsAsync_ConflictingFiles_SameDestination() string expectedErrorMessage = "Conflicting file \"lib\\package.json\" found in more than one library: jquery, d3"; var manifest = Manifest.FromJson(_docDifferentLibraries_SameFiles_SameLocation, _dependencies); - IEnumerable conflicts = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); + IEnumerable> conflicts = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); var conflictsList = conflicts.ToList(); Assert.AreEqual(1, conflictsList.Count); @@ -62,7 +62,7 @@ public async Task DetectConflictsAsync_ConflictingFiles_DifferentDestinations() { var manifest = Manifest.FromJson(_docDifferentLibraries_SameFiles_DifferentLocation, _dependencies); - IEnumerable conflicts = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); + IEnumerable> conflicts = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); Assert.IsTrue(conflicts.All(c => c.Success)); } @@ -74,7 +74,7 @@ public async Task DetectConflictsAsync_SameLibrary_DifferentDestinations() string expectedErrorMessage = "Cannot restore. Multiple definitions for libraries: jquery"; var manifest = Manifest.FromJson(_docSameLibrary_DifferentDestination, _dependencies); - IEnumerable results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); + IEnumerable> results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); var conflictsList = results.ToList(); Assert.AreEqual(1, conflictsList.Count); @@ -90,7 +90,7 @@ public async Task DetectConflictsAsync_SameLibrary_DifferentProviders() string expectedErrorMessage = "Cannot restore. Multiple definitions for libraries: jquery"; var manifest = Manifest.FromJson(_docSameLibrary_DifferentProviders, _dependencies); - IEnumerable results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); + IEnumerable> results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); var conflictsList = results.ToList(); Assert.AreEqual(1, conflictsList.Count); @@ -106,7 +106,7 @@ public async Task DetectConflictsAsync_SameLibrary_DifferentVersions_DifferentFi string expectedErrorMessage = "Cannot restore. Multiple definitions for libraries: jquery"; var manifest = Manifest.FromJson(_docSameLibrary_DifferentVersions_DifferentFiles, _dependencies); - IEnumerable results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); + IEnumerable> results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); var conflictsList = results.ToList(); Assert.AreEqual(1, conflictsList.Count); @@ -121,7 +121,7 @@ public async Task GetManifestErrors_ManifestIsNull() string expectedErrorCode = "LIB004"; Manifest manifest = null; - IEnumerable results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); + IEnumerable> results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); var resultsList = results.ToList(); Assert.AreEqual(1, resultsList.Count); @@ -135,7 +135,7 @@ public async Task GetManifestErrors_ManifestHasUnsupportedVersion() string expectedErrorCode = "LIB009"; var manifest = Manifest.FromJson(_docUnsupportedVersion, _dependencies); - IEnumerable results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); + IEnumerable> results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); var resultsList = results.ToList(); Assert.AreEqual(1, resultsList.Count); @@ -149,7 +149,7 @@ public async Task GetLibrariesErrors_LibrariesNoProvider() string expectedErrorCode = "LIB007"; var manifest = Manifest.FromJson(_docNoProvider, _dependencies); - IEnumerable results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); + IEnumerable> results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); var resultsList = results.ToList(); Assert.AreEqual(1, resultsList.Count); diff --git a/test/LibraryManager.Test/LibraryInstallationResultTest.cs b/test/LibraryManager.Test/LibraryInstallationResultTest.cs deleted file mode 100644 index cf69e71d..00000000 --- a/test/LibraryManager.Test/LibraryInstallationResultTest.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Collections.Generic; -using Microsoft.Web.LibraryManager.Contracts; - -namespace Microsoft.Web.LibraryManager.Test -{ - [TestClass] - public class LibraryOperationResultTest - { - [TestMethod] - public void Constructor() - { - Mocks.LibraryInstallationState state = GetState(); - - var ctor1 = new LibraryOperationResult(state); - Assert.AreEqual(state, ctor1.InstallationState); - Assert.AreEqual(0, ctor1.Errors.Count); - Assert.IsTrue(ctor1.Success); - Assert.IsFalse(ctor1.Cancelled); - - var ctor2 = new LibraryOperationResult(state, PredefinedErrors.ManifestMalformed()); - Assert.AreEqual(state, ctor2.InstallationState); - Assert.AreEqual(1, ctor2.Errors.Count); - Assert.IsFalse(ctor2.Success); - Assert.IsFalse(ctor2.Cancelled); - } - - [TestMethod] - public void FromSuccess() - { - Mocks.LibraryInstallationState state = GetState(); - var result = LibraryOperationResult.FromSuccess(state); - - Assert.AreEqual(state, result.InstallationState); - Assert.AreEqual(0, result.Errors.Count); - Assert.IsTrue(result.Success); - Assert.IsFalse(result.Cancelled); - } - - [TestMethod] - public void FromCancelled() - { - Mocks.LibraryInstallationState state = GetState(); - var result = LibraryOperationResult.FromCancelled(state); - - Assert.AreEqual(state, result.InstallationState); - Assert.AreEqual(0, result.Errors.Count); - Assert.IsFalse(result.Success); - Assert.IsTrue(result.Cancelled); - } - - private static Mocks.LibraryInstallationState GetState() - { - return new Mocks.LibraryInstallationState - { - ProviderId = "_prov_", - Name = "_lib_", - DestinationPath = "_path_", - Files = new List() { "a", "b" }, - }; - } - - } -} diff --git a/test/LibraryManager.Test/LibraryInstallationStateTest.cs b/test/LibraryManager.Test/LibraryInstallationStateTest.cs index 4843b145..f77f0bc2 100644 --- a/test/LibraryManager.Test/LibraryInstallationStateTest.cs +++ b/test/LibraryManager.Test/LibraryInstallationStateTest.cs @@ -66,7 +66,7 @@ public async Task IsValidAsync_NullState() { ILibraryInstallationState state = null; - ILibraryOperationResult result = await state.IsValidAsync(_dependencies); + OperationResult result = await state.IsValidAsync(_dependencies); Assert.IsFalse(result.Success); Assert.AreEqual(result.Errors.Count, 1); @@ -84,7 +84,7 @@ public async Task IsValidAsync_State_HasUnknownProvider() Files = new List() { "a", "b" }, }; - ILibraryOperationResult result = await state.IsValidAsync(_dependencies); + OperationResult result = await state.IsValidAsync(_dependencies); Assert.IsFalse(result.Success); Assert.AreEqual(result.Errors.Count, 1); @@ -101,7 +101,7 @@ public async Task IsValidAsync_State_HasNoProvider() Files = new List() { "a", "b" }, }; - ILibraryOperationResult result = await state.IsValidAsync(_dependencies); + OperationResult result = await state.IsValidAsync(_dependencies); Assert.IsFalse(result.Success); Assert.AreEqual(result.Errors.Count, 1); @@ -118,7 +118,7 @@ public async Task IsValidAsync_State_HasNoLibraryId() Files = new List() { "a", "b" }, }; - ILibraryOperationResult result = await state.IsValidAsync(_dependencies); + OperationResult result = await state.IsValidAsync(_dependencies); Assert.IsFalse(result.Success); Assert.AreEqual(result.Errors.Count, 1); @@ -136,7 +136,7 @@ public async Task IsValidAsync_State_HasUnknownLibrary() Files = new List() { "a", "b" }, }; - ILibraryOperationResult result = await state.IsValidAsync(_dependencies); + OperationResult result = await state.IsValidAsync(_dependencies); Assert.IsFalse(result.Success); Assert.AreEqual(result.Errors.Count, 1); @@ -155,7 +155,7 @@ public async Task IsValidAsync_State_HasUnknownLibraryFile() Files = new List() { "a", "b" }, }; - ILibraryOperationResult result = await state.IsValidAsync(_dependencies); + OperationResult result = await state.IsValidAsync(_dependencies); // IsValidAsync does not validate library files // Issue https://github.com/aspnet/LibraryManager/issues/254 should fix that @@ -173,7 +173,7 @@ public async Task IsValidAsync_State_FileSystem_LibraryIdHasInvalidPathCharacter Files = new List() { "a", "b" }, }; - ILibraryOperationResult result = await state.IsValidAsync(_dependencies); + OperationResult result = await state.IsValidAsync(_dependencies); Assert.IsFalse(result.Success); Assert.AreEqual(result.Errors.Count, 1); @@ -190,7 +190,7 @@ public async Task IsValidAsync_State_FileSystem_LibraryIdDoesNotExist() DestinationPath = "lib", }; - ILibraryOperationResult result = await state.IsValidAsync(_dependencies); + OperationResult result = await state.IsValidAsync(_dependencies); Assert.IsFalse(result.Success); Assert.AreEqual(result.Errors.Count, 1); @@ -208,7 +208,7 @@ public async Task IsValidAsync_State_FileSystem_FilesDoNotExist() Files = new[] { "foo.png" } }; - ILibraryOperationResult result = await state.IsValidAsync(_dependencies); + OperationResult result = await state.IsValidAsync(_dependencies); // FileSystemProvider supports renaming, therefore validation does not fail Assert.IsTrue(result.Success); @@ -224,7 +224,7 @@ public async Task IsValidAsync_State_DestinationPath_HasInvalidCharacters() DestinationPath = "|lib" }; - ILibraryOperationResult result = await state.IsValidAsync(_dependencies); + OperationResult result = await state.IsValidAsync(_dependencies); Assert.IsFalse(result.Success); Assert.AreEqual(result.Errors.Count, 1); diff --git a/test/LibraryManager.Test/ManifestTest.cs b/test/LibraryManager.Test/ManifestTest.cs index 13709056..b94c4820 100644 --- a/test/LibraryManager.Test/ManifestTest.cs +++ b/test/LibraryManager.Test/ManifestTest.cs @@ -60,7 +60,7 @@ public async Task SaveAsync_Success() Files = new[] { "jquery.min.js" } }; - ILibraryOperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsTrue(result.Success); manifest.AddVersion("1.0"); @@ -92,7 +92,7 @@ public async Task UninstallAsync_Success() manifest.AddVersion("1.0"); manifest.AddLibrary(desiredState); - IEnumerable results = await manifest.RestoreAsync(token); + IList> results = await manifest.RestoreAsync(token); string file1 = Path.Combine(_projectFolder, "lib", "jquery.js"); string file2 = Path.Combine(_projectFolder, "lib", "jquery.min.js"); @@ -101,7 +101,7 @@ public async Task UninstallAsync_Success() Assert.IsTrue(results.Count() == 1); Assert.IsTrue(results.First().Success); - ILibraryOperationResult uninstallResult = await manifest.UninstallAsync(desiredState.Name, desiredState.Version, (file) => _hostInteraction.DeleteFilesAsync(file, token), token); + OperationResult uninstallResult = await manifest.UninstallAsync(desiredState.Name, desiredState.Version, (file) => _hostInteraction.DeleteFilesAsync(file, token), token); Assert.IsFalse(File.Exists(file1)); Assert.IsFalse(File.Exists(file2)); @@ -146,7 +146,7 @@ public async Task CleanAsync_Success() Assert.IsTrue(File.Exists(file2)); Assert.IsTrue(File.Exists(file3)); - IEnumerable results = await manifest.CleanAsync((file) => _hostInteraction.DeleteFilesAsync(file, token), token); + IEnumerable> results = await manifest.CleanAsync((file) => _hostInteraction.DeleteFilesAsync(file, token), token); Assert.IsFalse(File.Exists(file1)); Assert.IsFalse(File.Exists(file2)); @@ -159,7 +159,7 @@ public async Task CleanAsync_Success() public async Task RestoreAsync_PartialSuccess() { var manifest = Manifest.FromJson(_doc, _dependencies); - IEnumerable result = await manifest.RestoreAsync(CancellationToken.None).ConfigureAwait(false); + IList> result = await manifest.RestoreAsync(CancellationToken.None).ConfigureAwait(false); Assert.AreEqual(2, result.Count()); Assert.AreEqual(1, result.Count(v => v.Success)); @@ -171,22 +171,22 @@ public async Task RestoreAsync_PartialSuccess() public async Task RestoreAsync_UsingDefaultProvider() { var manifest = Manifest.FromJson(_docDefaultProvider, _dependencies); - IEnumerable result = await manifest.RestoreAsync(CancellationToken.None).ConfigureAwait(false); + IList> result = await manifest.RestoreAsync(CancellationToken.None).ConfigureAwait(false); Assert.AreEqual(2, result.Count()); Assert.AreEqual(2, result.Count(v => v.Success)); - Assert.AreEqual(manifest.DefaultProvider, result.First().InstallationState.ProviderId); + Assert.AreEqual(manifest.DefaultProvider, result.First().Result.InstallationState.ProviderId); } [TestMethod] public async Task RestoreAsync_UsingDefaultDestination() { var manifest = Manifest.FromJson(_docDefaultDestination, _dependencies); - IEnumerable result = await manifest.RestoreAsync(CancellationToken.None).ConfigureAwait(false); + IList> result = await manifest.RestoreAsync(CancellationToken.None).ConfigureAwait(false); Assert.AreEqual(1, result.Count()); Assert.AreEqual(1, result.Count(v => v.Success)); - Assert.AreEqual(manifest.DefaultDestination, result.First().InstallationState.DestinationPath); + Assert.AreEqual(manifest.DefaultDestination, result.First().Result.InstallationState.DestinationPath); } [TestMethod] @@ -194,7 +194,7 @@ public async Task RestoreAsync_UsingUnknownProvider() { var dependencies = new Dependencies(_dependencies.GetHostInteractions()); var manifest = Manifest.FromJson(_doc, dependencies); - IEnumerable result = await manifest.RestoreAsync(CancellationToken.None).ConfigureAwait(false); + IList> result = await manifest.RestoreAsync(CancellationToken.None).ConfigureAwait(false); Assert.AreEqual(2, result.Count()); Assert.AreEqual(2, result.Count(v => !v.Success)); @@ -219,7 +219,7 @@ public async Task RestoreAsync_PathOutsideWorkingDir(string path) manifest.AddVersion("1.0"); manifest.AddLibrary(state); - IEnumerable result = await manifest.RestoreAsync(CancellationToken.None); + IList> result = await manifest.RestoreAsync(CancellationToken.None); Assert.AreEqual(1, result.Count()); Assert.AreEqual("LIB008", result.First().Errors.First().Code); @@ -231,7 +231,7 @@ public async Task RestoreAsync_AllRestoreOperationsCancelled() var manifest = Manifest.FromJson(_doc, _dependencies); var source = new CancellationTokenSource(); source.Cancel(); - IEnumerable result = await manifest.RestoreAsync(source.Token); + IList> result = await manifest.RestoreAsync(source.Token); } [TestMethod] @@ -239,7 +239,7 @@ public async Task RestoreAsync_ConflictingLibraries_Validate() { var manifest = Manifest.FromJson(_docConflictingLibraries, _dependencies); - IEnumerable result = await manifest.GetValidationResultsAsync(CancellationToken.None); + IEnumerable> result = await manifest.GetValidationResultsAsync(CancellationToken.None); Assert.AreEqual(1, result.Count()); Assert.IsTrue(result.Last().Errors.Any(e => e.Code == "LIB019"), "LIB019 error code expected."); @@ -250,7 +250,7 @@ public async Task RestoreAsync_ConflictingLibraries_Restore() { var manifest = Manifest.FromJson(_docConflictingLibraries, _dependencies); - IEnumerable results = await manifest.RestoreAsync(CancellationToken.None); + IList> results = await manifest.RestoreAsync(CancellationToken.None); Assert.AreEqual(2, results.Count()); Assert.IsTrue(results.All(r =>r.Success)); @@ -268,7 +268,7 @@ public async Task FromJson_MissingManifestVersion() { var manifest = Manifest.FromJson("{}", _dependencies); - List result = (await manifest.GetValidationResultsAsync(CancellationToken.None)).ToList(); + List> result = (await manifest.GetValidationResultsAsync(CancellationToken.None)).ToList(); Assert.AreEqual(1, result.Count); Assert.IsFalse(result[0].Success); @@ -299,7 +299,7 @@ public async Task FromFileAsync_PathUndefined_Restore() manifest.AddVersion("1.0"); manifest.AddLibrary(state); - var result = await manifest.RestoreAsync(CancellationToken.None) as List; + IList> result = await manifest.RestoreAsync(CancellationToken.None); Assert.AreEqual(1, result.Count); Assert.IsFalse(result[0].Success); @@ -320,7 +320,7 @@ public async Task FromFileAsync_PathUndefined_Validate() manifest.AddVersion("1.0"); manifest.AddLibrary(state); - var result = await manifest.GetValidationResultsAsync(CancellationToken.None) as List; + List> result = (await manifest.GetValidationResultsAsync(CancellationToken.None)).ToList(); Assert.AreEqual(1, result.Count); Assert.IsFalse(result[0].Success); @@ -343,7 +343,7 @@ public async Task FromFileAsync_ProviderUndefined_Restore() manifest.AddVersion("1.0"); manifest.AddLibrary(state); - IEnumerable result = await manifest.RestoreAsync(CancellationToken.None); + IList> result = await manifest.RestoreAsync(CancellationToken.None); Assert.AreEqual(1, result.Count()); Assert.IsFalse(result.First().Success); @@ -366,7 +366,7 @@ public async Task FromFileAsync_ProviderUndefined_Validate() manifest.AddVersion("1.0"); manifest.AddLibrary(state); - IEnumerable result = await manifest.GetValidationResultsAsync(CancellationToken.None); + IEnumerable> result = await manifest.GetValidationResultsAsync(CancellationToken.None); Assert.AreEqual(1, result.Count()); Assert.IsFalse(result.First().Success); @@ -379,7 +379,7 @@ public async Task InstallLibraryAsync() var manifest = Manifest.FromJson("{}", _dependencies); // Null LibraryId - ILibraryOperationResult result = await manifest.InstallLibraryAsync(null, null,"cdnjs", null, "wwwroot", CancellationToken.None); + OperationResult result = await manifest.InstallLibraryAsync(null, null, "cdnjs", null, "wwwroot", CancellationToken.None); Assert.IsFalse(result.Success); Assert.AreEqual(1, result.Errors.Count); Assert.AreEqual("LIB006", result.Errors[0].Code); @@ -400,12 +400,13 @@ public async Task InstallLibraryAsync() // Valid Options all files. result = await manifest.InstallLibraryAsync("jquery", "3.3.1", "cdnjs", null, "wwwroot", CancellationToken.None); + LibraryInstallationGoalState goalstate = result.Result; Assert.IsTrue(result.Success); - Assert.AreEqual("wwwroot", result.InstallationState.DestinationPath); - Assert.AreEqual("jquery", result.InstallationState.Name); - Assert.AreEqual("3.3.1", result.InstallationState.Version); - Assert.AreEqual("cdnjs", result.InstallationState.ProviderId); + Assert.AreEqual("wwwroot", goalstate.InstallationState.DestinationPath); + Assert.AreEqual("jquery", goalstate.InstallationState.Name); + Assert.AreEqual("3.3.1", goalstate.InstallationState.Version); + Assert.AreEqual("cdnjs", goalstate.InstallationState.ProviderId); // Valid parameters and files. var files = new List() { "jquery.min.js" }; @@ -454,7 +455,7 @@ public async Task RestoreAsync_VersionIsNotSupported_Validate(string version) manifest.AddVersion(version); manifest.AddLibrary(state); - IEnumerable result = await manifest.GetValidationResultsAsync(CancellationToken.None); + IEnumerable> result = await manifest.GetValidationResultsAsync(CancellationToken.None); Assert.AreEqual(1, result.Count()); Assert.AreEqual("LIB009", result.First().Errors.First().Code); @@ -480,7 +481,7 @@ public async Task RestoreAsync_VersionIsNotSupported_Restore(string version) manifest.AddVersion(version); manifest.AddLibrary(state); - List results = await manifest.RestoreAsync(CancellationToken.None) as List; + IList> results = await manifest.RestoreAsync(CancellationToken.None); Assert.AreEqual(1, results.Count); Assert.IsTrue(results[0].Success); diff --git a/test/LibraryManager.Test/Providers/Cdnjs/CdnjsProviderTest.cs b/test/LibraryManager.Test/Providers/Cdnjs/CdnjsProviderTest.cs index 58664a91..fd88ef58 100644 --- a/test/LibraryManager.Test/Providers/Cdnjs/CdnjsProviderTest.cs +++ b/test/LibraryManager.Test/Providers/Cdnjs/CdnjsProviderTest.cs @@ -60,7 +60,7 @@ public async Task InstallAsync_InvalidState() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsFalse(result.Success); } @@ -76,7 +76,7 @@ public async Task InstallAsync_EmptyFilesArray() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsTrue(result.Success); foreach (string file in new[] { "jquery.js", "jquery.min.js" }) @@ -97,7 +97,7 @@ public async Task InstallAsync_NoPathDefined() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsFalse(result.Success); Assert.AreEqual("LIB021", result.Errors[0].Code); @@ -114,7 +114,7 @@ public async Task InstallAsync_NoProviderDefined() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsTrue(result.Success); } @@ -131,7 +131,7 @@ public async Task InstallAsync_InvalidLibraryFiles() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsFalse(result.Success); Assert.AreEqual("LIB018", result.Errors[0].Code); } @@ -155,7 +155,7 @@ public async Task InstallAsync_WithGlobPatterns_CorrectlyInstallsAllMatchingFile Assert.AreEqual("jquery.js", Path.GetFileName(goalState.InstalledFiles.Keys.First())); // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsTrue(result.Success); } diff --git a/test/LibraryManager.Test/Providers/FileSystem/FileSystemProviderTest.cs b/test/LibraryManager.Test/Providers/FileSystem/FileSystemProviderTest.cs index bd88cf98..2fed987a 100644 --- a/test/LibraryManager.Test/Providers/FileSystem/FileSystemProviderTest.cs +++ b/test/LibraryManager.Test/Providers/FileSystem/FileSystemProviderTest.cs @@ -66,7 +66,7 @@ public async Task InstallAsync_Success() Files = new[] { "file1.txt" } }; - ILibraryOperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); + OperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); Assert.IsTrue(result.Success, "Didn't install"); string copiedFile = Path.Combine(_projectFolder, desiredState.DestinationPath, desiredState.Files[0]); @@ -93,7 +93,7 @@ public async Task InstallAsync_RelativeFile() Files = new[] { "relative.txt" } }; - ILibraryOperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); + OperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); Assert.IsTrue(result.Success, "Didn't install"); string copiedFile = Path.Combine(_projectFolder, desiredState.DestinationPath, desiredState.Files[0]); @@ -128,7 +128,7 @@ public async Task InstallAsync_AbsoluteFolderFiles() Files = new[] { "file1.js", "file2.js" } }; - ILibraryOperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); + OperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); Assert.IsTrue(result.Success, "Didn't install"); string file1 = Path.Combine(_projectFolder, desiredState.DestinationPath, desiredState.Files[0]); @@ -163,7 +163,7 @@ public async Task InstallAsync_RelativeFolderFiles() Files = new[] { "file1.js", "file2.js" } }; - ILibraryOperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); + OperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); Assert.IsTrue(result.Success, "Didn't install"); string file1 = Path.Combine(_projectFolder, desiredState.DestinationPath, desiredState.Files[0]); @@ -188,7 +188,7 @@ public async Task InstallAsync_Uri() Files = new[] { "event.js" } }; - ILibraryOperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); + OperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); Assert.IsTrue(result.Success, "Didn't install"); string copiedFile = Path.Combine(_projectFolder, desiredState.DestinationPath, desiredState.Files[0]); @@ -215,7 +215,7 @@ public async Task InstallAsync_UriImage() Files = new[] { "Flag.png" } }; - ILibraryOperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); + OperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); Assert.IsTrue(result.Success, "Didn't install"); string copiedFile = Path.Combine(_projectFolder, desiredState.DestinationPath, desiredState.Files[0]); @@ -235,7 +235,7 @@ public async Task InstallAsync_FileNotFound() Files = new[] { "file.js" } }; - ILibraryOperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); + OperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); Assert.IsFalse(result.Success); Assert.AreEqual("LIB002", result.Errors[0].Code); } @@ -252,7 +252,7 @@ public async Task InstallAsync_PathNotDefined() Files = new[] { "file.js" } }; - ILibraryOperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); + OperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); Assert.IsFalse(result.Success); Assert.AreEqual(result.Errors.Count(), 1); Assert.AreEqual("LIB002", result.Errors[0].Code); @@ -270,7 +270,7 @@ public async Task InstallAsync_IdNotDefined() Files = new[] { "file.js" } }; - ILibraryOperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); + OperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); Assert.IsFalse(result.Success); Assert.AreEqual("LIB002", result.Errors[0].Code); } @@ -286,7 +286,7 @@ public async Task InstallAsync_ProviderNotDefined() Files = new[] { "Flag.png" } }; - ILibraryOperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); + OperationResult result = await provider.InstallAsync(desiredState, CancellationToken.None); Assert.IsTrue(result.Success); } @@ -296,7 +296,7 @@ public async Task RestoreAsync_Manifest() IProvider provider = _dependencies.GetProvider("filesystem"); string config = GetConfig(); var manifest = Manifest.FromJson(config, _dependencies); - IEnumerable result = await manifest.RestoreAsync(CancellationToken.None); + IList> result = await manifest.RestoreAsync(CancellationToken.None); Assert.IsTrue(result.Count() == 2, "Didn't install"); diff --git a/test/LibraryManager.Test/Providers/JsDelivr/JsDelivrProviderTest.cs b/test/LibraryManager.Test/Providers/JsDelivr/JsDelivrProviderTest.cs index f25e809d..0f05527e 100644 --- a/test/LibraryManager.Test/Providers/JsDelivr/JsDelivrProviderTest.cs +++ b/test/LibraryManager.Test/Providers/JsDelivr/JsDelivrProviderTest.cs @@ -60,7 +60,7 @@ public async Task InstallAsync_InvalidState() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsFalse(result.Success); } @@ -76,7 +76,7 @@ public async Task InstallAsync_EmptyFilesArray() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsTrue(result.Success); foreach (string file in new[] { "dist/jquery.js", "dist/jquery.min.js" }) @@ -97,7 +97,7 @@ public async Task InstallAsync_NoPathDefined() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsFalse(result.Success); Assert.AreEqual("LIB021", result.Errors[0].Code); @@ -114,7 +114,7 @@ public async Task InstallAsync_NoProviderDefined() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsTrue(result.Success); } @@ -131,7 +131,7 @@ public async Task InstallAsync_InvalidLibraryFiles() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsFalse(result.Success); Assert.AreEqual("LIB018", result.Errors[0].Code); } @@ -156,7 +156,7 @@ public async Task InstallAsync_WithGlobPatterns_CorrectlyInstallsAllMatchingFile 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); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsTrue(result.Success); } diff --git a/test/LibraryManager.Test/Providers/Unpkg/UnpkgProviderTest.cs b/test/LibraryManager.Test/Providers/Unpkg/UnpkgProviderTest.cs index 5f9c0466..988b8279 100644 --- a/test/LibraryManager.Test/Providers/Unpkg/UnpkgProviderTest.cs +++ b/test/LibraryManager.Test/Providers/Unpkg/UnpkgProviderTest.cs @@ -59,7 +59,7 @@ public async Task InstallAsync_InvalidState() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsFalse(result.Success); } @@ -75,7 +75,7 @@ public async Task InstallAsync_EmptyFilesArray() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsTrue(result.Success); foreach (string file in new[] { "dist/jquery.js", "dist/jquery.min.js" }) @@ -96,7 +96,7 @@ public async Task InstallAsync_NoPathDefined() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsFalse(result.Success); Assert.AreEqual("LIB021", result.Errors[0].Code); @@ -113,7 +113,7 @@ public async Task InstallAsync_NoProviderDefined() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsTrue(result.Success); } @@ -130,7 +130,7 @@ public async Task InstallAsync_InvalidLibraryFiles() }; // Install library - ILibraryOperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsFalse(result.Success); Assert.AreEqual("LIB018", result.Errors[0].Code); } @@ -155,7 +155,7 @@ public async Task InstallAsync_WithGlobPatterns_CorrectlyInstallsAllMatchingFile 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); + OperationResult result = await _provider.InstallAsync(desiredState, CancellationToken.None).ConfigureAwait(false); Assert.IsTrue(result.Success); } diff --git a/test/LibraryManager.Vsix.Test/Shared/LibraryCommandServiceTest.cs b/test/LibraryManager.Vsix.Test/Shared/LibraryCommandServiceTest.cs index c062404b..2b003a9b 100644 --- a/test/LibraryManager.Vsix.Test/Shared/LibraryCommandServiceTest.cs +++ b/test/LibraryManager.Vsix.Test/Shared/LibraryCommandServiceTest.cs @@ -46,9 +46,9 @@ public async Task UninstallAsync_DeletesFilesFromDisk() { Id = "testProvider", Catalog = new Mocks.LibraryCatalog(), - Result = new LibraryOperationResult + Result = new OperationResult { - InstallationState = testInstallationState + Result = testGoalState, }, GoalState = testGoalState, SupportsLibraryVersions = true, From 0b2495b64595956c5fe9af2bb8b75d8318144084 Mon Sep 17 00:00:00 2001 From: Jimmy Lewis Date: Sat, 29 Mar 2025 15:03:10 -0700 Subject: [PATCH 02/11] Make FileMappings on mock not throw --- test/LibraryManager.Mocks/LibraryInstallationState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/LibraryManager.Mocks/LibraryInstallationState.cs b/test/LibraryManager.Mocks/LibraryInstallationState.cs index b29e0ae0..59b1650c 100644 --- a/test/LibraryManager.Mocks/LibraryInstallationState.cs +++ b/test/LibraryManager.Mocks/LibraryInstallationState.cs @@ -34,6 +34,6 @@ public class LibraryInstallationState : ILibraryInstallationState public bool IsUsingDefaultProvider { get; set; } /// - public IReadOnlyList FileMappings => throw new System.NotImplementedException(); + public IReadOnlyList FileMappings { get; set; } } } From 18f1c99a8b06d11fd8b69a251ffa6efc0e3474fb Mon Sep 17 00:00:00 2001 From: Jimmy Lewis Date: Sat, 29 Mar 2025 15:03:53 -0700 Subject: [PATCH 03/11] Fix unit test expectation Now that we compute the goalstate, we do validate that all files are valid. Closes #254 --- test/LibraryManager.Test/LibraryInstallationStateTest.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/LibraryManager.Test/LibraryInstallationStateTest.cs b/test/LibraryManager.Test/LibraryInstallationStateTest.cs index f77f0bc2..f15b7881 100644 --- a/test/LibraryManager.Test/LibraryInstallationStateTest.cs +++ b/test/LibraryManager.Test/LibraryInstallationStateTest.cs @@ -157,9 +157,8 @@ public async Task IsValidAsync_State_HasUnknownLibraryFile() OperationResult result = await state.IsValidAsync(_dependencies); - // IsValidAsync does not validate library files - // Issue https://github.com/aspnet/LibraryManager/issues/254 should fix that - Assert.IsTrue(result.Success); + Assert.IsFalse(result.Success); + Assert.AreEqual("LIB018", result.Errors[0].Code); } [TestMethod] From 86cda61eb8ca057d430bf32b1bf249910b1ab5f9 Mon Sep 17 00:00:00 2001 From: Jimmy Lewis Date: Tue, 1 Apr 2025 23:47:41 -0700 Subject: [PATCH 04/11] Refactor computing goal state and errors to be extensible This allows FileSystemProvider to handle its special cases (i.e. rename support for a single file). FileSystemProvider did not get support for fileMappings prior to this. This will enable it to support that feature as well. --- src/LibraryManager/Providers/BaseProvider.cs | 54 +++++++++++++------ .../FileSystem/FileSystemProvider.cs | 21 ++++++++ 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/LibraryManager/Providers/BaseProvider.cs b/src/LibraryManager/Providers/BaseProvider.cs index 2178c582..bb1a9dc5 100644 --- a/src/LibraryManager/Providers/BaseProvider.cs +++ b/src/LibraryManager/Providers/BaseProvider.cs @@ -165,6 +165,13 @@ public async Task> GetInstallation #endregion + /// + /// Generates the goal state for library installation based on the desired state and library information. + /// + /// Specifies the target state for the library installation, including file mappings and destination paths. + /// Represents the library from which files are being installed, containing file information and validation + /// methods. + /// Returns an operation result containing the goal state or errors encountered during the generation process. private OperationResult GenerateGoalState(ILibraryInstallationState desiredState, ILibrary library) { var mappings = new List(desiredState.FileMappings ?? []); @@ -212,22 +219,19 @@ private OperationResult GenerateGoalState(ILibrary fileFilters = fileFilters.Select(f => $"{mappingRoot}/{f}").ToList(); } - List outFiles = FileGlobbingUtility.ExpandFileGlobs(fileFilters, library.Files.Keys).ToList(); + List filteredFiles = FileGlobbingUtility.ExpandFileGlobs(fileFilters, library.Files.Keys).ToList(); - if (library.GetInvalidFiles(outFiles) is IReadOnlyList invalidFiles - && invalidFiles.Count > 0) + if (library.GetInvalidFiles(filteredFiles) is IReadOnlyList { Count: > 0 } invalidFiles) { errors ??= []; errors.Add(PredefinedErrors.InvalidFilesInLibrary(desiredState.Name, invalidFiles, library.Files.Keys)); + filteredFiles.RemoveAll(file => invalidFiles.Contains(file)); } - foreach (string outFile in outFiles) - { - // strip the source prefix - string relativeOutFile = mappingRoot.Length > 0 ? outFile.Substring(mappingRoot.Length + 1) : outFile; - string destinationFile = Path.Combine(HostInteraction.WorkingDirectory, destination, relativeOutFile); - destinationFile = FileHelpers.NormalizePath(destinationFile); + Dictionary fileMappings = GetFileMappings(library, filteredFiles, mappingRoot, destination, desiredState, errors); + foreach ((string destinationFile, string sourceFile) in fileMappings) + { if (!FileHelpers.IsUnderRootDirectory(destinationFile, HostInteraction.WorkingDirectory)) { errors ??= []; @@ -235,10 +239,6 @@ private OperationResult GenerateGoalState(ILibrary continue; } - // include the cache folder in the path - string sourceFile = GetCachedFileLocalPath(desiredState, outFile); - sourceFile = FileHelpers.NormalizePath(sourceFile); - // map destination back to the library-relative file it originated from if (installFiles.ContainsKey(destinationFile)) { @@ -248,10 +248,8 @@ private OperationResult GenerateGoalState(ILibrary errors.Add(PredefinedErrors.LibraryCannotBeInstalledDueToConflicts(destinationFile, [libraryId])); continue; } - else - { - installFiles.Add(destinationFile, sourceFile); - } + + installFiles.Add(destinationFile, sourceFile); } } @@ -264,6 +262,28 @@ private OperationResult GenerateGoalState(ILibrary return OperationResult.FromSuccess(goalState); } + + protected virtual Dictionary GetFileMappings(ILibrary library, IReadOnlyList libraryFiles, string mappingRoot, string destination, ILibraryInstallationState desiredState, List errors) + { + Dictionary installFiles = new(StringComparer.OrdinalIgnoreCase); + + foreach (string file in libraryFiles) + { + // strip the source prefix + string relativeOutFile = mappingRoot.Length > 0 ? file.Substring(mappingRoot.Length + 1) : file; + string destinationFile = Path.Combine(HostInteraction.WorkingDirectory, destination, relativeOutFile); + destinationFile = FileHelpers.NormalizePath(destinationFile); + + // include the cache folder in the path + string sourceFile = GetCachedFileLocalPath(desiredState, file); + sourceFile = FileHelpers.NormalizePath(sourceFile); + + installFiles.Add(destinationFile, sourceFile); + } + + return installFiles; + } + public bool IsSourceCacheReady(LibraryInstallationGoalState goalState) { foreach (KeyValuePair item in goalState.InstalledFiles) diff --git a/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs b/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs index 2084e7cf..6d480f19 100644 --- a/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs +++ b/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; @@ -221,5 +222,25 @@ protected override string GetCachedFileLocalPath(ILibraryInstallationState state // be handled elsewhere. return Path.Combine(state.Name, sourceFile); } + + /// + protected override Dictionary GetFileMappings(ILibrary library, IReadOnlyList fileFilters, string mappingRoot, string destination, ILibraryInstallationState desiredState, List errors) + { + Dictionary fileMappings = new(); + // Handle single-file edge cases for FileSystem + if (library.Files.Count == 1 + && fileFilters.Count == 1 + && GetCachedFileLocalPath(desiredState, library.Files.Keys.First()) == library.Name) + { + // direct 1:1 file mapping, allowing file rename + string destinationFile = Path.Combine(HostInteraction.WorkingDirectory, destination, fileFilters[0]); + destinationFile = FileHelpers.NormalizePath(destinationFile); + + fileMappings.Add(destinationFile, library.Files.Keys.First()); + return fileMappings; + } + + return base.GetFileMappings(library, fileFilters, mappingRoot, destination, desiredState, errors); + } } } From 644c2b95df2df8c327b8f393dc16c2b8aca505b8 Mon Sep 17 00:00:00 2001 From: Jimmy Lewis Date: Tue, 1 Apr 2025 23:49:40 -0700 Subject: [PATCH 05/11] Fix FileSystemProvider validation during rename Because FileSystemProvider allows specifying a file as the library and a different file name as the destination (i.e. rename scenario), it was failing during goal state validation. However, in the FSP case, this is always valid - when a single file is specified, it can be mapped to one output file of any name. --- src/LibraryManager/Helpers/Extensions.cs | 21 +++++++++++++++++++ .../FileSystem/FileSystemProvider.cs | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/LibraryManager/Helpers/Extensions.cs b/src/LibraryManager/Helpers/Extensions.cs index 96b74396..6383babc 100644 --- a/src/LibraryManager/Helpers/Extensions.cs +++ b/src/LibraryManager/Helpers/Extensions.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Web.LibraryManager.Contracts; +using Microsoft.Web.LibraryManager.Providers.FileSystem; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -108,6 +109,16 @@ public static IReadOnlyList GetInvalidFiles(this ILibrary library, IRead var invalidFiles = new List(); + if (library.ProviderId == FileSystemProvider.IdText + && library.Files.Count == 1) + { + // Ideally this would be in the FileSystemLibrary, but this is an extension method. + // For FileSystem provider, if a single file is specified, it is always valid. + // It can be mapped to a different file name at the destination. + // This is valid for FileSystem but not for any other provider. + return invalidFiles; + } + if (files == null || !files.Any()) { return invalidFiles; @@ -172,5 +183,15 @@ public static string GetJObjectMemberStringValue(this JObject jObject, string pr return propertyValue; } +#if NETFRAMEWORK + /// + /// Destructs the KeyValuePair into separate values for key and value. + /// + public static void Deconstruct(this KeyValuePair pair, out T1 key, out T2 value) + { + key = pair.Key; + value = pair.Value; + } +#endif } } diff --git a/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs b/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs index 6d480f19..d2b4b6a3 100644 --- a/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs +++ b/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs @@ -17,6 +17,7 @@ namespace Microsoft.Web.LibraryManager.Providers.FileSystem /// Internal use only internal sealed class FileSystemProvider : BaseProvider { + public const string IdText = "filesystem"; private FileSystemCatalog _catalog; /// Internal use only @@ -28,7 +29,7 @@ public FileSystemProvider(IHostInteraction hostInteraction) /// /// The unique identifier of the provider. /// - public override string Id => "filesystem"; + public override string Id => IdText; /// /// Hint text for the library id. From b56ad61cbc61eddb8ee172528b2ceb6741ef8aa3 Mon Sep 17 00:00:00 2001 From: Jimmy Lewis Date: Tue, 1 Apr 2025 23:53:55 -0700 Subject: [PATCH 06/11] make tests more readable with raw strings --- test/libman.Test/LibmanInstallTest.cs | 205 ++++++++++++++------------ 1 file changed, 113 insertions(+), 92 deletions(-) diff --git a/test/libman.Test/LibmanInstallTest.cs b/test/libman.Test/LibmanInstallTest.cs index 79815c82..ccf98946 100644 --- a/test/libman.Test/LibmanInstallTest.cs +++ b/test/libman.Test/LibmanInstallTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.IO; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -35,16 +34,18 @@ public void TestInstall_CleanDirectory() Assert.IsTrue(File.Exists(Path.Combine(WorkingDir, "libman.json"))); string text = File.ReadAllText(Path.Combine(WorkingDir, "libman.json")); - string expectedText = @"{ - ""version"": ""3.0"", - ""defaultProvider"": ""cdnjs"", - ""libraries"": [ - { - ""library"": ""jquery@3.2.1"", - ""destination"": ""wwwroot"" - } - ] -}"; + string expectedText = """ + { + "version": "3.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "jquery@3.2.1", + "destination": "wwwroot" + } + ] + } + """; Assert.AreEqual(StringHelper.NormalizeNewLines(expectedText), StringHelper.NormalizeNewLines(text)); } @@ -63,16 +64,18 @@ public void TestInstall_CleanDirectory_WithPromptForProvider() Assert.IsTrue(File.Exists(Path.Combine(WorkingDir, "libman.json"))); string text = File.ReadAllText(Path.Combine(WorkingDir, "libman.json")); - string expectedText = @"{ - ""version"": ""3.0"", - ""defaultProvider"": ""cdnjs"", - ""libraries"": [ - { - ""library"": ""jquery@3.2.1"", - ""destination"": ""wwwroot"" - } - ] -}"; + string expectedText = """ + { + "version": "3.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "jquery@3.2.1", + "destination": "wwwroot" + } + ] + } + """; Assert.AreEqual(StringHelper.NormalizeNewLines(expectedText), StringHelper.NormalizeNewLines(text)); } @@ -83,11 +86,13 @@ public void TestInstall_ExistingLibman_WithPromptForProvider() testInputReader.Inputs.Add("ProviderId", "cdnjs"); - string initialContent = @"{ - ""version"": ""1.0"", - ""libraries"": [ - ] -}"; + string initialContent = """ + { + "version": "1.0", + "libraries": [ + ] + } + """; File.WriteAllText(Path.Combine(WorkingDir, "libman.json"), initialContent); var command = new InstallCommand(HostEnvironment); @@ -98,16 +103,18 @@ public void TestInstall_ExistingLibman_WithPromptForProvider() Assert.IsTrue(File.Exists(Path.Combine(WorkingDir, "libman.json"))); string text = File.ReadAllText(Path.Combine(WorkingDir, "libman.json")); - string expectedText = @"{ - ""version"": ""1.0"", - ""defaultProvider"": ""cdnjs"", - ""libraries"": [ - { - ""library"": ""jquery@3.2.1"", - ""destination"": ""wwwroot"" - } - ] -}"; + string expectedText = """ + { + "version": "1.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "jquery@3.2.1", + "destination": "wwwroot" + } + ] + } + """; Assert.AreEqual(StringHelper.NormalizeNewLines(expectedText), StringHelper.NormalizeNewLines(text)); } @@ -118,13 +125,15 @@ public void TestInstall_WithExistingLibmanJson() var command = new InstallCommand(HostEnvironment); command.Configure(null); - string initialContent = @"{ - ""version"": ""1.0"", - ""defaultProvider"": ""cdnjs"", - ""defaultDestination"": ""wwwroot"", - ""libraries"": [ - ] -}"; + string initialContent = """ + { + "version": "1.0", + "defaultProvider": "cdnjs", + "defaultDestination": "wwwroot", + "libraries": [ + ] + } + """; File.WriteAllText(Path.Combine(WorkingDir, "libman.json"), initialContent); @@ -133,16 +142,18 @@ public void TestInstall_WithExistingLibmanJson() Assert.IsTrue(File.Exists(Path.Combine(WorkingDir, "libman.json"))); string actualText = File.ReadAllText(Path.Combine(WorkingDir, "libman.json")); - string expectedText = @"{ - ""version"": ""1.0"", - ""defaultProvider"": ""cdnjs"", - ""defaultDestination"": ""wwwroot"", - ""libraries"": [ - { - ""library"": ""jquery@3.2.1"" - } - ] -}"; + string expectedText = """ + { + "version": "1.0", + "defaultProvider": "cdnjs", + "defaultDestination": "wwwroot", + "libraries": [ + { + "library": "jquery@3.2.1" + } + ] + } + """; Assert.AreEqual(StringHelper.NormalizeNewLines(expectedText), StringHelper.NormalizeNewLines(actualText)); } @@ -152,16 +163,18 @@ public void TestInstall_Duplicate() var command = new InstallCommand(HostEnvironment); command.Configure(null); - string initialContent = @"{ - ""version"": ""1.0"", - ""defaultProvider"": ""cdnjs"", - ""defaultDestination"": ""wwwroot"", - ""libraries"": [ - { - ""library"": ""jquery@3.2.1"" - } - ] -}"; + string initialContent = """ + { + "version": "1.0", + "defaultProvider": "cdnjs", + "defaultDestination": "wwwroot", + "libraries": [ + { + "library": "jquery@3.2.1" + } + ] + } + """; File.WriteAllText(Path.Combine(WorkingDir, "libman.json"), initialContent); @@ -177,13 +190,15 @@ public void TestInstall_WithExistingLibmanJson_SpecificFiles() var command = new InstallCommand(HostEnvironment); command.Configure(null); - string initialContent = @"{ - ""version"": ""1.0"", - ""defaultProvider"": ""cdnjs"", - ""defaultDestination"": ""wwwroot"", - ""libraries"": [ - ] -}"; + string initialContent = """ + { + "version": "1.0", + "defaultProvider": "cdnjs", + "defaultDestination": "wwwroot", + "libraries": [ + ] + } + """; File.WriteAllText(Path.Combine(WorkingDir, "libman.json"), initialContent); command.Execute("jquery@3.2.1", "--files", "jquery.min.js"); @@ -191,19 +206,21 @@ public void TestInstall_WithExistingLibmanJson_SpecificFiles() Assert.IsTrue(File.Exists(Path.Combine(WorkingDir, "libman.json"))); string actualText = File.ReadAllText(Path.Combine(WorkingDir, "libman.json")); - string expectedText = @"{ - ""version"": ""1.0"", - ""defaultProvider"": ""cdnjs"", - ""defaultDestination"": ""wwwroot"", - ""libraries"": [ - { - ""library"": ""jquery@3.2.1"", - ""files"": [ - ""jquery.min.js"" - ] - } - ] -}"; + string expectedText = """ + { + "version": "1.0", + "defaultProvider": "cdnjs", + "defaultDestination": "wwwroot", + "libraries": [ + { + "library": "jquery@3.2.1", + "files": [ + "jquery.min.js" + ] + } + ] + } + """; Assert.AreEqual(StringHelper.NormalizeNewLines(expectedText), StringHelper.NormalizeNewLines(actualText)); } @@ -213,18 +230,22 @@ public void TestInstall_WithInvalidFiles() var command = new InstallCommand(HostEnvironment); command.Configure(null); - string initialContent = @"{ - ""version"": ""1.0"", - ""defaultProvider"": ""cdnjs"", - ""defaultDestination"": ""wwwroot"", - ""libraries"": [ - ] -}"; + string initialContent = """ + { + "version": "1.0", + "defaultProvider": "cdnjs", + "defaultDestination": "wwwroot", + "libraries": [ + ] + } + """; File.WriteAllText(Path.Combine(WorkingDir, "libman.json"), initialContent); command.Execute("jquery@3.5.0", "--files", "abc.js"); - string expectedMessage = @"[LIB018]: ""jquery@3.5.0"" does not contain the following: abc.js -Valid files are jquery.js, jquery.min.js, jquery.min.map, jquery.slim.js, jquery.slim.min.js, jquery.slim.min.map"; + string expectedMessage = """ + [LIB018]: "jquery@3.5.0" does not contain the following: abc.js + Valid files are jquery.js, jquery.min.js, jquery.min.map, jquery.slim.js, jquery.slim.min.js, jquery.slim.min.map + """; var logger = HostEnvironment.Logger as TestLogger; Assert.AreEqual(expectedMessage, logger.Messages.Last().Value); From db64e7085a960e297845e37fbece6a3ed933b954 Mon Sep 17 00:00:00 2001 From: Jimmy Lewis Date: Tue, 1 Apr 2025 23:55:32 -0700 Subject: [PATCH 07/11] fixup failing tests - file conflicts are now the full path, since the goal state is fully resolved. - added a helper to make it easier to read test output when errors don't match. --- .../LibrariesValidatorTest.cs | 15 ++++----- .../TestUtilities/AssertExtensions.cs | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 test/LibraryManager.Test/TestUtilities/AssertExtensions.cs diff --git a/test/LibraryManager.Test/LibrariesValidatorTest.cs b/test/LibraryManager.Test/LibrariesValidatorTest.cs index e9437028..793b105c 100644 --- a/test/LibraryManager.Test/LibrariesValidatorTest.cs +++ b/test/LibraryManager.Test/LibrariesValidatorTest.cs @@ -13,8 +13,9 @@ using Microsoft.Web.LibraryManager.Mocks; using Microsoft.Web.LibraryManager.Providers.Cdnjs; using Microsoft.Web.LibraryManager.Providers.FileSystem; -using Microsoft.Web.LibraryManager.Providers.Unpkg; using Microsoft.Web.LibraryManager.Providers.jsDelivr; +using Microsoft.Web.LibraryManager.Providers.Unpkg; +using Microsoft.Web.LibraryManager.Test.TestUtilities; using Moq; namespace Microsoft.Web.LibraryManager.Test @@ -44,8 +45,9 @@ public void Setup() [TestMethod] public async Task DetectConflictsAsync_ConflictingFiles_SameDestination() { + string conflictFilePath = Path.Combine(_dependencies.GetHostInteractions().WorkingDirectory, "lib", "package.json"); string expectedErrorCode = "LIB016"; - string expectedErrorMessage = "Conflicting file \"lib\\package.json\" found in more than one library: jquery, d3"; + string expectedErrorMessage = $"Conflicting file \"{conflictFilePath}\" found in more than one library: jquery, d3"; var manifest = Manifest.FromJson(_docDifferentLibraries_SameFiles_SameLocation, _dependencies); IEnumerable> conflicts = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); @@ -92,11 +94,10 @@ public async Task DetectConflictsAsync_SameLibrary_DifferentProviders() IEnumerable> results = await LibrariesValidator.GetManifestErrorsAsync(manifest, _dependencies, CancellationToken.None); - var conflictsList = results.ToList(); + var conflictsList = results.Where(r => r.Errors?.Count > 0).ToList(); Assert.AreEqual(1, conflictsList.Count); - Assert.IsTrue(conflictsList[0].Errors.Count == 1); - Assert.AreEqual(conflictsList[0].Errors[0].Code, expectedErrorCode); - Assert.AreEqual(conflictsList[0].Errors[0].Message, expectedErrorMessage); + List expectedErrors = [new Contracts.Error(expectedErrorCode, expectedErrorMessage)]; + Assert.That.ErrorsEqual(expectedErrors, conflictsList[0].Errors); } [TestMethod] @@ -244,7 +245,7 @@ public async Task GetLibrariesErrors_LibrariesNoProvider() ""{ManifestConstants.Library}"": ""jquery@3.1.1"", ""{ManifestConstants.Provider}"": ""unpkg"", ""{ManifestConstants.Destination}"": ""lib2"", - ""{ManifestConstants.Files}"": [ ""jquery.js"" ] + ""{ManifestConstants.Files}"": [ ""dist/jquery.js"" ] }}, ] }} diff --git a/test/LibraryManager.Test/TestUtilities/AssertExtensions.cs b/test/LibraryManager.Test/TestUtilities/AssertExtensions.cs new file mode 100644 index 00000000..e3533198 --- /dev/null +++ b/test/LibraryManager.Test/TestUtilities/AssertExtensions.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Web.LibraryManager.Contracts; + +namespace Microsoft.Web.LibraryManager.Test.TestUtilities; + +public static class AssertExtensions +{ + public static void ErrorsEqual(this Assert assert, IList expected, IList actual) + { + string BuildString(IList errors) + { + StringBuilder stringBuilder = new StringBuilder(); + foreach (IError error in errors) + { + stringBuilder.AppendLine($"{error.Code}: {error.Message}"); + } + return stringBuilder.ToString(); + } + + Assert.AreEqual(BuildString(expected), BuildString(actual)); + } +} From b0a1bbb9f88990f10dca6efd66f3d6d761e5b872 Mon Sep 17 00:00:00 2001 From: Jimmy Lewis Date: Wed, 2 Apr 2025 01:11:18 -0700 Subject: [PATCH 08/11] fix error message to match test expectation Invalid files were previously caught on a different code path, so this makes the behavior match. --- src/LibraryManager/Providers/BaseProvider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/LibraryManager/Providers/BaseProvider.cs b/src/LibraryManager/Providers/BaseProvider.cs index bb1a9dc5..68d3806e 100644 --- a/src/LibraryManager/Providers/BaseProvider.cs +++ b/src/LibraryManager/Providers/BaseProvider.cs @@ -224,7 +224,8 @@ private OperationResult GenerateGoalState(ILibrary if (library.GetInvalidFiles(filteredFiles) is IReadOnlyList { Count: > 0 } invalidFiles) { errors ??= []; - errors.Add(PredefinedErrors.InvalidFilesInLibrary(desiredState.Name, invalidFiles, library.Files.Keys)); + string libraryId = LibraryNamingScheme.GetLibraryId(desiredState.Name, desiredState.Version); + errors.Add(PredefinedErrors.InvalidFilesInLibrary(libraryId, invalidFiles, library.Files.Keys)); filteredFiles.RemoveAll(file => invalidFiles.Contains(file)); } From 86329c34cdeadef1edbc7d1b7190442764eb888e Mon Sep 17 00:00:00 2001 From: Jimmy Lewis Date: Thu, 3 Apr 2025 01:17:40 -0700 Subject: [PATCH 09/11] Fix another case better solved via GoalState --- .../FileSystem/FileSystemProvider.cs | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs b/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs index d2b4b6a3..e9305a7c 100644 --- a/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs +++ b/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Web.LibraryManager.Contracts; +using Microsoft.Web.LibraryManager.Helpers; using Microsoft.Web.LibraryManager.LibraryNaming; using Microsoft.Web.LibraryManager.Resources; @@ -73,25 +74,25 @@ public override async Task> Instal return goalStateResult; } - foreach (string file in desiredState.Files) + foreach ((string destFile, string sourceFile) in goalStateResult.Result.InstalledFiles) { if (cancellationToken.IsCancellationRequested) { return OperationResult.FromCancelled(goalStateResult.Result); } - if (string.IsNullOrEmpty(file)) + if (string.IsNullOrEmpty(destFile)) { - return OperationResult.FromError(PredefinedErrors.CouldNotWriteFile(file)); + return OperationResult.FromError(PredefinedErrors.CouldNotWriteFile(destFile)); } - string path = Path.Combine(desiredState.DestinationPath, file); - var sourceStream = new Func(() => GetStreamAsync(desiredState, file, cancellationToken).Result); - bool writeOk = await HostInteraction.WriteFileAsync(path, sourceStream, desiredState, cancellationToken).ConfigureAwait(false); + string libraryName = LibraryNamingScheme.GetLibraryId(desiredState.Name, desiredState.Version); + var sourceStream = new Func(() => GetStreamAsync(sourceFile, libraryName, cancellationToken).Result); + bool writeOk = await HostInteraction.WriteFileAsync(destFile, sourceStream, desiredState, cancellationToken).ConfigureAwait(false); if (!writeOk) { - return OperationResult.FromError(PredefinedErrors.CouldNotWriteFile(file)); + return OperationResult.FromError(PredefinedErrors.CouldNotWriteFile(destFile)); } } @@ -135,10 +136,8 @@ public override string GetSuggestedDestination(ILibrary library) return string.Empty; } - private async Task GetStreamAsync(ILibraryInstallationState state, string file, CancellationToken cancellationToken) + private async Task GetStreamAsync(string sourceFile, string libraryName, CancellationToken cancellationToken) { - string sourceFile = state.Name; - try { if (!Uri.TryCreate(sourceFile, UriKind.RelativeOrAbsolute, out Uri url)) @@ -154,14 +153,7 @@ private async Task GetStreamAsync(ILibraryInstallationState state, strin // File if (url.IsFile) { - if (Directory.Exists(url.OriginalString)) - { - return await FileHelpers.ReadFileAsStreamAsync(Path.Combine(url.OriginalString, file), cancellationToken).ConfigureAwait(false); - } - else - { - return await FileHelpers.ReadFileAsStreamAsync(sourceFile, cancellationToken).ConfigureAwait(false); - } + return await FileHelpers.ReadFileAsStreamAsync(sourceFile, cancellationToken).ConfigureAwait(false); } // Url else @@ -175,7 +167,7 @@ private async Task GetStreamAsync(ILibraryInstallationState state, strin } catch (Exception) { - throw new InvalidLibraryException(state.Name, state.ProviderId); + throw new InvalidLibraryException(libraryName, Id); } } From 9dd98cfd377942c1444f7bea5e46a821f8ff6f1e Mon Sep 17 00:00:00 2001 From: Jimmy Lewis Date: Fri, 4 Apr 2025 16:21:26 -0700 Subject: [PATCH 10/11] Add a restore integration tests for FileSystemProvider Coverage includes fileMappings and the rename case (library name is a file, files list includes a different name). --- test/libman.IntegrationTest/CliBaseTest.cs | 23 ++++++++ test/libman.IntegrationTest/RestoreTests.cs | 58 +++++++++++++++++++ .../TestFiles/EmptyFile.css | 0 .../TestFiles/EmptyFile.js | 0 .../TestFiles/EmptyFile.min.css | 0 .../TestFiles/EmptyFile.min.js | 0 .../libman.IntegrationTest.csproj | 11 ++++ 7 files changed, 92 insertions(+) create mode 100644 test/libman.IntegrationTest/TestFiles/EmptyFile.css create mode 100644 test/libman.IntegrationTest/TestFiles/EmptyFile.js create mode 100644 test/libman.IntegrationTest/TestFiles/EmptyFile.min.css create mode 100644 test/libman.IntegrationTest/TestFiles/EmptyFile.min.js diff --git a/test/libman.IntegrationTest/CliBaseTest.cs b/test/libman.IntegrationTest/CliBaseTest.cs index 94c9736a..29983ea6 100644 --- a/test/libman.IntegrationTest/CliBaseTest.cs +++ b/test/libman.IntegrationTest/CliBaseTest.cs @@ -2,14 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading.Tasks; namespace Microsoft.Web.LibraryManager.Cli.IntegrationTest; [TestClass] [DeploymentItem(@"TestPackages", "TestPackages")] +[DeploymentItem("TestFiles", "TestFiles")] public class CliTestBase { private const string CliPackageName = "Microsoft.Web.LibraryManager.Cli"; @@ -117,4 +120,24 @@ protected void AssertFileExists(string relativeFilePath) string filePath = Path.Combine(_testDirectory, relativeFilePath); Assert.IsTrue(File.Exists(filePath), $"Expected file '{relativeFilePath}' does not exist."); } + + protected void AssertDirectoryContents(string directoryPath, IEnumerable expectedFiles, bool failOnExtraFiles = false) + { + string fullPath = Path.Combine(_testDirectory, directoryPath); + Assert.IsTrue(Directory.Exists(fullPath), $"Expected directory '{directoryPath}' does not exist."); + HashSet actualFiles = Directory.GetFiles(fullPath, "*", SearchOption.AllDirectories) + .Select(file => Path.GetRelativePath(fullPath, file)) + .ToHashSet(); + + foreach (string file in expectedFiles) + { + Assert.IsTrue(actualFiles.Contains(file), $"Directory contents do not match. Expected: {string.Join(", ", expectedFiles)}. Actual: {string.Join(", ", actualFiles)}"); + } + + if (failOnExtraFiles) + { + List extraFiles = actualFiles.Except(expectedFiles).Order().ToList(); + Assert.IsFalse(extraFiles.Any(), $"Unexpected files found in directory '{directoryPath}': {string.Join(", ", extraFiles)}"); + } + } } diff --git a/test/libman.IntegrationTest/RestoreTests.cs b/test/libman.IntegrationTest/RestoreTests.cs index a2274dcc..321e80f3 100644 --- a/test/libman.IntegrationTest/RestoreTests.cs +++ b/test/libman.IntegrationTest/RestoreTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.IO; using System.Threading.Tasks; namespace Microsoft.Web.LibraryManager.Cli.IntegrationTest; @@ -99,4 +101,60 @@ public async Task Restore_WithFileMapping_FileGlobs_SetRootPath() AssertFileExists("wwwroot/lib/jquery/jquery.min.js"); AssertFileExists("wwwroot/lib/jquery/jquery.min.map"); } + + [TestMethod] + public async Task Restore_FileSystemProvider_WithFileMapping() + { + string testFilesPath = Path.Combine(Environment.CurrentDirectory, "TestFiles"); + string manifest = $$""" + { + "version": "3.0", + "libraries": [ + { + "provider": "filesystem", + "library": "{{testFilesPath.Replace('\\', '/')}}", + "destination": "wwwroot/lib/testFiles", + "fileMappings": [ + { + "files": [ + "*.min.js", + ] + } + ] + } + ] + } + """; + await CreateManifestFileAsync(manifest); + + await ExecuteCliToolAsync("restore"); + + AssertDirectoryContents("wwwroot/lib/testFiles/", ["EmptyFile.min.js"], failOnExtraFiles: true); + } + + [TestMethod] + public async Task Restore_FileSystemProvider_WithFileRename() + { + string testFilesPath = Path.Combine(Environment.CurrentDirectory, "TestFiles"); + string manifest = $$""" + { + "version": "3.0", + "libraries": [ + { + "provider": "filesystem", + "library": "{{testFilesPath.Replace('\\', '/')}}/EmptyFile.min.js", + "destination": "wwwroot/lib/testFiles", + "files": [ + "TheOnlyFile.js", + ] + } + ] + } + """; + await CreateManifestFileAsync(manifest); + + await ExecuteCliToolAsync("restore"); + + AssertDirectoryContents("wwwroot/lib/testFiles/", ["TheOnlyFile.js"], failOnExtraFiles: true); + } } diff --git a/test/libman.IntegrationTest/TestFiles/EmptyFile.css b/test/libman.IntegrationTest/TestFiles/EmptyFile.css new file mode 100644 index 00000000..e69de29b diff --git a/test/libman.IntegrationTest/TestFiles/EmptyFile.js b/test/libman.IntegrationTest/TestFiles/EmptyFile.js new file mode 100644 index 00000000..e69de29b diff --git a/test/libman.IntegrationTest/TestFiles/EmptyFile.min.css b/test/libman.IntegrationTest/TestFiles/EmptyFile.min.css new file mode 100644 index 00000000..e69de29b diff --git a/test/libman.IntegrationTest/TestFiles/EmptyFile.min.js b/test/libman.IntegrationTest/TestFiles/EmptyFile.min.js new file mode 100644 index 00000000..e69de29b diff --git a/test/libman.IntegrationTest/libman.IntegrationTest.csproj b/test/libman.IntegrationTest/libman.IntegrationTest.csproj index 09d98f59..8a153595 100644 --- a/test/libman.IntegrationTest/libman.IntegrationTest.csproj +++ b/test/libman.IntegrationTest/libman.IntegrationTest.csproj @@ -20,6 +20,17 @@ + + + + + + + + PreserveNewest + + + From 5e58a14d7702b2cd9d3cc7fecb4c9c5693d3c882 Mon Sep 17 00:00:00 2001 From: Jimmy Lewis Date: Thu, 10 Apr 2025 00:53:30 -0700 Subject: [PATCH 11/11] Improve how FileSystemProvider handles fileMappings * Remove hack using GetCachedFileLocalPath to determine if the library name is a file path. Added a new helper method that checks this directly, handling HTTP URLs, relative paths, and absolute paths. * GetCachedFileLocalPath now returns rooted (absolute) paths to the cached file. This avoids ambiguity about the working directory. --- .../FileSystem/FileSystemProvider.cs | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs b/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs index e9305a7c..ca2e669e 100644 --- a/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs +++ b/src/LibraryManager/Providers/FileSystem/FileSystemProvider.cs @@ -206,13 +206,22 @@ protected override string GetCachedFileLocalPath(ILibraryInstallationState state // For other filesystem libraries, the state.Name may be a either a file or folder // TODO: abstract file system - if (File.Exists(state.Name)) + (bool isFile, string resolvedFilePath) = LibraryNameIsFile(state.Name); + + if (isFile) { - return state.Name; + return resolvedFilePath; } // as a fallback, assume state.Name is a directory. If this path doesn't exist, it will // be handled elsewhere. + + // root relative paths to the libman working directory + if (!Path.IsPathRooted(state.Name)) + { + return Path.GetFullPath(Path.Combine(HostInteraction.WorkingDirectory, state.Name, sourceFile)); + } + return Path.Combine(state.Name, sourceFile); } @@ -221,19 +230,41 @@ protected override Dictionary GetFileMappings(ILibrary library, { Dictionary fileMappings = new(); // Handle single-file edge cases for FileSystem - if (library.Files.Count == 1 - && fileFilters.Count == 1 - && GetCachedFileLocalPath(desiredState, library.Files.Keys.First()) == library.Name) + (bool librarySpecifiedIsFile, string resolvedFilePath) = LibraryNameIsFile(library.Name); + if (librarySpecifiedIsFile && fileFilters.Count == 1) { // direct 1:1 file mapping, allowing file rename string destinationFile = Path.Combine(HostInteraction.WorkingDirectory, destination, fileFilters[0]); destinationFile = FileHelpers.NormalizePath(destinationFile); - fileMappings.Add(destinationFile, library.Files.Keys.First()); + // the library specified is a single file, so use that as the source directly + fileMappings.Add(destinationFile, resolvedFilePath); return fileMappings; } return base.GetFileMappings(library, fileFilters, mappingRoot, destination, desiredState, errors); } + + /// + /// Checks if a specified library name corresponds to an existing file and returns the result along with the + /// file path. + /// + /// The name of the library being checked for existence as a file. + /// A tuple containing a boolean indicating if the file exists and the resolved file path. + private (bool, string) LibraryNameIsFile(string libraryName) + { + string filePath = libraryName; + if (FileHelpers.IsHttpUri(filePath)) + { + return (true, filePath); + } + + if (!Path.IsPathRooted(filePath)) + { + filePath = Path.Combine(HostInteraction.WorkingDirectory, filePath); + } + + return (File.Exists(filePath), filePath); + } } }