diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Update-TypeData.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Update-TypeData.cs index b73d8570040..559e5561354 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Update-TypeData.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Update-TypeData.cs @@ -788,7 +788,7 @@ private void ProcessTypeFiles() for (int i = prependPathTotal.Count - 1; i >= 0; i--) { string formattedTarget = string.Format(CultureInfo.InvariantCulture, target, prependPathTotal[i]); - string resolvedPath = ModuleCmdletBase.ResolveRootedFilePath(prependPathTotal[i], Context) ?? prependPathTotal[i]; + string resolvedPath = ModuleCmdletBase.ResolveToFileSystemPathIfRooted(prependPathTotal[i], Context) ?? prependPathTotal[i]; if (ShouldProcess(formattedTarget, action)) { @@ -804,7 +804,7 @@ private void ProcessTypeFiles() { if (entry.FileName != null) { - string resolvedPath = ModuleCmdletBase.ResolveRootedFilePath(entry.FileName, Context) ?? entry.FileName; + string resolvedPath = ModuleCmdletBase.ResolveToFileSystemPathIfRooted(entry.FileName, Context) ?? entry.FileName; if (fullFileNameHash.Add(resolvedPath)) { newTypes.Add(entry); @@ -819,7 +819,7 @@ private void ProcessTypeFiles() foreach (string appendPathTotalItem in appendPathTotal) { string formattedTarget = string.Format(CultureInfo.InvariantCulture, target, appendPathTotalItem); - string resolvedPath = ModuleCmdletBase.ResolveRootedFilePath(appendPathTotalItem, Context) ?? appendPathTotalItem; + string resolvedPath = ModuleCmdletBase.ResolveToFileSystemPathIfRooted(appendPathTotalItem, Context) ?? appendPathTotalItem; if (ShouldProcess(formattedTarget, action)) { @@ -1135,7 +1135,7 @@ protected override void ProcessRecord() // Resolving the file path because the path to the types file in module manifest is now specified as // ..\..\types.ps1xml which expands to C:\Windows\System32\WindowsPowerShell\v1.0\Modules\Microsoft.PowerShell.Core\..\..\types.ps1xml - fileName = ModuleCmdletBase.ResolveRootedFilePath(fileName, Context) ?? fileName; + fileName = ModuleCmdletBase.ResolveToFileSystemPathIfRooted(fileName, Context) ?? fileName; ConstructFileToIndexMap(fileName, index, fileToIndexMap); } } diff --git a/src/System.Management.Automation/engine/InitialSessionState.cs b/src/System.Management.Automation/engine/InitialSessionState.cs index a6d50f47443..a0f20a26ebe 100644 --- a/src/System.Management.Automation/engine/InitialSessionState.cs +++ b/src/System.Management.Automation/engine/InitialSessionState.cs @@ -3494,7 +3494,7 @@ internal static void RemoveTypesAndFormats(ExecutionContext context, IList resolvedTypeFilesToRemove = new List(); foreach (var typeFile in typeFilesToRemove) { - resolvedTypeFilesToRemove.Add(ModuleCmdletBase.ResolveRootedFilePath(typeFile, context) ?? typeFile); + resolvedTypeFilesToRemove.Add(ModuleCmdletBase.ResolveToFileSystemPathIfRooted(typeFile, context) ?? typeFile); } foreach (SessionStateTypeEntry entry in context.InitialSessionState.Types) @@ -3508,7 +3508,7 @@ internal static void RemoveTypesAndFormats(ExecutionContext context, IList GetModuleForRootedPaths(List modulePat } // Now we resolve the possible paths in case it is relative path/path contains wildcards - var modulePathCollection = GetResolvedPathCollection(modulePath, this.Context); + var modulePathCollection = ResolveToFileSystemPaths(modulePath, this.Context); if (modulePathCollection != null) { @@ -2287,7 +2287,7 @@ internal PSModuleInfo LoadModuleManifest( WriteVerbose(loadMessage); bool isAlreadyLoaded = false; - string resolvedFileName = ResolveRootedFilePath(fileName, Context) ?? fileName; + string resolvedFileName = ResolveToFileSystemPathIfRooted(fileName, Context) ?? fileName; foreach (var entry in Context.InitialSessionState.Types) { if (entry.FileName == null) @@ -2295,7 +2295,7 @@ internal PSModuleInfo LoadModuleManifest( continue; } - string resolvedEntryFileName = ResolveRootedFilePath(entry.FileName, Context) ?? entry.FileName; + string resolvedEntryFileName = ResolveToFileSystemPathIfRooted(entry.FileName, Context) ?? entry.FileName; if (resolvedEntryFileName.Equals(resolvedFileName, StringComparison.OrdinalIgnoreCase)) { isAlreadyLoaded = true; @@ -4673,8 +4673,8 @@ private string FixFileName(string moduleName, string moduleBase, string fileName // but it's safer to keep the original behavior to avoid unexpected breaking changes. string combinedPath = Path.Combine(moduleBase, fileName); string resolvedPath = IsRooted(fileName) - ? ResolveRootedFilePath(fileName, Context) ?? ResolveRootedFilePath(combinedPath, Context) - : ResolveRootedFilePath(combinedPath, Context); + ? ResolveToFileSystemPathIfRooted(fileName, Context) ?? ResolveToFileSystemPathIfRooted(combinedPath, Context) + : ResolveToFileSystemPathIfRooted(combinedPath, Context); // Return the path if successfully resolved. if (resolvedPath is not null) @@ -4758,7 +4758,7 @@ internal static bool IsRooted(string filePath) /// The filename to resolve. /// Execution context. /// The resolved filename. - internal static string ResolveRootedFilePath(string filePath, ExecutionContext context) + internal static string ResolveToFileSystemPathIfRooted(string filePath, ExecutionContext context) { // If the path is not fully qualified or relative rooted, then // we need to do path-based resolution... @@ -4767,45 +4767,23 @@ internal static string ResolveRootedFilePath(string filePath, ExecutionContext c return null; } - ProviderInfo provider = null; - Collection filePaths = null; - if (context.EngineSessionState.IsProviderLoaded(context.ProviderNames.FileSystem)) + try { - try - { - filePaths = - context.SessionState.Path.GetResolvedProviderPathFromPSPath(filePath, out provider); - } - catch (ItemNotFoundException) - { - return null; - } - - // Make sure that the path is in the file system - that's all we can handle currently... - if (!provider.NameEquals(context.ProviderNames.FileSystem)) - { - // "The current provider ({0}) cannot open a file" - throw InterpreterError.NewInterpreterException( - filePath, - typeof(RuntimeException), - errorPosition: null, - "FileOpenError", - ParserStrings.FileOpenError, - provider.FullName); - } + filePaths = ResolveToFileSystemPathsThrowing(filePath, context, allowNonExistingPaths: false); + } + catch (ItemNotFoundException) + { + // Ignore. } - // Make sure at least one file was found... if (filePaths == null || filePaths.Count < 1) { return null; } - - if (filePaths.Count > 1) + else if (filePaths.Count > 1) { - // "The path resolved to more than one file; can only process one file at a time." throw InterpreterError.NewInterpreterException( filePaths, typeof(RuntimeException), @@ -4813,82 +4791,162 @@ internal static string ResolveRootedFilePath(string filePath, ExecutionContext c "AmbiguousPath", ParserStrings.AmbiguousPath); } - - return filePaths[0]; + else + { + return filePaths[0]; + } } - internal static string GetResolvedPath(string filePath, ExecutionContext context) + /// + /// Resolves to a single file system path using the file system provider. + /// + /// + /// + /// Path resolution is considered successful if resolves to exactly + /// one file system path. + /// + /// + /// The file path to resolve. + /// The execution context. + /// The resolved, fully qualified file system path if resolution succeeded; otherwise . + internal static string ResolveToSingleFileSystemPath(string path, ExecutionContext context) { - ProviderInfo provider = null; - - Collection filePaths; + Collection paths = null; - if (context != null && context.EngineSessionState != null && context.EngineSessionState.IsProviderLoaded(context.ProviderNames.FileSystem)) + if (!TryResolveToFileSystemPaths(path, context, out paths, allowNonExistingPaths: true)) { - try + // Ported from legacy code to preserve behavior. + if (context?.EngineSessionState?.IsProviderLoaded(context.ProviderNames.FileSystem) != true) { - filePaths = context.SessionState.Path.GetResolvedProviderPathFromPSPath(filePath, true /* allowNonExistentPaths */, out provider); - } - catch (Exception) - { - return null; - } - // Make sure that the path is in the file system - that's all we can handle currently... - if ((provider == null) || !provider.NameEquals(context.ProviderNames.FileSystem)) - { - return null; + paths = [path]; } } - else - { - filePaths = new Collection(); - filePaths.Add(filePath); - } - // Make sure at least one file was found... - if (filePaths == null || filePaths.Count < 1 || filePaths.Count > 1) + if (paths?.Count != 1) { return null; } - return filePaths[0]; + return paths[0]; } - internal static Collection GetResolvedPathCollection(string filePath, ExecutionContext context) + /// + /// Resolves to file system paths using the file system provider. + /// + /// + /// + /// Path resolution is considered successful if resolves to + /// at least one file system path. + /// + /// + /// The path to resolve. + /// The execution context. + /// The resolved, fully qualified file system paths if path resolution succeeded; otherwise . + internal static Collection ResolveToFileSystemPaths(string path, ExecutionContext context) { - ProviderInfo provider = null; - - Collection filePaths; + Collection paths; - if (context != null && context.EngineSessionState != null && context.EngineSessionState.IsProviderLoaded(context.ProviderNames.FileSystem)) + if (!TryResolveToFileSystemPaths(path, context, out paths, allowNonExistingPaths: true)) { - try - { - filePaths = context.SessionState.Path.GetResolvedProviderPathFromPSPath(filePath, true /* allowNonExistentPaths */, out provider); - } - catch (Exception) + // Ported from legacy code to preserve behavior. + if (context?.EngineSessionState?.IsProviderLoaded(context.ProviderNames.FileSystem) == false) { - return null; + paths = [path]; } - // Make sure that the path is in the file system - that's all we can handle currently... - if ((provider == null) || !provider.NameEquals(context.ProviderNames.FileSystem)) + else { return null; } } - else + + if (paths == null || paths.Count < 1) { - filePaths = new Collection(); - filePaths.Add(filePath); + return null; } - // Make sure at least one file was found... - if (filePaths == null || filePaths.Count < 1) + return paths; + } + + /// + /// Resolves using the file system provider, without error handling. + /// + /// + /// + /// This method does not normalize 's directory separators. + /// + /// + /// The path to resolve. + /// The execution context. + /// Whether non-existing paths should be resolved. + /// + /// + /// if the file system provider isn't available. + /// All resolved paths if resolution of succeeded. + /// + /// + /// Thrown if path resolution is not performed by the file system provider. + internal static Collection ResolveToFileSystemPathsThrowing(string path, ExecutionContext context, bool allowNonExistingPaths = false) + { + if (context?.EngineSessionState?.IsProviderLoaded(context.ProviderNames.FileSystem) != true) { + // We're only interested in resolving file system paths. return null; } - return filePaths; + // TODO: Can path resolution succeed and return null? + Collection resolvedPaths = context.SessionState.Path.GetResolvedProviderPathFromPSPath(path, + allowNonExistingPaths, + out ProviderInfo provider); + + // Ported from legacy code to preserve behavior. + if (!provider.NameEquals(context.ProviderNames.FileSystem)) + { + throw InterpreterError.NewInterpreterException( + path, + typeof(RuntimeException), + errorPosition: null, + "FileOpenError", + ParserStrings.FileOpenError, + provider.FullName); + } + + return resolvedPaths; + } + + /// + /// Resolves using the file system provider. + /// + /// + /// + /// This method does not normalize 's directory separators. + /// This method does not throw. + /// + /// + /// The path to resolve. + /// Execution context. + /// Whether non-existing paths should be resolved. + /// All resolved file system paths if the return value is , otherwise . + /// + /// if path resolution through the file system provider succeeded, otherwise . + /// + internal static bool TryResolveToFileSystemPaths( + string path, + ExecutionContext context, + out Collection resolvedPaths, + bool allowNonExistingPaths = false) + { + resolvedPaths = null; + + try + { + resolvedPaths = ResolveToFileSystemPathsThrowing(path, context, allowNonExistingPaths); + } + catch (Exception) + { + // Ignore. + } + + return resolvedPaths != null; } internal static PSSession GetWindowsPowerShellCompatRemotingSession() @@ -5439,7 +5497,7 @@ internal PSModuleInfo LoadUsingExtensions(PSModuleInfo parentModule, string fileName = fileBaseName + ext; // Get the resolved file name - fileName = GetResolvedPath(fileName, Context); + fileName = ResolveToSingleFileSystemPath(fileName, Context); if (fileName == null) continue; diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index 538c4775f0a..251e830c5ff 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -724,56 +724,62 @@ internal static bool MatchesModulePath(string modulePath, string requiredPath) } /// - /// Takes the name of a module as used in a module specification - /// and either returns it as a simple name (if it was a simple name) - /// or a fully qualified, PowerShell-resolved path. + /// Takes a module name from a module specification and returns a + /// normalized module name. /// - /// The name or path of the module from the specification. - /// The path to base relative paths off. + /// + /// + /// A normalized module name is either: + /// + /// A simple module name if was a simple name. + /// A fully qualified, PowerShell-resolved path. + /// + /// A fully qualified path by combination of and + /// if PowerShell could not resolve the path. + /// + /// + /// + /// + /// + /// The name or path of the module from the specification. + /// The path to base relative paths off. /// The current execution context. /// - /// The simple module name if the given one was simple, - /// otherwise a fully resolved, absolute path to the module. + /// A simple module name if was a simple module + /// name, otherwise a fully qualified path to the module. /// - /// - /// 2018-11-09 rjmholt: - /// There are several, possibly inconsistent, path handling mechanisms - /// in the module cmdlets. After looking through all of them and seeing - /// they all make some assumptions about their caller I wrote this method. - /// Hopefully we can find a standard path resolution API to settle on. - /// - internal static string NormalizeModuleName( - string moduleName, - string basePath, - ExecutionContext executionContext) + internal static string NormalizeModuleName(string moduleNameOrPath, string relativeTo, ExecutionContext executionContext) { - if (moduleName == null) - { - return null; - } + ArgumentNullException.ThrowIfNull(moduleNameOrPath); + ArgumentNullException.ThrowIfNull(relativeTo); - // Check whether the module is a path -- if not, it is a simple name and we just return it. - if (!IsModuleNamePath(moduleName)) + if (!IsModuleNamePath(moduleNameOrPath)) { - return moduleName; + return moduleNameOrPath; } - // Standardize directory separators -- Path.IsPathRooted() will return false for "\path\here" on *nix and for "/path/there" on Windows - moduleName = moduleName.Replace(StringLiterals.AlternatePathSeparator, StringLiterals.DefaultPathSeparator); + // Ensure OS default directory separators because + // - Path.IsPathRooted("\some\path") returns false on *nix, and + // - Path.IsPathRooted("/some/path") return false on Windows. + moduleNameOrPath = PathHandling.NormalizeDirectorySeparators(moduleNameOrPath); - // Note: Path.IsFullyQualified("\default\root") is false on Windows, but Path.IsPathRooted returns true - if (!Path.IsPathRooted(moduleName)) + // On Windows: + // - Path.IsFullyQualified("\default\root") returns false, but + // - Path.IsPathRooted("\default\root") returns true. + if (!Path.IsPathRooted(moduleNameOrPath)) { - moduleName = Path.Join(basePath, moduleName); + moduleNameOrPath = Path.Join(relativeTo, moduleNameOrPath); } - // Use the PowerShell filesystem provider to fully resolve the path - // If there is a problem, null could be returned -- so default back to the pre-normalized path - string normalizedPath = ModuleCmdletBase.GetResolvedPath(moduleName, executionContext)?.TrimEnd(StringLiterals.DefaultPathSeparator); + string normalizedPath = ModuleCmdletBase.ResolveToSingleFileSystemPath(moduleNameOrPath, executionContext)?.TrimEnd(StringLiterals.DefaultPathSeparator); - // ModuleCmdletBase.GetResolvePath will return null in the unlikely event that it failed. - // If it does, we return the fully qualified path generated before. - return normalizedPath ?? Path.GetFullPath(moduleName); + // If the resolved path is null, just return the fully qualified path generated before. + return normalizedPath ?? Path.GetFullPath(moduleNameOrPath); } /// diff --git a/src/System.Management.Automation/engine/Modules/ModuleSpecification.cs b/src/System.Management.Automation/engine/Modules/ModuleSpecification.cs index 3c06ee856f3..4a9d6a2cab1 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleSpecification.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleSpecification.cs @@ -90,24 +90,25 @@ internal static Exception ModuleSpecificationInitHelper(ModuleSpecification modu { string field = entry.Key.ToString(); - if (field.Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) + if (field.EqualsOrdinalIgnoreCase("ModuleName")) { moduleSpecification.Name = LanguagePrimitives.ConvertTo(entry.Value); } - else if (field.Equals("ModuleVersion", StringComparison.OrdinalIgnoreCase)) + else if (field.EqualsOrdinalIgnoreCase("ModuleVersion")) { moduleSpecification.Version = LanguagePrimitives.ConvertTo(entry.Value); } - else if (field.Equals("RequiredVersion", StringComparison.OrdinalIgnoreCase)) + else if (field.EqualsOrdinalIgnoreCase("RequiredVersion")) { moduleSpecification.RequiredVersion = LanguagePrimitives.ConvertTo(entry.Value); } - else if (field.Equals("MaximumVersion", StringComparison.OrdinalIgnoreCase)) + else if (field.EqualsOrdinalIgnoreCase("MaximumVersion")) { moduleSpecification.MaximumVersion = LanguagePrimitives.ConvertTo(entry.Value); - ModuleCmdletBase.GetMaximumVersion(moduleSpecification.MaximumVersion); + // Ensure max version is correctly formatted. + _ = ModuleCmdletBase.GetMaximumVersion(moduleSpecification.MaximumVersion); } - else if (field.Equals("GUID", StringComparison.OrdinalIgnoreCase)) + else if (field.EqualsOrdinalIgnoreCase("GUID")) { moduleSpecification.Guid = LanguagePrimitives.ConvertTo(entry.Value); } @@ -339,7 +340,7 @@ internal ModuleSpecification WithNormalizedName(ExecutionContext context, string } /// - /// Compares two ModuleSpecification objects for equality. + /// Compares two objects for structural equality. /// internal class ModuleSpecificationComparer : IEqualityComparer { @@ -351,7 +352,7 @@ internal class ModuleSpecificationComparer : IEqualityComparerTrue if the specifications are equal, false otherwise. public bool Equals(ModuleSpecification x, ModuleSpecification y) { - if (x == y) + if (ReferenceEquals(x, y)) { return true; } @@ -361,7 +362,7 @@ public bool Equals(ModuleSpecification x, ModuleSpecification y) && Guid.Equals(x.Guid, y.Guid) && Version.Equals(x.RequiredVersion, y.RequiredVersion) && Version.Equals(x.Version, y.Version) - && string.Equals(x.MaximumVersion, y.MaximumVersion); + && string.Equals(x.MaximumVersion, y.MaximumVersion, StringComparison.OrdinalIgnoreCase); } /// diff --git a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs index 4315f8bcb2b..e68ac4a8054 100644 --- a/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs +++ b/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs @@ -78,7 +78,7 @@ internal PSModuleInfo(string name, string path, ExecutionContext context, Sessio { if (path != null) { - string resolvedPath = ModuleCmdletBase.GetResolvedPath(path, context); + string resolvedPath = ModuleCmdletBase.ResolveToSingleFileSystemPath(path, context); // The resolved path might be null if we're building a dynamic module and the path // is just a GUID, not an actual path that can be resolved. Path = resolvedPath ?? path; diff --git a/src/System.Management.Automation/engine/Modules/PathHandling.cs b/src/System.Management.Automation/engine/Modules/PathHandling.cs new file mode 100644 index 00000000000..25cfe3159b1 --- /dev/null +++ b/src/System.Management.Automation/engine/Modules/PathHandling.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace System.Management.Automation.Internal +{ + internal static class PathHandling + { + public static string NormalizeDirectorySeparators(string path) + { + return path.Replace(StringLiterals.AlternatePathSeparator, StringLiterals.DefaultPathSeparator); + } + } +} diff --git a/src/System.Management.Automation/utils/PathUtils.cs b/src/System.Management.Automation/utils/PathUtils.cs index 3afea30a962..5016751501b 100644 --- a/src/System.Management.Automation/utils/PathUtils.cs +++ b/src/System.Management.Automation/utils/PathUtils.cs @@ -618,7 +618,7 @@ internal static DirectoryInfo CreateModuleDirectory(PSCmdlet cmdlet, string modu { // Even if 'moduleNameOrPath' is a rooted path, 'ResolveRootedFilePath' may return null when the path doesn't exist yet, // or when it contains wildcards but cannot be resolved to a single path. - string rootedPath = ModuleCmdletBase.ResolveRootedFilePath(moduleNameOrPath, cmdlet.Context); + string rootedPath = ModuleCmdletBase.ResolveToFileSystemPathIfRooted(moduleNameOrPath, cmdlet.Context); if (string.IsNullOrEmpty(rootedPath) && moduleNameOrPath.StartsWith('.')) { PathInfo currentPath = cmdlet.CurrentProviderLocation(cmdlet.Context.ProviderNames.FileSystem); diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Get-Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Get-Module.Tests.ps1 index fddd7a12ea6..e4855255df3 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Get-Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Get-Module.Tests.ps1 @@ -280,6 +280,754 @@ Describe "Get-Module -ListAvailable" -Tags "CI" { } } +Describe 'Get-Module -ListAvailable -(FullyQualifiedName|Name) when argument is module name or filename-like' -Tags "CI" { + BeforeAll { + $oldPSModulePath = $env:PSModulePath + $env:PSModulePath = New-Item -ItemType Directory (Join-Path $TestDrive modules) + + # Manifest modules under $env:PSModulePath + # + # Versioned, manifest + $inPSModulePathWithManifestFileName = 'existing' + $inPSModulePatWithManifestDirectory = Join-Path $env:PSModulePath $inPSModulePathWithManifestFileName '0.0.1' + $inPSModulePathWithManifestFilePath = Join-Path $inPSModulePatWithManifestDirectory "$inPSModulePathWithManifestFileName.psm1" + New-Item -ItemType File -Force $inPSModulePathWithManifestFilePath > $null + New-ModuleManifest -Path (Join-Path $inPSModulePatWithManifestDirectory "$inPSModulePathWithManifestFileName.psd1") + # + # Versioned, manifest + $inPSModulePathWithManifestFileName2 = 'existing2' + $inPSModulePatWithManifestDirectory2 = Join-Path $env:PSModulePath $inPSModulePathWithManifestFileName2 '0.0.1' + $inPSModulePathWithManifestFilePath2 = Join-Path $inPSModulePatWithManifestDirectory2 "$inPSModulePathWithManifestFileName2.psm1" + New-Item -ItemType File -Force $inPSModulePathWithManifestFilePath2 > $null + New-ModuleManifest -Path (Join-Path $inPSModulePatWithManifestDirectory2 "$inPSModulePathWithManifestFileName2.psd1") + # + # Versioned, manifest, multiple components in name + $inPSModulePathWithManifestWithPathLikeNameFileName = 'existing.xxx' + $inPsModulePathWithManifestWithPathLikeNameDirectory = Join-Path $env:PSModulePath $inPSModulePathWithManifestWithPathLikeNameFileName '0.0.1' + $inPsModulePathWithManifestWithPathLikeNameFilePath = Join-Path $inPsModulePathWithManifestWithPathLikeNameDirectory "$inPSModulePathWithManifestWithPathLikeNameFileName.psm1" + New-Item -ItemType File -Force $inPsModulePathWithManifestWithPathLikeNameFilePath > $null + New-ModuleManifest -Path (Join-Path $inPsModulePathWithManifestWithPathLikeNameDirectory "$inPSModulePathWithManifestWithPathLikeNameFileName.psd1") + # + # Versioned, manifest, multiple components in name, name ending with PS module extension + $inPSModulePathWithManifestWithPathLikeNameAsPSExtensionFileName = 'existing.psm1' + $inPSModulePathWithManifestWithPathLikeNameAsPSExtensionDirectory = Join-Path $env:PSModulePath $inPSModulePathWithManifestWithPathLikeNameAsPSExtensionFileName '0.0.1' + $inPSModulePathWithManifestWithPathLikeNameAsPSExtensionFilePath = Join-Path $inPSModulePathWithManifestWithPathLikeNameAsPSExtensionDirectory "$inPSModulePathWithManifestWithPathLikeNameAsPSExtensionFileName.psm1" + New-Item -ItemType File -Force $inPSModulePathWithManifestWithPathLikeNameAsPSExtensionFilePath > $null + New-ModuleManifest -Path (Join-Path $inPSModulePathWithManifestWithPathLikeNameAsPSExtensionDirectory "$inPSModulePathWithManifestWithPathLikeNameAsPSExtensionFileName.psd1") + + # Script modules + # + # Under $env:PSModulePath. TODO: Is this a supported scenario? + $inPSModulePathLooseFilePath = Join-Path $env:PSModulePath 'existing-loose.psm1' + New-Item -ItemType File -Force $inPSModulePathLooseFilePath > $null + # + # Under $env:PSModulePath, multiple components in name. TODO: Is this a supported scenario? + $inPSModulePathLooseWithPathLikeFilePath = Join-Path $env:PSModulePath "existing-loose.xxx.psm1" + New-Item -ItemType File -Force $inPSModulePathLooseWithPathLikeFilePath > $null + # + # Under $env:PSModulePath, multiple components in name, name ending with PS module extension. TODO: Is this a supported scenario? + $inPSModulePathLooseWithPathLikeNameAsPSExtensionFilePath = Join-Path $env:PSModulePath "existing.psm1.psm1" + New-Item -ItemType File -Force $inPSModulePathLooseWithPathLikeNameAsPSExtensionFilePath > $null + # + # In working directory + $inWorkingDirectoryLooseFilePath = Join-Path . existing.psm1 + New-Item -ItemType File -Force $inWorkingDirectoryLooseFilePath > $null + # + # In parent directory + $inParentDirectoryLooseFilePath = Join-Path .. existing.psm1 + New-Item -ItemType File -Force $inParentDirectoryLooseFilePath > $null + # + # In working directory, ignored + 'function foo { "_" }' > (Join-Path $pwd ignore.psm1) + 'function foo { "_" }' > (Join-Path $pwd ignore.psm1.psm1) + } + + AfterAll { + $env:PSModulePath = $oldPSModulePath + + Remove-Item -ErrorAction Stop -Force -Recurse $inPSModulePatWithManifestDirectory + Remove-Item -ErrorAction Stop -Force -Recurse $inPSModulePatWithManifestDirectory2 + Remove-Item -ErrorAction Stop -Force -Recurse $inPsModulePathWithManifestWithPathLikeNameDirectory + Remove-Item -ErrorAction Stop -Force -Recurse $inPSModulePathWithManifestWithPathLikeNameAsPSExtensionDirectory + + Remove-Item -ErrorAction Stop $inPSModulePathLooseFilePath + Remove-Item -ErrorAction Stop $inPSModulePathLooseWithPathLikeFilePath + Remove-Item -ErrorAction Stop $inPSModulePathLooseWithPathLikeNameAsPSExtensionFilePath + Remove-Item -ErrorAction Stop $inWorkingDirectoryLooseFilePath + Remove-Item -ErrorAction Stop $inParentDirectoryLooseFilePath + Remove-Item -ErrorAction Stop ignore.psm1 + Remove-Item -ErrorAction Stop ignore.psm1.psm1 + } + + It 'returns module information for manifest module' { + Get-Module -ListAvailable -FullyQualifiedName existing | ForEach-Object Path | Should -BeExactly (Join-Path $env:PSModulePath existing '0.0.1','existing.psd1') + Get-Module -ListAvailable -Name existing | ForEach-Object Path | Should -BeExactly (Join-Path $env:PSModulePath existing '0.0.1','existing.psd1') + } + + It 'ignores module at current directory' { + Get-Module -ListAvailable -FullyQualifiedName ignore | Should -Be $null + Get-Module -ListAvailable -Name ignore | Should -Be $null + } + + Context 'When path value has PS module extension, such as ''name.psm1''' { + # Because non-rooted/non-relative-rooted paths MUST be resolved to modules under $env:PSModulePath. + It 'ignores module at current directory' { + Get-Module -ListAvailable -FullyQualifiedName ignore.psm1 | Should -Be $null + Get-Module -ListAvailable -Name ignore.psm1 | Should -Be $null + } + + Context 'Resolves to module under $env:PSModulePath' { + # Unclear whether this is correct behavior. Are loose modules allowed under $env:PSModulePath? + # Could resolve to: + # - 'existing-loose.psm1' loose module + It 'wrongly/correctly returns $null instead of module information for loose module' { + Get-Module -ListAvailable -FullyQualifiedName existing-loose.psm1 | Should -Be $null + Get-Module -ListAvailable -Name existing-loose.psm1 | Should -Be $null + } + + # Unclear whether this is correct behavior. Are loose modules allowed under $env:PSModulePath?. + # Could resolve to: + # - 'existing2' manifest module + It 'wrongly/correctly returns $null instead of module information for versioned module' { + Get-Module -ListAvailable -FullyQualifiedName existing2.psm1 | Should -Be $null + Get-Module -ListAvailable -Name existing2.psm1 | Should -Be $null + } + + # Finds manifest module 'existing.psm1.psm1'. TODO: Is that correct? + It 'wrongly/correctly returns module information for versioned module ending with PS extension' { + Get-Module -ListAvailable -FullyQualifiedName existing.psm1 | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + Get-Module -ListAvailable -Name existing.psm1 | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + } + } + + Context 'Resolves to non-existing loose or versioned module under $env:PSModulePath' { + It 'wrongly returns $null instead of error' { + $path1 = Join-Path $env:PSModulePath missing.psm1 + $path2 = Join-Path $env:PSModulePath missing.psm1 *, missing.psm1 + Test-Path $path1 | Should -BeFalse + Test-Path $path2 | Should -BeFalse + + Get-Module -ListAvailable -FullyQualifiedName missing.psm1 | Should -Be $null + Get-Module -ListAvailable -Name missing.psm1 | Should -Be $null + } + } + } + + Context 'When path value has unknown extension, such as ''name.xxx''' { + Context 'Resolves to non-existing loose or versioned module under $env:PSModulePath' { + It 'wrongly returns $null instead of error' { + $path1 = Join-Path $env:PSModulePath missing.xxx.psm1 + $path2 = Join-Path $env:PSModulePath missing *, missing.xxx.psm1 + Test-Path $path1 | Should -BeFalse + Test-Path $path2 | Should -BeFalse + + Get-Module -ListAvailable -FullyQualifiedName missing.xxx | Should -Be $null + Get-Module -ListAvailable -Name missing.xxx | Should -Be $null + } + } + + Context 'Resolves to loose module under $env:PSModule' { + # Unclear whether only manifest modules are discovered under $env:PSModulePath. + It 'wrongly/correctly returns $null instead of module information' { + $path = Join-Path $env:PSModulePath existing-loose.xxx.psm1 + Test-Path $path | Should -BeTrue + + Get-Module -ListAvailable -FullyQualifiedName existing-loose.xxx | Should -Be $null + } + } + + Context 'Resolves to manifest module under $env:PSModule' { + # Unclear whether only manifest modules are discovered under $env:PSModulePath. + It 'returns module information' { + $path = Join-Path $env:PSModulePath existing.xxx 0.0.1, existing.xxx.psm1 + Test-Path $path | Should -BeTrue + + Get-Module -ListAvailable -FullyQualifiedName existing.xxx | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + Get-Module -ListAvailable -Name existing.xxx | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + } + } + + Context 'When argument has wildcards' { + It 'ignores existing script modules in working directory' { + Join-Path $pwd ignore.psm1 | Should -BeTrue + + $actual = Get-Module -ListAvailable -Name ignore* + $actual | Should -Be $null + + $actual = Get-Module -ListAvailable -FullyQualifiedName ignore* + $actual | Should -Be $null + } + + Context 'Resolves to existing script modules' { + It 'wrongly/correctly returns $null' { + $actual = Get-Module -ListAvailable -Name existing-loose* + $actual | Should -Be $null + + $actual = Get-Module -ListAvailable -FullyQualifiedName existing-loose* + $actual | Should -Be $null + } + } + + Context 'Resolves to existing manifest modules' { + It 'returns modules' { + $actual = Get-Module -ListAvailable -Name existing* + $actual | Should -HaveCount 4 + $actual[0].Name | Should -BeExactly existing + $actual[1].Name | Should -BeExactly existing.psm1 + $actual[2].Name | Should -BeExactly existing.xxx + $actual[3].Name | Should -BeExactly existing2 + + $actual = Get-Module -ListAvailable -FullyQualifiedName existing* + $actual | Should -HaveCount 4 + $actual[0].Name | Should -BeExactly existing + $actual[1].Name | Should -BeExactly existing.psm1 + $actual[2].Name | Should -BeExactly existing.xxx + $actual[3].Name | Should -BeExactly existing2 + } + } + + Context 'Resolves to non-existing modules' { + It 'wrongly/correctly returns $null' { + $actual = Get-Module -ListAvailable -Name missing* + $actual | Should -Be $null + + $actual = Get-Module -ListAvailable -FullyQualifiedName missing* + $actual | Should -Be $null + } + } + } + } +} + +Describe 'Get-Module -ListAvailable -(FullyQualifiedName|Name) when argument is home-rooted' -Tags "CI" { + It 'wrongly does not expand ''~'' to $HOME' { + $path = Join-Path ~ missing.psm1 + Test-Path $path | Should -BeFalse + + Get-Module -ListAvailable -Name $path | ForEach-Object Path | Should -BeExactly (Join-Path $HOME missing.psm1) + # TODO: This is a bug. + Get-Module -ListAvailable -FullyQualifiedName $path | ForEach-Object Path | Should -BeExactly (Join-Path $pwd $path) + } + + It 'wrongly returns module information instead of $null or error for missing script module' { + $path = Join-Path ~ missing.psm1 + Test-Path $path | Should -BeFalse + + Get-Module -ListAvailable -Name $path | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + Get-Module -ListAvailable -FullyQualifiedName $path | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + } + + It 'writes error for missing manifest module' { + $path = Join-Path ~ missing.psm1 + Test-Path $path | Should -BeFalse + + $name = Join-Path ~ missing + + $err = { Get-Module -ListAvailable -Name $name -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + + $err = { Get-Module -ListAvailable -FullyQualifiedName $name -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + } + + Context 'Locating existing script module' { + BeforeAll { + # Script modules + # + $inHomeLooseFilePath = Join-Path $HOME loose.psm1 + New-Item -ItemType File -Force $inHomeLooseFilePath > $null + } + + AfterAll { + Remove-Item $inHomeLooseFilePath + } + + # TODO: This looks like a bug. + It 'wrongly writes error instead of returning module information for existing script module using basename' { + $path = Join-Path ~ loose.psm1 + Test-Path $path | Should -BeTrue + + $name = Join-Path ~ loose + + $err = { Get-Module -ListAvailable -Name $name -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + + $err = { Get-Module -ListAvailable -FullyQualifiedName $name -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + } + + It 'returns module information for existing script module using file name' { + $path = Join-Path ~ loose.psm1 + Test-Path $path | Should -BeTrue + + $actual = Get-Module -ListAvailable -Name $path + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly (Join-Path $HOME loose.psm1) + + # TODO: This is a bug. + $actual = Get-Module -ListAvailable -FullyQualifiedName $path + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly (Join-Path $pwd $path) + } + } + + Context 'When argument contains wildcards' { + BeforeAll { + # Manifest modules under '~' + # + # Versioned, manifest + $inHomeWithManifestFileName = 'existing' + $inHomeWithManifestDirectory = Join-Path '~' $inHomeWithManifestFileName '0.0.1' + $inHomeWithManifestFilePath = Join-Path $inHomeWithManifestDirectory "$inHomeWithManifestFileName.psm1" + New-Item -ItemType File -Force $inHomeWithManifestFilePath > $null + New-ModuleManifest -Path (Join-Path $inHomeWithManifestDirectory "$inHomeWithManifestFileName.psd1") + # + # Versioned, manifest + $inHomeWithManifestFileName2 = 'existing2' + $inHomeWithManifestDirectory2 = Join-Path '~' $inHomeWithManifestFileName2 '0.0.1' + $inHomeWithManifestFilePath2 = Join-Path $inHomeWithManifestDirectory2 "$inHomeWithManifestFileName2.psm1" + New-Item -ItemType File -Force $inHomeWithManifestFilePath2 > $null + New-ModuleManifest -Path (Join-Path $inHomeWithManifestDirectory2 "$inHomeWithManifestFileName2.psd1") + + # Script modules under '~' + # + $inHomeLooseFilePath = Join-Path '~' 'loose.psm1' + New-Item -ItemType File -Force $inHomeLooseFilePath > $null + } + + AfterAll { + Remove-Item -Force -Recurse $inHomeWithManifestDirectory + Remove-Item -Force -Recurse $inHomeWithManifestDirectory2 + + Remove-Item $inHomeLooseFilePath + } + + It 'returns existing manifest modules when using the -Name parameter' { + $name = Join-Path ~ existing* + + $actual = Get-Module -ListAvailable -Name $name + $actual | Should -HaveCount 2 + $actual[0].Path | Should -BeExactly (Join-Path $HOME existing 0.0.1, existing.psd1) + $actual[1].Path | Should -BeExactly (Join-Path $HOME existing2 0.0.1, existing2.psd1) + } + + # TODO: This looks like a bug. + It 'wrongly returns $null for existing manifest modules when using the -FullyQualifiedName parameter' { + $actual = Get-Module -ListAvailable -FullyQualifiedName (Join-Path ~ existing*) + $actual | Should -Be $null + } + + It 'returns module information for existing script module' { + $actual = Get-Module -ListAvailable -Name (Join-Path ~ loose*) + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly (Join-Path $HOME loose.psm1) + } + } +} + +Describe 'Get-Module -ListAvailable -(FullyQualifiedName|Name) when argument is relative-rooted' -Tags "CI" { + It 'wrongly returns module information instead of $null or error for missing script module' { + $path1 = Join-Path . missing.psm1 + $path2 = Join-Path .. missing.psm1 + Test-Path $path1 | Should -BeFalse + Test-Path $path2 | Should -BeFalse + + Get-Module -ListAvailable -Name $path1 | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + Get-Module -ListAvailable -Name $path2 | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + Get-Module -ListAvailable -FullyQualifiedName $path1 | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + Get-Module -ListAvailable -FullyQualifiedName $path2 | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + } + + It 'writes error for missing manifest module' { + $path1 = Join-Path . missing + $path2 = Join-Path .. missing + Test-Path $path1 | Should -BeFalse + Test-Path $path2 | Should -BeFalse + + { Get-Module -ListAvailable -Name $path1 -ErrorAction Stop } | Should -Throw -Because '*Update the Name parameter*' + { Get-Module -ListAvailable -Name $path2 -ErrorAction Stop } | Should -Throw -Because '*Update the Name parameter*' + { Get-Module -ListAvailable -FullyQualifiedName $path1 -ErrorAction Stop } | Should -Throw -Because '*Update the Name parameter*' + { Get-Module -ListAvailable -FullyQualifiedName $path2 -ErrorAction Stop } | Should -Throw -Because '*Update the Name parameter*' + } + + Context 'Locating existing script module' { + BeforeAll { + # Script modules + # + # Under $env:PSModulePath. TODO: Is this a supported scenario? + $inPSModulePathLooseFilePath = Join-Path . loose.psm1 + New-Item -ItemType File -Force $inPSModulePathLooseFilePath > $null + # + # Under $pwd + $inPSModulePathLooseFilePathParent = Join-Path .. loose.psm1 + New-Item -ItemType File -Force $inPSModulePathLooseFilePathParent > $null + } + + AfterAll { + Remove-Item $inPSModulePathLooseFilePath + Remove-Item $inPSModulePathLooseFilePathParent + } + + # TODO: This looks like a bug. + It 'wrongly writes error instead of returning module information for existing script module using basename' { + $path1 = Join-Path . loose.psm1 + $path2 = Join-Path .. loose.psm1 + Test-Path $path1 | Should -BeTrue + Test-Path $path2 | Should -BeTrue + + $name1 = Join-Path . loose + $name2 = Join-Path .. loose + + $err = { Get-Module -ListAvailable -Name $name1 -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + + $err = { Get-Module -ListAvailable -Name $name2 -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + } + + It 'returns module information for existing script module using file name' { + $name1 = Join-Path . loose.psm1 + $name2 = Join-Path .. loose.psm1 + Test-Path $name1 | Should -BeTrue + Test-Path $name2 | Should -BeTrue + + $actual = Get-Module -ListAvailable -Name $name1 + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly ([System.IO.Path]::GetFullPath($name1)) + + $actual = Get-Module -ListAvailable -Name $name2 + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly ([System.IO.Path]::GetFullPath($name2)) + + $actual = Get-Module -ListAvailable -FullyQualifiedName $name1 + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly ([System.IO.Path]::GetFullPath($name1)) + + $actual = Get-Module -ListAvailable -FullyQualifiedName $name2 + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly ([System.IO.Path]::GetFullPath($name2)) + } + } + + Context 'When argument contains wildcards' { + BeforeAll { + # Manifest modules under '.' + # + # Versioned, manifest + $inCwdWithManifestFileName = 'existing' + $inCwdWithManifestDirectory = Join-Path '.' $inCwdWithManifestFileName '0.0.1' + $inCwdWithManifestFilePath = Join-Path $inCwdWithManifestDirectory "$inCwdWithManifestFileName.psm1" + New-Item -ItemType File -Force $inCwdWithManifestFilePath > $null + New-ModuleManifest -Path (Join-Path $inCwdWithManifestDirectory "$inCwdWithManifestFileName.psd1") + # + # Versioned, manifest + $inCwdWithManifestFileName2 = 'existing2' + $inCwdWithManifestDirectory2 = Join-Path '.' $inCwdWithManifestFileName2 '0.0.1' + $inCwdWithManifestFilePath2 = Join-Path $inCwdWithManifestDirectory2 "$inCwdWithManifestFileName2.psm1" + New-Item -ItemType File -Force $inCwdWithManifestFilePath2 > $null + New-ModuleManifest -Path (Join-Path $inCwdWithManifestDirectory2 "$inCwdWithManifestFileName2.psd1") + + # Manifest modules under '..' + # + # Versioned, manifest + $inParentWithManifestFileName = 'existing' + $inParentWithManifestDirectory = Join-Path '..' $inParentWithManifestFileName '0.0.1' + $inParentWithManifestFilePath = Join-Path $inParentWithManifestDirectory "$inParentWithManifestFileName.psm1" + New-Item -ItemType File -Force $inParentWithManifestFilePath > $null + New-ModuleManifest -Path (Join-Path $inParentWithManifestDirectory "$inParentWithManifestFileName.psd1") + # + # Versioned, manifest + $inParentWithManifestFileName2 = 'existing2' + $inParentWithManifestDirectory2 = Join-Path '..' $inParentWithManifestFileName2 '0.0.1' + $inParentWithManifestFilePath2 = Join-Path $inParentWithManifestDirectory2 "$inParentWithManifestFileName2.psm1" + New-Item -ItemType File -Force $inParentWithManifestFilePath2 > $null + New-ModuleManifest -Path (Join-Path $inParentWithManifestDirectory2 "$inParentWithManifestFileName2.psd1") + + # Script modules under '.' and '..' + # + # Under '.' + $inCwdLooseFilePath = Join-Path '.' 'loose.psm1' + New-Item -ItemType File -Force $inCwdLooseFilePath > $null + # + # Under '..' + $inParentLooseFilePath = Join-Path '..' 'loose.psm1' + New-Item -ItemType File -Force $inParentLooseFilePath > $null + } + + AfterAll { + Remove-Item -Force -Recurse $inCwdWithManifestDirectory + Remove-Item -Force -Recurse $inCwdWithManifestDirectory2 + + Remove-Item -Force -Recurse $inParentWithManifestDirectory + Remove-Item -Force -Recurse $inParentWithManifestDirectory2 + + Remove-Item $inCwdLooseFilePath + } + + It 'returns existing manifest modules when using the -Name parameter' { + $actual = Get-Module -ListAvailable -Name '.\existing*' + $actual | Should -HaveCount 2 + $actual[0].Path | Should -BeExactly ([System.IO.Path]::GetFullPath('.\existing\0.0.1\existing.psd1')) + $actual[1].Path | Should -BeExactly ([System.IO.Path]::GetFullPath('.\existing2\0.0.1\existing2.psd1')) + + $actual = Get-Module -ListAvailable -Name '..\existing*' + $actual | Should -HaveCount 2 + $actual[0].Path | Should -BeExactly ([System.IO.Path]::GetFullPath('..\existing\0.0.1\existing.psd1')) + $actual[1].Path | Should -BeExactly ([System.IO.Path]::GetFullPath('..\existing2\0.0.1\existing2.psd1')) + } + + # TODO: This looks like a bug. + It 'wrongly returns $null for existing manifest modules when using the -FullyQualifiedName parameter' { + $actual = Get-Module -ListAvailable -FullyQualifiedName '.\existing*' + $actual | Should -Be $null + + $actual = Get-Module -ListAvailable -FullyQualifiedName '..\existing*' + $actual | Should -Be $null + } + + It 'returns module information for existing script module' { + $actual = Get-Module -ListAvailable -Name '.\loose*' + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly ([System.IO.Path]::GetFullPath('.\loose.psm1')) + + $actual = Get-Module -ListAvailable -FullyQualifiedName '..\loose*' + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly ([System.IO.Path]::GetFullPath('..\loose.psm1')) + } + } +} + +Describe 'Get-Module -ListAvailable -(FullyQualifiedName|Name) when argument is absolute path' -Tags "CI" { + BeforeAll { + $oldPSModulePath = $env:PSModulePath + $env:PSModulePath = New-Item -ItemType Directory (Join-Path $TestDrive modules) + } + + AfterAll { + $env:PSModulePath = $oldPSModulePath + } + + It 'wrongly returns module information instead of $null or error for missing script module' { + $path = [System.IO.Path]::GetFullPath((Join-Path $pwd missing.psm1)) + Test-Path $path | Should -BeFalse + + Get-Module -ListAvailable -FullyQualifiedName $path | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + Get-Module -ListAvailable -Name $path | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + } + + It 'wrongly returns module information instead of $null or error for missing script module under $env:PSModulePath' { + $path = [System.IO.Path]::GetFullPath((Join-Path $env:PSModulePath missing.psm1)) + Test-Path $path | Should -BeFalse + + Get-Module -ListAvailable -FullyQualifiedName $path | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + Get-Module -ListAvailable -Name $path | Should -BeOfType ([System.Management.Automation.PSModuleInfo]) + } + + It 'writes error for missing manifest module' { + $path = [System.IO.Path]::GetFullPath((Join-Path $pwd missing)) + Test-Path $path | Should -BeFalse + + $err = { Get-Module -ListAvailable -FullyQualifiedName $path -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + + $err = { Get-Module -ListAvailable -Name $path -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + } + + It 'writes error for missing manifest module under $env:PSModulePath' { + $path = [System.IO.Path]::GetFullPath((Join-Path $env:PSModulePath missing)) + Test-Path $path | Should -BeFalse + + $err = { Get-Module -ListAvailable -FullyQualifiedName $path -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + + $err = { Get-Module -ListAvailable -Name $path -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + } + + Context 'Locating existing script module' { + BeforeAll { + # Script modules + # + # Under $env:PSModulePath. TODO: Is this a supported scenario? + $inPSModulePathLooseFilePath = Join-Path $env:PSModulePath 'loose.psm1' + New-Item -ItemType File -Force $inPSModulePathLooseFilePath > $null + # + # Under $pwd + $inCwdLooseFilePath = Join-Path $pwd 'loose.psm1' + New-Item -ItemType File -Force $inCwdLooseFilePath > $null + } + + AfterAll { + Remove-Item $inPSModulePathLooseFilePath + Remove-Item $inCwdLooseFilePath + } + + # TODO: This looks like a bug. + It 'wrongly writes error instead of returning module information for existing script module under $env:PSModulePath using basename' { + Test-Path (Join-Path $env:PSModulePath loose.psm1) | Should -BeTrue + $path = Join-Path $env:PSModulePath loose + + $err = { Get-Module -ListAvailable -Name $path -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + + $err = { Get-Module -ListAvailable -FullyQualifiedName $path -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + } + + # TODO: This looks like a bug. + It 'wrongly writes error instead of returning module information for existing script module using basename' { + Test-Path (Join-Path $pwd loose.psm1) | Should -BeTrue + $path = Join-Path $pwd loose + + $err = { Get-Module -ListAvailable -Name $path -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + + $err = { Get-Module -ListAvailable -FullyQualifiedName $path -ErrorAction Stop } | Should -Throw -PassThru + $err.Exception.Message | Should -BeLike '*Update the Name parameter*' + } + + It 'returns module information for existing script module using file name' { + $path = Join-Path $pwd loose.psm1 + Test-Path $path | Should -BeTrue + + $actual = Get-Module -ListAvailable -Name $path + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly $path + + $actual = Get-Module -ListAvailable -FullyQualifiedName $path + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly $path + } + + It 'returns module information for existing script module under $env:PSModulePath using file name' { + $path = Join-Path $env:PSModulePath loose.psm1 + Test-Path $path | Should -BeTrue + + $actual = Get-Module -ListAvailable -Name $path + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly $path + + $actual = Get-Module -ListAvailable -FullyQualifiedName $path + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly $path + } + } + + Context 'When argument contains wildcards' { + BeforeAll { + # Manifest modules under $env:PSModulePath + # + # Versioned, manifest + $inPSModulePathWithManifestFileName = 'existing' + $inPSModulePatWithManifestDirectory = Join-Path $env:PSModulePath $inPSModulePathWithManifestFileName '0.0.1' + $inPSModulePathWithManifestFilePath = Join-Path $inPSModulePatWithManifestDirectory "$inPSModulePathWithManifestFileName.psm1" + New-Item -ItemType File -Force $inPSModulePathWithManifestFilePath > $null + New-ModuleManifest -Path (Join-Path $inPSModulePatWithManifestDirectory "$inPSModulePathWithManifestFileName.psd1") + # + # Versioned, manifest + $inPSModulePathWithManifestFileName2 = 'existing2' + $inPSModulePatWithManifestDirectory2 = Join-Path $env:PSModulePath $inPSModulePathWithManifestFileName2 '0.0.1' + $inPSModulePathWithManifestFilePath2 = Join-Path $inPSModulePatWithManifestDirectory2 "$inPSModulePathWithManifestFileName2.psm1" + New-Item -ItemType File -Force $inPSModulePathWithManifestFilePath2 > $null + New-ModuleManifest -Path (Join-Path $inPSModulePatWithManifestDirectory2 "$inPSModulePathWithManifestFileName2.psd1") + + # Manifest modules under $pwd + # + # Versioned, manifest + $inCwdhWithManifestFileName = 'existing' + $inCwdWithManifestDirectory = Join-Path $pwd $inCwdhWithManifestFileName '0.0.1' + $inCwdWithManifestFilePath = Join-Path $inCwdWithManifestDirectory "$inCwdhWithManifestFileName.psm1" + New-Item -ItemType File -Force $inCwdWithManifestFilePath > $null + New-ModuleManifest -Path (Join-Path $inCwdWithManifestDirectory "$inCwdhWithManifestFileName.psd1") + # + # Versioned, manifest + $inCwdWithManifestFileName2 = 'existing2' + $inCwdWithManifestDirectory2 = Join-Path $pwd $inCwdWithManifestFileName2 '0.0.1' + $inCwdWithManifestFilePath2 = Join-Path $inCwdWithManifestDirectory2 "$inCwdWithManifestFileName2.psm1" + New-Item -ItemType File -Force $inCwdWithManifestFilePath2 > $null + New-ModuleManifest -Path (Join-Path $inCwdWithManifestDirectory2 "$inCwdWithManifestFileName2.psd1") + + # Script modules + # + # Under $env:PSModulePath. TODO: Is this a supported scenario? + $inPSModulePathLooseFilePath = Join-Path $env:PSModulePath 'loose.psm1' + New-Item -ItemType File -Force $inPSModulePathLooseFilePath > $null + # + # + # Under $pwd + $inCwdFilePath = Join-Path $pwd 'loose.psm1' + New-Item -ItemType File -Force $inCwdFilePath > $null + } + + AfterAll { + Remove-Item -Force -Recurse $inPSModulePatWithManifestDirectory + Remove-Item -Force -Recurse $inPSModulePatWithManifestDirectory2 + + Remove-Item -Force -Recurse $inCwdWithManifestDirectory + Remove-Item -Force -Recurse $inCwdWithManifestDirectory2 + + Remove-Item $inPSModulePathLooseFilePath + Remove-Item $inCwdFilePath + } + + It 'returns existing manifest modules under $env:PSModulePath when using the -Name parameter' { + $moduleManifestPath1 = Join-Path $env:PSModulePath existing 0.0.1,existing.psd1 + $moduleManifestPath2 = Join-Path $env:PSModulePath existing2 0.0.1,existing2.psd1 + Test-Path $moduleManifestPath1 | Should -BeTrue + Test-Path $moduleManifestPath2 | Should -BeTrue + + $actual = Get-Module -ListAvailable -Name (Join-Path $env:PSModulePath 'existing*') + $actual | Should -HaveCount 2 + $actual[0].Path | Should -BeExactly $moduleManifestPath1 + $actual[1].Path | Should -BeExactly $moduleManifestPath2 + } + + # TODO: This looks like a bug. + It 'wrongly returns $null for existing manifest modules under $env:PSModulePath when using the -FullyQualifiedName parameter' { + $actual = Get-Module -ListAvailable -FullyQualifiedName (Join-Path $env:PSModulePath 'existing*') + $actual | Should -Be $null + } + + It 'returns existing manifest modules when using the -Name parameter' { + $moduleManifestPath1 = Join-Path $pwd existing 0.0.1,existing.psd1 + $moduleManifestPath2 = Join-Path $pwd existing2 0.0.1,existing2.psd1 + Test-Path $moduleManifestPath1 | Should -BeTrue + Test-Path $moduleManifestPath2 | Should -BeTrue + + $actual = Get-Module -ListAvailable -Name (Join-Path $pwd 'existing*') + $actual | Should -HaveCount 2 + $actual[0].Path | Should -BeExactly $moduleManifestPath1 + $actual[1].Path | Should -BeExactly $moduleManifestPath2 + } + + # TODO: This looks like a bug. + It 'wrongly returns $null for existing manifest modules when using the -FullyQualifiedName parameter' { + $actual = Get-Module -ListAvailable -FullyQualifiedName (Join-Path $pwd 'existing*') + $actual | Should -Be $null + } + + It 'returns module information for existing script module under $env:PSModulePath' { + $actual = Get-Module -ListAvailable -Name (Join-Path $env:PSModulePath 'loose*') + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly (Join-Path $env:PSModulePath loose.psm1) + } + + It 'returns module information for existing script module using file name' { + $path = Join-Path $pwd loose.psm1 + Test-Path $path | Should -BeTrue + + $actual = Get-Module -ListAvailable -Name $path + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly $path + + $actual = Get-Module -ListAvailable -FullyQualifiedName $path + $actual | Should -HaveCount 1 + $actual[0].Path | Should -BeExactly $path + } + } +} + Describe 'Get-Module -ListAvailable with path' -Tags "CI" { BeforeAll { $moduleName = 'Banana'