From d6abc95dfc863edac2edc285a944627bee43e8af Mon Sep 17 00:00:00 2001 From: Driss B Date: Mon, 2 Feb 2026 21:41:58 +0100 Subject: [PATCH 01/69] Implement list by os arch --- src/actions/common.ps1 | 9 +++++---- src/actions/list.ps1 | 33 ++++++++++++++++++++++----------- src/core/router.ps1 | 8 +++++++- src/functions/helpers.ps1 | 16 ++++++++++++++++ 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/actions/common.ps1 b/src/actions/common.ps1 index 85d2773..953fa61 100644 --- a/src/actions/common.ps1 +++ b/src/actions/common.ps1 @@ -42,12 +42,13 @@ function Get-Installed-PHP-Versions { try { $directories = Get-All-Subdirectories -path "$STORAGE_PATH\php" $names = $directories | ForEach-Object { + $filenameParts = $_.Name -split '_' if (Test-Path "$($_.FullName)\php.exe"){ - return $_.Name + return @{ name = $filenameParts[0]; arch = $filenameParts[1]; dirName = $_.Name } } return $null } - return ($names | Sort-Object { [version]$_ }) + return ($names | Sort-Object { [version]$_.name }) } catch { $logged = Log-Data -data @{ header = "$($MyInvocation.MyCommand.Name) - Failed to retrieve installed PHP versions" @@ -100,8 +101,8 @@ function Get-Matching-PHP-Versions { $matchingVersions = @() foreach ($v in $installedVersions) { - if ($v -like "$version*") { - $matchingVersions += ($v -replace 'php', '') + if ($v.dirName -like "$version*") { + $matchingVersions += ($v.dirName -replace 'php', '') } } diff --git a/src/actions/list.ps1 b/src/actions/list.ps1 index 15df647..fbfb211 100644 --- a/src/actions/list.ps1 +++ b/src/actions/list.ps1 @@ -1,5 +1,6 @@ function Get-From-Source { + param ($arch = $null) try { $urls = Get-Source-Urls @@ -19,8 +20,9 @@ function Get-From-Source { $fetchedVersions = $fetchedVersions + ($filteredLinks | ForEach-Object { $_.href }) } - $arch = if (Is-OS-64Bit) { 'x64' } else { 'x86' } - $fetchedVersions = $fetchedVersions | Where-Object { $_ -match "$arch" } + if ($null -ne $arch) { + $fetchedVersions = $fetchedVersions | Where-Object { $_ -match $arch } + } $fetchedVersionsGrouped = [ordered]@{ 'Archives' = $fetchedVersions | Where-Object { $_ -match "archives" } @@ -46,6 +48,7 @@ function Get-From-Source { } function Get-PHP-List-To-Install { + param ($arch = $null) try { $cacheFile = "$CACHE_PATH\available_php_versions.json" $fetchedVersionsGrouped = @{} @@ -59,10 +62,10 @@ function Get-PHP-List-To-Install { if ($useCache) { $fetchedVersionsGrouped = Get-Data-From-Cache -cacheFileName "available_php_versions" if (-not $fetchedVersionsGrouped -or $fetchedVersionsGrouped.Count -eq 0) { - $fetchedVersionsGrouped = Get-From-Source + $fetchedVersionsGrouped = Get-From-Source -arch $arch } } else { - $fetchedVersionsGrouped = Get-From-Source + $fetchedVersionsGrouped = Get-From-Source -arch $arch } return $fetchedVersionsGrouped @@ -76,12 +79,12 @@ function Get-PHP-List-To-Install { } function Get-Available-PHP-Versions { - param($term = $null) + param($term = $null, $arch = $null) try { Write-Host "`nLoading available PHP versions..." - $fetchedVersionsGrouped = Get-PHP-List-To-Install + $fetchedVersionsGrouped = Get-PHP-List-To-Install -arch $arch if ($fetchedVersionsGrouped.Count -eq 0) { Write-Host "`nNo PHP versions found in the source. Please check your internet connection or the source URLs." @@ -118,7 +121,11 @@ function Get-Available-PHP-Versions { Write-Host "`n$key`n" $fetchedVersionsGroupe | ForEach-Object { $versionItem = $_ -replace '/downloads/releases/archives/|/downloads/releases/|php-|-Win.*|.zip', '' - Write-Host " $versionItem" + $fileName = $_ -split "/" + $fileName = $fileName[$fileName.Count - 1] + $arch = ($fileName -replace '.*\b(x64|x86)\b.*', '$1') + + Write-Host " $versionItem $arch" } } @@ -163,10 +170,14 @@ function Display-Installed-PHP-Versions { Write-Host "------------------" $duplicates = @() $installedPhp | ForEach-Object { - $versionNumber = $_ + $versionNumber = $_.name + $arch = $_.arch if ($duplicates -notcontains $versionNumber) { - $duplicates += $versionNumber + $duplicates += "$($versionNumber)_$($arch)" $isCurrent = "" + if ($arch) { + $versionNumber += " " + $arch + } if ($currentVersion -eq $versionNumber) { $isCurrent = "(Current)" } @@ -186,10 +197,10 @@ function Display-Installed-PHP-Versions { function Get-PHP-Versions-List { - param($available = $false, $term = $null) + param($available = $false, $term = $null, $arch = $null) if ($available) { - $result = Get-Available-PHP-Versions -term $term + $result = Get-Available-PHP-Versions -term $term -arch $arch } else { $result = Display-Installed-PHP-Versions -term $term } diff --git a/src/core/router.ps1 b/src/core/router.ps1 index 8168e22..f14621a 100644 --- a/src/core/router.ps1 +++ b/src/core/router.ps1 @@ -43,8 +43,14 @@ function Invoke-PVMCurrent { function Invoke-PVMList{ param($arguments) + $arch = Resolve-Arch -arch ($arguments | Where-Object { @('x86', 'x64') -contains $_ }) + if ($null -eq $arch) { + Write-Host "`nInvalid architecture specified. Allowed values are 'x86' or 'x64'." -ForegroundColor DarkYellow + return -1 + } + $term = ($arguments | Where-Object { $_ -match '^--search=(.+)$' }) -replace '^--search=', '' - $result = Get-PHP-Versions-List -available ($arguments -contains "available") -term $term + $result = Get-PHP-Versions-List -available ($arguments -contains "available") -term $term -arch $arch return $result } diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index 7348f5f..41bef24 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -383,3 +383,19 @@ function Format-Seconds { function Is-OS-64Bit { return [System.Environment]::Is64BitOperatingSystem } + +function Resolve-Arch { + param ($arch = $null) + + if ($null -eq $arch) { + $arch = if (Is-OS-64Bit) { 'x64' } else { 'x86' } + } + + $arch = $arch.ToLower() + + if (@('x86', 'x64') -notcontains $arch) { + return $null + } + + return $arch +} \ No newline at end of file From d58f425c0abf7ea10cdc6f31258b01c10005656a Mon Sep 17 00:00:00 2001 From: Driss B Date: Mon, 2 Feb 2026 21:45:00 +0100 Subject: [PATCH 02/69] Implement install by os arch (wip) --- src/actions/install.ps1 | 50 ++++++++++++++++++++++++----------------- src/core/router.ps1 | 8 ++++++- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/actions/install.ps1 b/src/actions/install.ps1 index 8bcddd3..4cadebf 100644 --- a/src/actions/install.ps1 +++ b/src/actions/install.ps1 @@ -20,7 +20,8 @@ function Get-PHP-Versions-From-Url { $version = $_.href -replace '/downloads/releases/archives/|/downloads/releases/|php-|-Win.*|.zip', '' $fileName = $_.href -split "/" $fileName = $fileName[$fileName.Count - 1] - $formattedList += @{ href = $_.href; version = $version; fileName = $fileName } + $arch = ($fileName -replace '.*\b(x64|x86)\b.*', '$1') + $formattedList += @{ href = $_.href; version = $version; fileName = $fileName; arch = $arch } } return $formattedList @@ -34,7 +35,7 @@ function Get-PHP-Versions-From-Url { } function Get-PHP-Versions { - param ($version) + param ($version, $arch = $null) try { $urls = Get-Source-Urls @@ -45,8 +46,9 @@ function Get-PHP-Versions { if ($fetched.Count -eq 0) { continue } - $sysArch = if (Is-OS-64Bit) { 'x64' } else { 'x86' } - $fetched = $fetched | Where-Object { $_.href -match $sysArch } + if ($null -ne $arch) { + $fetched = $fetched | Where-Object { $_.href -match $arch } + } if ($fetched.Count -eq 0) { continue } @@ -197,7 +199,7 @@ function Configure-Opcache { param ($version, $phpPath) try { - Write-Host "`nEnabling Opcache for PHP..." + Write-Host "`nConfiguring Opcache..." $phpIniPath = "$phpPath\php.ini" if (-not (Test-Path $phpIniPath)) { @@ -226,7 +228,7 @@ function Configure-Opcache { } function Select-Version { - param ($matchingVersions) + param ($matchingVersions, $arch = $null) $matchingVersionsPartialList = [ordered]@{} $matchingVersions.GetEnumerator() | ForEach-Object { @@ -238,7 +240,12 @@ function Select-Version { # There is exactly one key with one item $selectedVersionObject = $matchingKeys } else { - Write-Host "`nMatching PHP versions:" + $text = "`nMatching PHP versions:" + if ($null -ne $arch) { + $text += " ($arch)" + } + Write-Host $text + $index = 0 $matchingVersionsPartialList.GetEnumerator() | ForEach-Object { $key = $_.Key $versionsList = $_.Value @@ -247,8 +254,10 @@ function Select-Version { } Write-Host "`n$key versions:`n" $versionsList | ForEach-Object { + $_.index = $index $versionItem = $_.version -replace '/downloads/releases/archives/|/downloads/releases/|php-|-Win.*|.zip', '' - Write-Host " $versionItem" + Write-Host " [$index] $versionItem $($_.arch)" + $index++ } } @@ -256,18 +265,18 @@ function Select-Version { $msg += "`n Releases : $PHP_WIN_RELEASES_URL" $msg += "`n Archives : $PHP_WIN_ARCHIVES_URL" Write-Host $msg - $selectedVersionInput = Read-Host "`nEnter the exact version to install (or press Enter to cancel)" + $selectedVersionInput = Read-Host "`nInsert the [number] matching the version to install (or press Enter to cancel)" $selectedVersionInput = $selectedVersionInput.Trim() if (-not $selectedVersionInput) { return $null } - $selectedVersionObject = $matchingVersions.Values | ForEach-Object { - $_ | Where-Object { - $_.version -eq $selectedVersionInput - } - } | Where-Object { $_ } | Select-Object -First 1 + $selectedVersionObject = $matchingVersionsPartialList.GetEnumerator() | ForEach-Object { + $_.Value | Where-Object { + $_.index -eq $selectedVersionInput + } + } } if (-not $selectedVersionObject) { @@ -279,7 +288,7 @@ function Select-Version { } function Install-PHP { - param ($version) + param ($version, $arch = $null) try { if (Is-PHP-Version-Installed -version $version) { @@ -316,7 +325,7 @@ function Install-PHP { } Write-Host "`nLoading the matching versions..." - $matchingVersions = Get-PHP-Versions -version $version + $matchingVersions = Get-PHP-Versions -version $version -arch $arch if ($matchingVersions.Count -eq 0) { $msg = "No matching PHP versions found for '$version', Check one of the following:" @@ -327,7 +336,7 @@ function Install-PHP { return @{ code = -1; message = $msg } } - $selectedVersionObject = Select-Version -matchingVersions $matchingVersions + $selectedVersionObject = Select-Version -matchingVersions $matchingVersions -arch $arch if (-not $selectedVersionObject) { return @{ code = -1; message = "Installation cancelled" } } @@ -345,11 +354,12 @@ function Install-PHP { } Write-Host "`nExtracting the downloaded zip ..." - Extract-And-Configure -path "$destination\$($selectedVersionObject.fileName)" -fileNamePath "$destination\$($selectedVersionObject.version)" + $phpDirectoryName = "$($selectedVersionObject.version)_$arch" + Extract-And-Configure -path "$destination\$($selectedVersionObject.fileName)" -fileNamePath "$destination\$phpDirectoryName" - $opcacheEnabled = Configure-Opcache -version $version -phpPath "$destination\$($selectedVersionObject.version)" + $opcacheEnabled = Configure-Opcache -version $version -phpPath "$destination\$phpDirectoryName" - $message = "`nPHP $($selectedVersionObject.version) installed successfully at: '$destination\$($selectedVersionObject.version)'" + $message = "`nPHP $($selectedVersionObject.version) installed successfully at: '$destination\$phpDirectoryName'" $message += "`nRun 'pvm use $($selectedVersionObject.version)' to use this version" return @{ code = 0; message = $message; color = "DarkGreen" } diff --git a/src/core/router.ps1 b/src/core/router.ps1 index f14621a..b9f79f6 100644 --- a/src/core/router.ps1 +++ b/src/core/router.ps1 @@ -77,7 +77,13 @@ function Invoke-PVMInstall { return -1 } - $result = Install-PHP -version $version + $arch = Resolve-Arch -arch ($arguments | Where-Object { @('x86', 'x64') -contains $_ }) + if ($null -eq $arch) { + Write-Host "`nInvalid architecture specified. Allowed values are 'x86' or 'x64'." -ForegroundColor DarkYellow + return -1 + } + + $result = Install-PHP -version $version -arch $arch Display-Msg-By-ExitCode -result $result return 0 } From 612295689ee8d47a3bbc16ecf5b4887d6fc74b67 Mon Sep 17 00:00:00 2001 From: Driss B Date: Mon, 2 Feb 2026 21:47:01 +0100 Subject: [PATCH 03/69] Implement install ext by os arch (wip) --- src/actions/ini.ps1 | 36 ++++++++++++++++++++++++------------ src/core/router.ps1 | 13 +++++++++++-- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/actions/ini.ps1 b/src/actions/ini.ps1 index 7878a74..d746624 100644 --- a/src/actions/ini.ps1 +++ b/src/actions/ini.ps1 @@ -1,16 +1,25 @@ function Get-XDebug-FROM-URL { - param ($url, $version) + param ($url, $version, $arch = $null) try { $html = Invoke-WebRequest -Uri $url $links = $html.Links # Filter the links to find versions that match the given version - $sysArch = if (Is-OS-64Bit) { 'x86_64' } else { '' } + if ($null -ne $arch) { + $arch = if ($arch -eq 'x64') { 'x86_64' } else { '' } + } $filteredLinks = $links | Where-Object { - $_.href -match "php_xdebug-[\d\.a-zA-Z]+-$version-.*$sysArch\.dll" + if ($arch) { + return ($_.href -match "php_xdebug-[\d\.a-zA-Z]+-$version-.*$arch\.dll") + } + if ($arch -eq '') { + return ($_.href -match "php_xdebug-[\d\.a-zA-Z]+-$version-.*\.dll" -and $_.href -notmatch "x86_64") + } + + return $_.href -match "php_xdebug-[\d\.a-zA-Z]+-$version-.*\.dll" } # Return the filtered links (PHP version names) @@ -773,11 +782,11 @@ function Get-PHP-Data { } function Install-XDebug-Extension { - param ($iniPath) + param ($iniPath, $arch = $null) try { $currentVersion = (Get-Current-PHP-Version).version -replace '^(\d+\.\d+)\..*$', '$1' - $xDebugList = Get-XDebug-FROM-URL -url $XDEBUG_HISTORICAL_URL -version $currentVersion + $xDebugList = Get-XDebug-FROM-URL -url $XDEBUG_HISTORICAL_URL -version $currentVersion -arch $arch $xDebugList = $xDebugList | Sort-Object { [version]$_.xDebugVersion } -Descending if ($null -eq $xDebugList -or $xDebugList.Count -eq 0) { @@ -866,7 +875,7 @@ function Install-XDebug-Extension { } function Install-Extension { - param ($iniPath, $extName) + param ($iniPath, $extName, $arch = $null) try { try { @@ -941,7 +950,10 @@ function Install-Extension { $html = Invoke-WebRequest -Uri "$PECL_PACKAGE_ROOT_URL/$extName/$extVersion/windows" $packageLinks = $html.Links | Where-Object { $packageName = $_.href -replace "$PECL_WIN_EXT_DOWNLOAD_URL/$extName/$extVersion/", "" - if ($packageName -match "^php_$extName-$extVersion-(\d+\.\d+)-.+\.zip$") { + if ($null -eq $arch) { + $arch = '' + } + if ($packageName -match "^php_$extName-$extVersion-(\d+\.\d+)-.+$arch\.zip$") { $phpVersion = $matches[1] return ($phpVersion -eq $currentVersion) } @@ -1025,7 +1037,7 @@ function Install-Extension { } function Install-IniExtension { - param ($iniPath, $extName) + param ($iniPath, $extName, $arch = $null) try { if (-not $extName) { @@ -1034,9 +1046,9 @@ function Install-IniExtension { } if ($extName -like "*xdebug*") { - $code = Install-XDebug-Extension -iniPath $iniPath + $code = Install-XDebug-Extension -iniPath $iniPath -arch $arch } else { - $code = Install-Extension -iniPath $iniPath -extName $extName + $code = Install-Extension -iniPath $iniPath -extName $extName -arch $arch } if ($code -ne 0) { @@ -1211,7 +1223,7 @@ function List-PHP-Extensions { } function Invoke-PVMIniAction { - param ( $action, $params ) + param ( $action, $params, $arch = $null ) try { $exitCode = 1 @@ -1302,7 +1314,7 @@ function Invoke-PVMIniAction { Write-Host "`nInstalling extension(s): $($params -join ', ')" foreach ($extName in $params) { - $exitCode = Install-IniExtension -iniPath $iniPath -extName $extName + $exitCode = Install-IniExtension -iniPath $iniPath -extName $extName -arch $arch } } "list" { diff --git a/src/core/router.ps1 b/src/core/router.ps1 index b9f79f6..b70f67f 100644 --- a/src/core/router.ps1 +++ b/src/core/router.ps1 @@ -138,9 +138,18 @@ function Invoke-PVMIni { return -1 } - $remainingArgs = if ($arguments.Count -gt 1) { $arguments[1..($arguments.Count - 1)] } else { @() } + $arch = Resolve-Arch -arch ($arguments | Where-Object { @('x86', 'x64') -contains $_ }) + if ($null -eq $arch) { + Write-Host "`nInvalid architecture specified. Allowed values are 'x86' or 'x64'." -ForegroundColor DarkYellow + return -1 + } + + $remainingArgs = if ($arguments.Count -gt 1) { + $arguments[1..($arguments.Count - 1)] | Where-Object { $_ -ne $arch } + } else { @() } + - $exitCode = Invoke-PVMIniAction -action $action -params $remainingArgs + $exitCode = Invoke-PVMIniAction -action $action -params $remainingArgs -arch $arch return $exitCode } From 2d44d28054b47172cee2e7d9265c6d403a85d2e7 Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 15:19:28 +0100 Subject: [PATCH 04/69] Add helpers to get php accurate data --- src/actions/current.ps1 | 122 ++++++++++++----------- src/actions/use.ps1 | 202 ++++++++++++++++++++------------------ src/core/router.ps1 | 6 +- src/functions/helpers.ps1 | 35 +++++++ 4 files changed, 207 insertions(+), 158 deletions(-) diff --git a/src/actions/current.ps1 b/src/actions/current.ps1 index 723aeb1..38b5d6c 100644 --- a/src/actions/current.ps1 +++ b/src/actions/current.ps1 @@ -1,61 +1,63 @@ - -function Get-PHP-Status { - param($phpPath) - - $status = @{ opcache = $false; xdebug = $false } - try { - $phpIniPath = "$phpPath\php.ini" - if (-not (Test-Path $phpIniPath)) { - return $status - } - - $iniContent = Get-Content $phpIniPath - - foreach ($line in $iniContent) { - $trimmed = $line.Trim() - if ($trimmed -match '^(;)?\s*zend_extension\s*=.*opcache.*$') { - $status.opcache = -not $trimmed.StartsWith(';') - } - - if ($trimmed -match '^(;)?\s*zend_extension\s*=.*xdebug.*$') { - $status.xdebug = -not $trimmed.StartsWith(';') - } - } - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to retrieve PHP status" - exception = $_ - } - Write-Host "An error occurred while checking PHP status: $_" - } - - return $status -} - -function Get-Current-PHP-Version { - - try { - $emptyResult = @{ version = $null; path = $null; status = @{ opcache = $false; xdebug = $false } } - $currentPhpVersionPath = Get-Item $PHP_CURRENT_VERSION_PATH - if (-not $currentPhpVersionPath) { - return $emptyResult - } - - $currentPhpVersionPath = $currentPhpVersionPath.Target - if (-not (Is-Directory-Exists -path $currentPhpVersionPath)) { - return $emptyResult - } - - return @{ - version = $(Split-Path $currentPhpVersionPath -Leaf) - path = $currentPhpVersionPath - status = Get-PHP-Status -phpPath $currentPhpVersionPath - } - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to retrieve current PHP version" - exception = $_ - } - return $emptyResult - } + +function Get-PHP-Status { + param($phpPath) + + $status = @{ opcache = $false; xdebug = $false } + try { + $phpIniPath = "$phpPath\php.ini" + if (-not (Test-Path $phpIniPath)) { + return $status + } + + $iniContent = Get-Content $phpIniPath + + foreach ($line in $iniContent) { + $trimmed = $line.Trim() + if ($trimmed -match '^(;)?\s*zend_extension\s*=.*opcache.*$') { + $status.opcache = -not $trimmed.StartsWith(';') + } + + if ($trimmed -match '^(;)?\s*zend_extension\s*=.*xdebug.*$') { + $status.xdebug = -not $trimmed.StartsWith(';') + } + } + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to retrieve PHP status" + exception = $_ + } + Write-Host "An error occurred while checking PHP status: $_" + } + + return $status +} + +function Get-Current-PHP-Version { + + try { + $emptyResult = @{ version = $null; path = $null; status = @{ opcache = $false; xdebug = $false } } + $currentPhpVersionPath = Get-Item $PHP_CURRENT_VERSION_PATH + if (-not $currentPhpVersionPath) { + return $emptyResult + } + + $currentPhpVersionPath = $currentPhpVersionPath.Target + if (-not (Is-Directory-Exists -path $currentPhpVersionPath)) { + return $emptyResult + } + $phpInfo = Get-PHPInstallInfo -path $currentPhpVersionPath + + return @{ + version = $phpInfo.Version + arch = "$($phpInfo.BuildType) $($phpInfo.Arch)" + path = $phpInfo.InstallPath + status = Get-PHP-Status -phpPath $currentPhpVersionPath + } + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to retrieve current PHP version" + exception = $_ + } + return $emptyResult + } } \ No newline at end of file diff --git a/src/actions/use.ps1 b/src/actions/use.ps1 index 2c3a936..eb431c3 100644 --- a/src/actions/use.ps1 +++ b/src/actions/use.ps1 @@ -1,98 +1,106 @@ -function Detect-PHP-VersionFromProject { - - try { - # 1. Check .php-version - if (Test-Path ".php-version") { - $version = Get-Content ".php-version" | Select-Object -First 1 - return $version.Trim() - } - - # 2. Check composer.json - if (Test-Path "composer.json") { - try { - $json = Get-Content "composer.json" -Raw | ConvertFrom-Json - if ($json.require.php) { - $constraint = $json.require.php.Trim() - # Extract first PHP version number in the string (e.g. from "^8.3" or ">=8.1 <8.3") - if ($constraint -match "(\d+(\.\d+(\.\d+)?)?)") { - return $matches[1] - } - } - } catch { - Write-Host "`nFailed to parse composer.json: $_" - throw $_ - } - } - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to detect PHP version from project" - exception = $_ - } - } - - return $null -} - -function Update-PHP-Version { - param ($version) - - try { - $phpPath = Get-PHP-Path-By-Version -version $version - if (-not $phpPath) { - $installedVersions = Get-Matching-PHP-Versions -version $version - $pathVersionObject = Get-UserSelected-PHP-Version -installedVersions $installedVersions - } else { - $pathVersionObject = @{ code = 0; version = $version; path = $phpPath } - } - - if (-not $pathVersionObject) { - return @{ code = -1; message = "PHP version $version was not found!"; color = "DarkYellow"} - } - - if ($pathVersionObject.code -ne 0) { - return $pathVersionObject - } - - $currentVersion = Get-Current-PHP-Version - if ($currentVersion -and $currentVersion.version) { - if ($pathVersionObject.version -eq $currentVersion.version) { - return @{ code = 0; message = "Already using PHP $($pathVersionObject.version)"; color = "DarkCyan"} - } - } - - if (-not $pathVersionObject.path) { - return @{ code = -1; message = "PHP version $($pathVersionObject.version) was not found!"; color = "DarkYellow"} - } - $linkCreated = Make-Symbolic-Link -link $PHP_CURRENT_VERSION_PATH -target $pathVersionObject.path - if ($linkCreated.code -ne 0) { - return $linkCreated - } - return @{ code = 0; message = "Now using PHP $($pathVersionObject.version)"; color = "DarkGreen"} - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to update PHP version to '$version'" - exception = $_ - } - return @{ code = -1; message = "No matching PHP versions found for '$version', Use 'pvm list' to see installed versions."; color = "DarkYellow"} - } -} - -function Auto-Select-PHP-Version { - - $version = Detect-PHP-VersionFromProject - - if (-not $version) { - return @{ code = -1; message = "Could not detect PHP version from .php-version or composer.json"; color = "DarkYellow"} - } - - Write-Host "`nDetected PHP version from project: $version" - - $installedVersions = Get-Matching-PHP-Versions -version $version - if (-not $installedVersions) { - $message = "PHP '$version' is not installed." - $message += "`nRun: pvm install $version" - return @{ code = -1; version = $version; message = $message; } - } - - return @{ code = 0; version = $version; } +function Detect-PHP-VersionFromProject { + + try { + # 1. Check .php-version + if (Test-Path ".php-version") { + $version = Get-Content ".php-version" | Select-Object -First 1 + return $version.Trim() + } + + # 2. Check composer.json + if (Test-Path "composer.json") { + try { + $json = Get-Content "composer.json" -Raw | ConvertFrom-Json + if ($json.require.php) { + $constraint = $json.require.php.Trim() + # Extract first PHP version number in the string (e.g. from "^8.3" or ">=8.1 <8.3") + if ($constraint -match "(\d+(\.\d+(\.\d+)?)?)") { + return $matches[1] + } + } + } catch { + Write-Host "`nFailed to parse composer.json: $_" + throw $_ + } + } + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to detect PHP version from project" + exception = $_ + } + } + + return $null +} + +function Update-PHP-Version { + param ($version) + + try { + $phpPath = Get-PHP-Path-By-Version -version $version + if (-not $phpPath) { + $installedVersions = Get-Matching-PHP-Versions -version $version + $pathVersionObject = Get-UserSelected-PHP-Version -installedVersions $installedVersions + } else { + $pathVersionObject = @{ code = 0; version = $version; path = $phpPath } + } + + if (-not $pathVersionObject) { + return @{ code = -1; message = "PHP version $version was not found!"; color = "DarkYellow"} + } + + if ($pathVersionObject.code -ne 0) { + return $pathVersionObject + } + + $currentVersion = Get-Current-PHP-Version + if ($currentVersion -and $currentVersion.version) { + if ($pathVersionObject.version -eq $currentVersion.version -and + $pathVersionObject.arch -eq $currentVersion.arch) { + return @{ code = 0; message = "Already using PHP $($pathVersionObject.version)"; color = "DarkCyan"} + } + } + + if (-not $pathVersionObject.path) { + return @{ code = -1; message = "PHP version $($pathVersionObject.version) was not found!"; color = "DarkYellow"} + } + $linkCreated = Make-Symbolic-Link -link $PHP_CURRENT_VERSION_PATH -target $pathVersionObject.path + if ($linkCreated.code -ne 0) { + return $linkCreated + } + $text = "Now using PHP $($pathVersionObject.version)" + if ($pathVersionObject.arch) { + $text += " ($($pathVersionObject.arch))" + } else { + $dll = Get-ChildItem "$currentPhpVersionPath\php*nts.dll","$currentPhpVersionPath\php*ts.dll" | Select-Object -First 1 + $arch = Get-BinaryArchitecture $dll.FullName + } + return @{ code = 0; message = $text; color = "DarkGreen"} + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to update PHP version to '$version'" + exception = $_ + } + return @{ code = -1; message = "No matching PHP versions found for '$version', Use 'pvm list' to see installed versions."; color = "DarkYellow"} + } +} + +function Auto-Select-PHP-Version { + + $version = Detect-PHP-VersionFromProject + + if (-not $version) { + return @{ code = -1; message = "Could not detect PHP version from .php-version or composer.json"; color = "DarkYellow"} + } + + Write-Host "`nDetected PHP version from project: $version" + + $installedVersions = Get-Matching-PHP-Versions -version $version + if (-not $installedVersions) { + $message = "PHP '$version' is not installed." + $message += "`nRun: pvm install $version" + return @{ code = -1; version = $version; message = $message; } + } + + return @{ code = 0; version = $version; } } \ No newline at end of file diff --git a/src/core/router.ps1 b/src/core/router.ps1 index b70f67f..0ca8f4a 100644 --- a/src/core/router.ps1 +++ b/src/core/router.ps1 @@ -21,7 +21,11 @@ function Invoke-PVMCurrent { Write-Host "`nNo PHP version is currently set. Please use 'pvm use ' to set a version." return -1 } - Write-Host "`nRunning version: PHP $($result.version)" + $text = "`nRunning version: PHP $($result.version)" + if ($result.arch) { + $text += " ($($result.arch))" + } + Write-Host $text if (-not $result.status) { Write-Host "No status information available for the current PHP version." -ForegroundColor Yellow diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index 41bef24..62b207e 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -398,4 +398,39 @@ function Resolve-Arch { } return $arch +} + +function Get-PHPInstallInfo { + param ($path) + + $dll = Get-ChildItem "$path\php*nts.dll","$path\php*ts.dll" -ErrorAction SilentlyContinue | Select-Object -First 1 + + if (-not $dll) { + return $null + } + + return @{ + Version = $dll.VersionInfo.ProductVersion + Arch = Get-BinaryArchitecture-From-DLL -path $dll.FullName + BuildType = if ($dll.Name -match 'nts') { 'NTS' } else { 'TS' } + Dll = $dll.Name + InstallPath = $path + } +} + + +function Get-BinaryArchitecture-From-DLL { + param ($path) + + $bytes = [System.IO.File]::ReadAllBytes($path) + + $peOffset = [BitConverter]::ToInt32($bytes, 0x3C) + + $machine = [BitConverter]::ToUInt16($bytes, $peOffset + 4) + + switch ($machine) { + 0x8664 { "x64" } + 0x014c { "x86" } + default { "Unknown" } + } } \ No newline at end of file From 163128a2ca4f607b3b6b64a28539af51cd5582e0 Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 16:31:05 +0100 Subject: [PATCH 05/69] Add Can-Use-Cache helper --- src/actions/ini.ps1 | 8 +------- src/actions/list.ps1 | 8 +------- src/functions/helpers.ps1 | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/actions/ini.ps1 b/src/actions/ini.ps1 index d746624..3a59cee 100644 --- a/src/actions/ini.ps1 +++ b/src/actions/ini.ps1 @@ -1144,13 +1144,7 @@ function List-PHP-Extensions { } else { Write-Host "`nLoading available extensions..." - $cacheFile = "$CACHE_PATH\available_extensions.json" - $useCache = $false - - if (Test-Path $cacheFile) { - $fileAgeHours = (New-TimeSpan -Start (Get-Item $cacheFile).LastWriteTime -End (Get-Date)).TotalHours - $useCache = ($fileAgeHours -lt $CACHE_MAX_HOURS) - } + $useCache = Can-Use-Cache -cacheFileName 'available_extensions' if ($useCache) { $availableExtensions = Get-Data-From-Cache -cacheFileName "available_extensions" diff --git a/src/actions/list.ps1 b/src/actions/list.ps1 index fbfb211..564d09a 100644 --- a/src/actions/list.ps1 +++ b/src/actions/list.ps1 @@ -50,14 +50,8 @@ function Get-From-Source { function Get-PHP-List-To-Install { param ($arch = $null) try { - $cacheFile = "$CACHE_PATH\available_php_versions.json" $fetchedVersionsGrouped = @{} - $useCache = $false - - if (Test-Path $cacheFile) { - $fileAgeHours = (New-TimeSpan -Start (Get-Item $cacheFile).LastWriteTime -End (Get-Date)).TotalHours - $useCache = ($fileAgeHours -lt $CACHE_MAX_HOURS) - } + $useCache = Can-Use-Cache -cacheFileName 'available_php_versions' if ($useCache) { $fetchedVersionsGrouped = Get-Data-From-Cache -cacheFileName "available_php_versions" diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index 62b207e..8363bae 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -3,6 +3,20 @@ function Get-Zend-Extensions-List { return @('xdebug', 'opcache') } +function Can-Use-Cache { + param ($cacheFileName) + + $path = "$CACHE_PATH\$cacheFileName.json" + $useCache = $false + + if (Test-Path $cacheFileName) { + $fileAgeHours = (New-TimeSpan -Start (Get-Item $cacheFileName).LastWriteTime -End (Get-Date)).TotalHours + $useCache = ($fileAgeHours -lt $CACHE_MAX_HOURS) + } + + return $useCache +} + function Get-Data-From-Cache { param ($cacheFileName) From e847862e6e6f0c509ac81721bb2665dec8ff6c6b Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 16:33:00 +0100 Subject: [PATCH 06/69] Remove formatting fetched data from Get-Data-From-Cache --- src/actions/ini.ps1 | 4 ++-- src/actions/list.ps1 | 2 +- src/functions/helpers.ps1 | 9 +-------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/actions/ini.ps1 b/src/actions/ini.ps1 index 3a59cee..c44244e 100644 --- a/src/actions/ini.ps1 +++ b/src/actions/ini.ps1 @@ -1161,7 +1161,7 @@ function List-PHP-Extensions { } $availableExtensionsPartialList = @{} - $availableExtensions.GetEnumerator() | ForEach-Object { + $availableExtensions.PSObject.Properties | ForEach-Object { $searchResult = $_.Value if ($term) { if ($_.Key -notlike "*$term*") { @@ -1172,7 +1172,7 @@ function List-PHP-Extensions { } } if ($searchResult.Count -gt 0) { - $availableExtensionsPartialList[$_.Key] = $searchResult | Select-Object -Last 10 + $availableExtensionsPartialList[$_.Name] = $searchResult | Select-Object -Last 10 } } diff --git a/src/actions/list.ps1 b/src/actions/list.ps1 index 564d09a..bb1c19e 100644 --- a/src/actions/list.ps1 +++ b/src/actions/list.ps1 @@ -94,7 +94,7 @@ function Get-Available-PHP-Versions { } } if ($searchResult.Count -ne 0) { - $fetchedVersionsGroupedPartialList[$_.Key] = $searchResult | Select-Object -Last $LATEST_VERSION_COUNT + $fetchedVersionsGroupedPartialList[$_.Name] = $searchResult | Select-Object -Last $LATEST_VERSION_COUNT } } diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index 8363bae..7fbd599 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -24,14 +24,7 @@ function Get-Data-From-Cache { $list = @{} try { $jsonData = Get-Content $path | ConvertFrom-Json - $jsonData.PSObject.Properties.GetEnumerator() | ForEach-Object { - $key = $_.Name - $value = $_.Value - - # Add the key-value pair to the hashtable - $list[$key] = $value - } - return $list + return $jsonData } catch { $logged = Log-Data -data @{ header = "$($MyInvocation.MyCommand.Name) - Failed to get data from cache" From ba7e4de238ec4aa2fc737009f585c4c018d93199 Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 16:35:18 +0100 Subject: [PATCH 07/69] Refactor Resolve-Arch --- src/core/router.ps1 | 18 +++--------------- src/functions/helpers.ps1 | 12 ++++++------ 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/src/core/router.ps1 b/src/core/router.ps1 index 0ca8f4a..a2284f1 100644 --- a/src/core/router.ps1 +++ b/src/core/router.ps1 @@ -47,11 +47,7 @@ function Invoke-PVMCurrent { function Invoke-PVMList{ param($arguments) - $arch = Resolve-Arch -arch ($arguments | Where-Object { @('x86', 'x64') -contains $_ }) - if ($null -eq $arch) { - Write-Host "`nInvalid architecture specified. Allowed values are 'x86' or 'x64'." -ForegroundColor DarkYellow - return -1 - } + $arch = Resolve-Arch -arguments $arguments $term = ($arguments | Where-Object { $_ -match '^--search=(.+)$' }) -replace '^--search=', '' $result = Get-PHP-Versions-List -available ($arguments -contains "available") -term $term -arch $arch @@ -81,11 +77,7 @@ function Invoke-PVMInstall { return -1 } - $arch = Resolve-Arch -arch ($arguments | Where-Object { @('x86', 'x64') -contains $_ }) - if ($null -eq $arch) { - Write-Host "`nInvalid architecture specified. Allowed values are 'x86' or 'x64'." -ForegroundColor DarkYellow - return -1 - } + $arch = Resolve-Arch -arguments $arguments $result = Install-PHP -version $version -arch $arch Display-Msg-By-ExitCode -result $result @@ -142,11 +134,7 @@ function Invoke-PVMIni { return -1 } - $arch = Resolve-Arch -arch ($arguments | Where-Object { @('x86', 'x64') -contains $_ }) - if ($null -eq $arch) { - Write-Host "`nInvalid architecture specified. Allowed values are 'x86' or 'x64'." -ForegroundColor DarkYellow - return -1 - } + $arch = Resolve-Arch -arguments $arguments $remainingArgs = if ($arguments.Count -gt 1) { $arguments[1..($arguments.Count - 1)] | Where-Object { $_ -ne $arch } diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index 7fbd599..a03fd1d 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -392,16 +392,16 @@ function Is-OS-64Bit { } function Resolve-Arch { - param ($arch = $null) + param ($arguments, $choseDefault = $false) - if ($null -eq $arch) { + $arch = $arguments | Where-Object { @('x86', 'x64') -contains $_ } + + if ($null -eq $arch -and $choseDefault) { $arch = if (Is-OS-64Bit) { 'x64' } else { 'x86' } } - $arch = $arch.ToLower() - - if (@('x86', 'x64') -notcontains $arch) { - return $null + if ($arch -ne $null) { + $arch = $arch.ToLower() } return $arch From be004c6be8f83a43187e77a25d8616abaa5f51af Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 16:39:09 +0100 Subject: [PATCH 08/69] Refactor php list commands and include nts versions --- src/actions/common.ps1 | 41 +++++++++++++++++++++------ src/actions/list.ps1 | 64 +++++++++++++++++++++++++++--------------- 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/src/actions/common.ps1 b/src/actions/common.ps1 index 953fa61..7dbc51b 100644 --- a/src/actions/common.ps1 +++ b/src/actions/common.ps1 @@ -37,18 +37,43 @@ function Is-PVM-Setup { } } +function Get-Installed-PHP-Versions-From-Directory { + $directories = Get-All-Subdirectories -path "$STORAGE_PATH\php" + $installedVersions = $directories | ForEach-Object { + if (Test-Path "$($_.FullName)\php.exe"){ + $phpInfo = Get-PHPInstallInfo -path $_.FullName + + return $phpInfo + } + return $null + } + + $installedVersions = ($installedVersions | Sort-Object { [version]$_.Version }) + + return $installedVersions +} + function Get-Installed-PHP-Versions { - + param ($arch = $null) try { - $directories = Get-All-Subdirectories -path "$STORAGE_PATH\php" - $names = $directories | ForEach-Object { - $filenameParts = $_.Name -split '_' - if (Test-Path "$($_.FullName)\php.exe"){ - return @{ name = $filenameParts[0]; arch = $filenameParts[1]; dirName = $_.Name } + $useCache = Can-Use-Cache -cacheFileName 'installed_php_versions' + + if ($useCache) { + $installedVersions = Get-Data-From-Cache -cacheFileName "installed_php_versions" + if (-not $installedVersions -or $installedVersions.Count -eq 0) { + $installedVersions = Get-Installed-PHP-Versions-From-Directory + $cached = Cache-Data -cacheFileName "installed_php_versions" -data $installedVersions -depth 1 } - return $null + } else { + $installedVersions = Get-Installed-PHP-Versions-From-Directory + $cached = Cache-Data -cacheFileName "installed_php_versions" -data $installedVersions -depth 1 + } + + if ($arch) { + $installedVersions = $installedVersions | Where-Object { $_.Arch -eq $arch } } - return ($names | Sort-Object { [version]$_.name }) + + return $installedVersions } catch { $logged = Log-Data -data @{ header = "$($MyInvocation.MyCommand.Name) - Failed to retrieve installed PHP versions" diff --git a/src/actions/list.ps1 b/src/actions/list.ps1 index bb1c19e..647be37 100644 --- a/src/actions/list.ps1 +++ b/src/actions/list.ps1 @@ -10,23 +10,35 @@ function Get-From-Source { $links = $html.Links # Filter the links to find versions that match the given version - $filteredLinks = $links | Where-Object { - $_.href -match "php-\d+\.\d+\.\d+(?:-\d+)?-Win32.*\.zip$" -and - $_.href -notmatch "php-debug" -and - $_.href -notmatch "php-devel" -and - $_.href -notmatch "nts" + $filteredLinks = @() + $links | ForEach-Object { + if ($_.href -match "php-\d+\.\d+\.\d+(?:-\d+)?-(?:nts-)?Win32.*\.zip$" -and + $_.href -notmatch "php-debug" -and + $_.href -notmatch "php-devel" # -and $_.href -notmatch "nts" + ) { + $fileName = $_.href -split "/" + $fileName = $fileName[$fileName.Count - 1] + $BuildType = if ($fileName -match 'nts') { 'NTS' } else { 'TS' } + + $filteredLinks += @{ + Version = ($_.href -replace '/downloads/releases/archives/|/downloads/releases/|php-|-nts|-Win.*|\.zip', '') + Arch = ($fileName -replace '.*\b(x64|x86)\b.*', '$1') + BuildType = $BuildType + Link = $_.href + } + } } # Return the filtered links (PHP version names) - $fetchedVersions = $fetchedVersions + ($filteredLinks | ForEach-Object { $_.href }) + $fetchedVersions = $fetchedVersions + $filteredLinks # ($filteredLinks | ForEach-Object { $_.href }) } if ($null -ne $arch) { - $fetchedVersions = $fetchedVersions | Where-Object { $_ -match $arch } + $fetchedVersions = $fetchedVersions | Where-Object { $_.Arch -match $arch } } $fetchedVersionsGrouped = [ordered]@{ - 'Archives' = $fetchedVersions | Where-Object { $_ -match "archives" } - 'Releases' = $fetchedVersions | Where-Object { $_ -notmatch "archives" } + 'Archives' = $fetchedVersions | Where-Object { $_.Link -match "archives" } + 'Releases' = $fetchedVersions | Where-Object { $_.Link -notmatch "archives" } } if ($fetchedVersionsGrouped.Count -eq 0 -or @@ -90,7 +102,7 @@ function Get-Available-PHP-Versions { $searchResult = $_.Value if ($term) { $searchResult = $searchResult | Where-Object { - $_ -like "*php-$term*" + $_.Version -like "$term*" } } if ($searchResult.Count -ne 0) { @@ -114,12 +126,12 @@ function Get-Available-PHP-Versions { } Write-Host "`n$key`n" $fetchedVersionsGroupe | ForEach-Object { - $versionItem = $_ -replace '/downloads/releases/archives/|/downloads/releases/|php-|-Win.*|.zip', '' - $fileName = $_ -split "/" - $fileName = $fileName[$fileName.Count - 1] - $arch = ($fileName -replace '.*\b(x64|x86)\b.*', '$1') + $version = $_.Version + $arch = $_.Arch + $buildType = $_.BuildType + $versionNumber = "$version ".PadRight(15, '.') - Write-Host " $versionItem $arch" + Write-Host " $versionNumber $arch $buildType" } } @@ -138,14 +150,14 @@ function Get-Available-PHP-Versions { } function Display-Installed-PHP-Versions { - param ($term) + param ($term = $null, $arch = $null) try { $currentVersion = Get-Current-PHP-Version if ($currentVersion -and $currentVersion.version) { $currentVersion = $currentVersion.version } - $installedPhp = Get-Installed-PHP-Versions + $installedPhp = Get-Installed-PHP-Versions -arch $arch if ($installedPhp.Count -eq 0) { Write-Host "`nNo PHP versions found" @@ -153,7 +165,7 @@ function Display-Installed-PHP-Versions { } if ($term) { - $installedPhp = $installedPhp | Where-Object { $_ -like "$term*" } + $installedPhp = $installedPhp | Where-Object { $_.Version -like "$term*" } if ($installedPhp.Count -eq 0) { Write-Host "`nNo PHP versions found matching '$term'" return -1 @@ -164,18 +176,24 @@ function Display-Installed-PHP-Versions { Write-Host "------------------" $duplicates = @() $installedPhp | ForEach-Object { - $versionNumber = $_.name - $arch = $_.arch + $versionNumber = $_.Version + $arch = $_.Arch + $buildType = $_.BuildType if ($duplicates -notcontains $versionNumber) { $duplicates += "$($versionNumber)_$($arch)" $isCurrent = "" + $metaData = "" if ($arch) { - $versionNumber += " " + $arch + $metaData += $arch + " " + } + if ($buildType) { + $metaData += $buildType } if ($currentVersion -eq $versionNumber) { $isCurrent = "(Current)" } - Write-Host " $versionNumber $isCurrent" + $versionNumber = "$versionNumber ".PadRight(15, '.') + Write-Host " $versionNumber $metaData $isCurrent" } } return 0 @@ -196,7 +214,7 @@ function Get-PHP-Versions-List { if ($available) { $result = Get-Available-PHP-Versions -term $term -arch $arch } else { - $result = Display-Installed-PHP-Versions -term $term + $result = Display-Installed-PHP-Versions -term $term -arch $arch } return $result From 3c32db7ff89de36fad178baf65e50e4de6b5d69a Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 19:58:43 +0100 Subject: [PATCH 09/69] Add buildType property to returned object in Get-Current-PHP-Version --- src/actions/current.ps1 | 3 ++- src/core/router.ps1 | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/actions/current.ps1 b/src/actions/current.ps1 index 38b5d6c..0a15c4b 100644 --- a/src/actions/current.ps1 +++ b/src/actions/current.ps1 @@ -49,7 +49,8 @@ function Get-Current-PHP-Version { return @{ version = $phpInfo.Version - arch = "$($phpInfo.BuildType) $($phpInfo.Arch)" + arch = $phpInfo.Arch + buildType = $phpInfo.BuildType path = $phpInfo.InstallPath status = Get-PHP-Status -phpPath $currentPhpVersionPath } diff --git a/src/core/router.ps1 b/src/core/router.ps1 index a2284f1..f8ef700 100644 --- a/src/core/router.ps1 +++ b/src/core/router.ps1 @@ -22,8 +22,11 @@ function Invoke-PVMCurrent { return -1 } $text = "`nRunning version: PHP $($result.version)" + if ($result.buildType) { + $text += " $($result.buildType)" + } if ($result.arch) { - $text += " ($($result.arch))" + $text += " $($result.arch)" } Write-Host $text From b093d8abee21429d54fcc888a8a19047d694588d Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 20:11:14 +0100 Subject: [PATCH 10/69] Refactor checking for current php version --- src/actions/list.ps1 | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/actions/list.ps1 b/src/actions/list.ps1 index 647be37..6a1dac2 100644 --- a/src/actions/list.ps1 +++ b/src/actions/list.ps1 @@ -18,12 +18,11 @@ function Get-From-Source { ) { $fileName = $_.href -split "/" $fileName = $fileName[$fileName.Count - 1] - $BuildType = if ($fileName -match 'nts') { 'NTS' } else { 'TS' } $filteredLinks += @{ Version = ($_.href -replace '/downloads/releases/archives/|/downloads/releases/|php-|-nts|-Win.*|\.zip', '') Arch = ($fileName -replace '.*\b(x64|x86)\b.*', '$1') - BuildType = $BuildType + BuildType = if ($fileName -match 'nts') { 'NTS' } else { 'TS' } Link = $_.href } } @@ -154,9 +153,6 @@ function Display-Installed-PHP-Versions { try { $currentVersion = Get-Current-PHP-Version - if ($currentVersion -and $currentVersion.version) { - $currentVersion = $currentVersion.version - } $installedPhp = Get-Installed-PHP-Versions -arch $arch if ($installedPhp.Count -eq 0) { @@ -177,19 +173,21 @@ function Display-Installed-PHP-Versions { $duplicates = @() $installedPhp | ForEach-Object { $versionNumber = $_.Version - $arch = $_.Arch - $buildType = $_.BuildType - if ($duplicates -notcontains $versionNumber) { - $duplicates += "$($versionNumber)_$($arch)" + $versionID = "$($_.Version)_$($_.buildType)_$($_.Arch)" + if ($duplicates -notcontains $versionID) { + $duplicates += $versionID $isCurrent = "" $metaData = "" - if ($arch) { - $metaData += $arch + " " + if ($_.Arch) { + $metaData += $_.Arch + " " } - if ($buildType) { - $metaData += $buildType + if ($_.BuildType) { + $metaData += $_.BuildType } - if ($currentVersion -eq $versionNumber) { + if ($currentVersion.version -eq $_.version -and + $currentVersion.arch -eq $_.arch -and + $currentVersion.buildType -eq $_.BuildType + ) { $isCurrent = "(Current)" } $versionNumber = "$versionNumber ".PadRight(15, '.') From e44166871bd9a835042127bb768edd534be0fc9f Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 20:14:14 +0100 Subject: [PATCH 11/69] Refactor Is-PHP-Version-Installed --- src/actions/common.ps1 | 8 ++++++-- src/actions/install.ps1 | 8 +------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/actions/common.ps1 b/src/actions/common.ps1 index 7dbc51b..84996a1 100644 --- a/src/actions/common.ps1 +++ b/src/actions/common.ps1 @@ -146,8 +146,12 @@ function Is-PHP-Version-Installed { param ($version) try { - $installedVersions = Get-Matching-PHP-Versions -version $version - return ($installedVersions -contains $version) + $installedVersions = Get-Matching-PHP-Versions -version $version.version + return ($installedVersions | Where-Object { + $_.Version -eq $version.version -and + $_.Arch -eq $version.arch -and + $_.BuildType -eq $version.BuildType + }) } catch { $logged = Log-Data -data @{ header = "$($MyInvocation.MyCommand.Name) - Failed to check if PHP version $version is installed" diff --git a/src/actions/install.ps1 b/src/actions/install.ps1 index 4cadebf..fdf3dc1 100644 --- a/src/actions/install.ps1 +++ b/src/actions/install.ps1 @@ -291,12 +291,6 @@ function Install-PHP { param ($version, $arch = $null) try { - if (Is-PHP-Version-Installed -version $version) { - $message = "Version '$($version)' already installed." - $message += "`nRun: pvm use $version" - return @{ code = -1; message = $message } - } - $foundInstalledVersions = Get-Matching-PHP-Versions -version $version if ($foundInstalledVersions) { @@ -341,7 +335,7 @@ function Install-PHP { return @{ code = -1; message = "Installation cancelled" } } - if (Is-PHP-Version-Installed -version $selectedVersionObject.version) { + if (Is-PHP-Version-Installed -version $selectedVersionObject) { $message = "Version '$($selectedVersionObject.version)' already installed" $message += "`nRun: pvm use $($selectedVersionObject.version)" return @{ code = -1; message = $message } From 3095b002ef78fd66d0893573d6b992cda53a05b6 Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 20:16:14 +0100 Subject: [PATCH 12/69] Refactor checking for current php version --- src/actions/install.ps1 | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/actions/install.ps1 b/src/actions/install.ps1 index fdf3dc1..c19a14a 100644 --- a/src/actions/install.ps1 +++ b/src/actions/install.ps1 @@ -296,18 +296,27 @@ function Install-PHP { if ($foundInstalledVersions) { if ($version -match '^(\d+)(?:\.(\d+))?') { $currentVersion = Get-Current-PHP-Version - if ($currentVersion -and $currentVersion.version) { - $currentVersion = $currentVersion.version - } $familyVersion = $matches[0] Write-Host "`nOther versions from the $familyVersion.x family are available:" $foundInstalledVersions | ForEach-Object { - $versionNumber = $_ + $versionNumber = $_.Version $isCurrent = "" - if ($currentVersion -eq $versionNumber) { + $metaData = "" + if ($_.Arch) { + $metaData += $_.Arch + " " + } + if ($_.BuildType) { + $metaData += $_.BuildType + } + if ($currentVersion -and + $currentVersion.version -eq $_.version -and + $currentVersion.arch -eq $_.arch -and + $currentVersion.buildType -eq $_.BuildType + ) { $isCurrent = "(Current)" } - Write-Host " - $versionNumber $isCurrent" + $versionNumber = "$versionNumber ".PadRight(15, '.') + Write-Host " $versionNumber $metaData $isCurrent" } $response = Read-Host "`nWould you like to install another version from the $familyVersion.x ? (y/n)" $response = $response.Trim() From a4087dae525a26d9f4d42443257d74f29f0fc70f Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 20:17:31 +0100 Subject: [PATCH 13/69] Refactor and include nts versions --- src/actions/install.ps1 | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/actions/install.ps1 b/src/actions/install.ps1 index c19a14a..871f65c 100644 --- a/src/actions/install.ps1 +++ b/src/actions/install.ps1 @@ -8,20 +8,24 @@ function Get-PHP-Versions-From-Url { # Filter the links to find versions that match the given version $filteredLinks = $links | Where-Object { - $_.href -match "php-$version(\.\d+)*-win.*\.zip$" -and + $_.href -match "php-$version(\.\d+)*-(?:nts-)?win.*\.zip$" -and $_.href -notmatch "php-debug" -and - $_.href -notmatch "php-devel" -and - $_.href -notmatch "nts" + $_.href -notmatch "php-devel" # -and $_.href -notmatch "nts" } # Return the filtered links (PHP version names) $formattedList = @() $filteredLinks = $filteredLinks | ForEach-Object { - $version = $_.href -replace '/downloads/releases/archives/|/downloads/releases/|php-|-Win.*|.zip', '' + $version = $_.href -replace '/downloads/releases/archives/|/downloads/releases/|php-|-nts|-Win.*|.zip', '' $fileName = $_.href -split "/" $fileName = $fileName[$fileName.Count - 1] - $arch = ($fileName -replace '.*\b(x64|x86)\b.*', '$1') - $formattedList += @{ href = $_.href; version = $version; fileName = $fileName; arch = $arch } + $formattedList += @{ + href = $_.href + version = $version + fileName = $fileName + BuildType = if ($fileName -match 'nts') { 'NTS' } else { 'TS' } + arch = ($fileName -replace '.*\b(x64|x86)\b.*', '$1') + } } return $formattedList @@ -99,6 +103,8 @@ function Download-PHP { $fileName = $versionObject.fileName $version = $versionObject.version + $buildType = $versionObject.BuildType + $arch = $versionObject.arch $destination = "$STORAGE_PATH\php" $created = Make-Directory -path $destination @@ -107,7 +113,7 @@ function Download-PHP { return $null } - Write-Host "`nDownloading PHP $version..." + Write-Host "`nDownloading PHP $version ($buildType $arch)..." foreach ($key in $urls.Keys) { $_url = $urls[$key] @@ -255,8 +261,7 @@ function Select-Version { Write-Host "`n$key versions:`n" $versionsList | ForEach-Object { $_.index = $index - $versionItem = $_.version -replace '/downloads/releases/archives/|/downloads/releases/|php-|-Win.*|.zip', '' - Write-Host " [$index] $versionItem $($_.arch)" + Write-Host " [$index] $($_.version) $($_.arch) $($_.BuildType)" $index++ } } @@ -357,7 +362,7 @@ function Install-PHP { } Write-Host "`nExtracting the downloaded zip ..." - $phpDirectoryName = "$($selectedVersionObject.version)_$arch" + $phpDirectoryName = "$($selectedVersionObject.version)_$($selectedVersionObject.BuildType)_$($selectedVersionObject.arch)" Extract-And-Configure -path "$destination\$($selectedVersionObject.fileName)" -fileNamePath "$destination\$phpDirectoryName" $opcacheEnabled = Configure-Opcache -version $version -phpPath "$destination\$phpDirectoryName" From 362396e271bbb6bed378b94bd22ae222ffd62b3e Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 20:18:15 +0100 Subject: [PATCH 14/69] Refresh cached list after downloading new php version --- src/actions/common.ps1 | 16 ++++++++++++++++ src/actions/install.ps1 | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/actions/common.ps1 b/src/actions/common.ps1 index 84996a1..f2d118b 100644 --- a/src/actions/common.ps1 +++ b/src/actions/common.ps1 @@ -37,6 +37,22 @@ function Is-PVM-Setup { } } +function Refresh-Installed-PHP-Versions-Cache { + try { + $installedVersions = Get-Installed-PHP-Versions-From-Directory + $cached = Cache-Data -cacheFileName "installed_php_versions" -data $installedVersions -depth 1 + + return 0 + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to refresh installed PHP versions cache" + exception = $_ + } + + return -1 + } +} + function Get-Installed-PHP-Versions-From-Directory { $directories = Get-All-Subdirectories -path "$STORAGE_PATH\php" $installedVersions = $directories | ForEach-Object { diff --git a/src/actions/install.ps1 b/src/actions/install.ps1 index 871f65c..c3ca319 100644 --- a/src/actions/install.ps1 +++ b/src/actions/install.ps1 @@ -370,6 +370,8 @@ function Install-PHP { $message = "`nPHP $($selectedVersionObject.version) installed successfully at: '$destination\$phpDirectoryName'" $message += "`nRun 'pvm use $($selectedVersionObject.version)' to use this version" + $cacheRefreshed = Refresh-Installed-PHP-Versions-Cache + return @{ code = 0; message = $message; color = "DarkGreen" } } catch { $logged = Log-Data -data @{ From 66832d30873c21b9e22f8a5cc5c65c87ff5149ec Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 20:19:13 +0100 Subject: [PATCH 15/69] Fix bug Get-Matching-PHP-Versions --- src/actions/common.ps1 | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/actions/common.ps1 b/src/actions/common.ps1 index f2d118b..3645c6d 100644 --- a/src/actions/common.ps1 +++ b/src/actions/common.ps1 @@ -138,15 +138,10 @@ function Get-Matching-PHP-Versions { param ($version) try { - $installedVersions = Get-Installed-PHP-Versions # You should have this function - - $matchingVersions = @() - foreach ($v in $installedVersions) { - if ($v.dirName -like "$version*") { - $matchingVersions += ($v.dirName -replace 'php', '') - } - } + $installedVersions = Get-Installed-PHP-Versions + $matchingVersions = $installedVersions | Where-Object { $_.Version -like "$version*" } + return $matchingVersions } catch { $logged = Log-Data -data @{ From 77fe7dd765bf56906652956d32cb698b20d97676 Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 20:43:28 +0100 Subject: [PATCH 16/69] Add helper to check equality of two php versions --- src/actions/common.ps1 | 2 +- src/actions/install.ps1 | 6 +-- src/actions/list.ps1 | 5 +- src/actions/uninstall.ps1 | 96 +++++++++++++++++++-------------------- src/functions/helpers.ps1 | 12 +++++ 5 files changed, 63 insertions(+), 58 deletions(-) diff --git a/src/actions/common.ps1 b/src/actions/common.ps1 index 3645c6d..2802b59 100644 --- a/src/actions/common.ps1 +++ b/src/actions/common.ps1 @@ -117,7 +117,7 @@ function Get-UserSelected-PHP-Version { $installedVersions | ForEach-Object { $versionNumber = $_ $isCurrent = "" - if ($currentVersion -eq $versionNumber) { + if (Is-Two-PHP-Versions-Equal -version1 $currentVersion -version2 $_) { $isCurrent = "(Current)" } Write-Host " - $versionNumber $isCurrent" diff --git a/src/actions/install.ps1 b/src/actions/install.ps1 index c3ca319..681b428 100644 --- a/src/actions/install.ps1 +++ b/src/actions/install.ps1 @@ -313,11 +313,7 @@ function Install-PHP { if ($_.BuildType) { $metaData += $_.BuildType } - if ($currentVersion -and - $currentVersion.version -eq $_.version -and - $currentVersion.arch -eq $_.arch -and - $currentVersion.buildType -eq $_.BuildType - ) { + if (Is-Two-PHP-Versions-Equal -version1 $currentVersion -version2 $_) { $isCurrent = "(Current)" } $versionNumber = "$versionNumber ".PadRight(15, '.') diff --git a/src/actions/list.ps1 b/src/actions/list.ps1 index 6a1dac2..23afa7d 100644 --- a/src/actions/list.ps1 +++ b/src/actions/list.ps1 @@ -184,10 +184,7 @@ function Display-Installed-PHP-Versions { if ($_.BuildType) { $metaData += $_.BuildType } - if ($currentVersion.version -eq $_.version -and - $currentVersion.arch -eq $_.arch -and - $currentVersion.buildType -eq $_.BuildType - ) { + if (Is-Two-PHP-Versions-Equal -version1 $currentVersion -version2 $_) { $isCurrent = "(Current)" } $versionNumber = "$versionNumber ".PadRight(15, '.') diff --git a/src/actions/uninstall.ps1 b/src/actions/uninstall.ps1 index 051ea8c..5ab7007 100644 --- a/src/actions/uninstall.ps1 +++ b/src/actions/uninstall.ps1 @@ -1,48 +1,48 @@ - - -function Uninstall-PHP { - param ($version) - - try { - - $phpPath = Get-PHP-Path-By-Version -version $version - - if (-not $phpPath) { - $installedVersions = Get-Matching-PHP-Versions -version $version - $pathVersionObject = Get-UserSelected-PHP-Version -installedVersions $installedVersions - } else { - $pathVersionObject = @{ code = 0; version = $version; path = $phpPath } - } - - if (-not $pathVersionObject) { - return @{ code = -1; message = "PHP version $version was not found!"; color = "DarkYellow"} - } - - if ($pathVersionObject.code -ne 0) { - return $pathVersionObject - } - - if (-not $pathVersionObject.path) { - return @{ code = -1; message = "PHP version $($pathVersionObject.version) was not found!"; color = "DarkYellow"} - } - - $currentVersion = Get-Current-PHP-Version - if ($currentVersion -and ($($pathVersionObject.version) -eq $currentVersion.version)) { - $response = Read-Host "`nYou are trying to uninstall the currently active PHP version ($($pathVersionObject.version)). Are you sure? (y/n)" - $response = $response.Trim() - if ($response -ne "y" -and $response -ne "Y") { - return @{ code = -1; message = "Uninstallation cancelled"} - } - } - - Remove-Item -Path ($pathVersionObject.path) -Recurse -Force - - return @{ code = 0; message = "PHP version $($pathVersionObject.version) has been uninstalled successfully"; color = "DarkGreen" } - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to uninstall PHP version '$version'" - exception = $_ - } - return @{ code = -1; message = "Failed to uninstall PHP version '$version'"; color = "DarkYellow" } - } -} + + +function Uninstall-PHP { + param ($version) + + try { + + $phpPath = Get-PHP-Path-By-Version -version $version + + if (-not $phpPath) { + $installedVersions = Get-Matching-PHP-Versions -version $version + $pathVersionObject = Get-UserSelected-PHP-Version -installedVersions $installedVersions + } else { + $pathVersionObject = @{ code = 0; version = $version; path = $phpPath } + } + + if (-not $pathVersionObject) { + return @{ code = -1; message = "PHP version $version was not found!"; color = "DarkYellow"} + } + + if ($pathVersionObject.code -ne 0) { + return $pathVersionObject + } + + if (-not $pathVersionObject.path) { + return @{ code = -1; message = "PHP version $($pathVersionObject.version) was not found!"; color = "DarkYellow"} + } + + $currentVersion = Get-Current-PHP-Version + if (Is-Two-PHP-Versions-Equal -version1 $currentVersion -version2 $pathVersionObject) { + $response = Read-Host "`nYou are trying to uninstall the currently active PHP version ($($pathVersionObject.version)). Are you sure? (y/n)" + $response = $response.Trim() + if ($response -ne "y" -and $response -ne "Y") { + return @{ code = -1; message = "Uninstallation cancelled"} + } + } + + Remove-Item -Path ($pathVersionObject.path) -Recurse -Force + + return @{ code = 0; message = "PHP version $($pathVersionObject.version) has been uninstalled successfully"; color = "DarkGreen" } + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to uninstall PHP version '$version'" + exception = $_ + } + return @{ code = -1; message = "Failed to uninstall PHP version '$version'"; color = "DarkYellow" } + } +} diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index a03fd1d..b65ca6f 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -440,4 +440,16 @@ function Get-BinaryArchitecture-From-DLL { 0x014c { "x86" } default { "Unknown" } } +} + +function Is-Two-PHP-Versions-Equal { + param ($version1, $version2) + + if ($null -eq $version1 -or $null -eq $version2) { + return $false + } + + return (($version1.version -eq $version2.version) -and + ($version1.arch -eq $version2.arch) -and + ($version1.buildType -eq $version2.buildType)) } \ No newline at end of file From 37cbdb56d1b15ca29a3218fcdb6798ddbb9a3cda Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 20:53:43 +0100 Subject: [PATCH 17/69] Refactor uninstall command --- src/actions/common.ps1 | 26 ++++++++++++++++---------- src/actions/uninstall.ps1 | 14 ++------------ 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/actions/common.ps1 b/src/actions/common.ps1 index 2802b59..0b7925a 100644 --- a/src/actions/common.ps1 +++ b/src/actions/common.ps1 @@ -107,31 +107,37 @@ function Get-UserSelected-PHP-Version { return $null } if ($installedVersions.Count -eq 1) { - $version = $($installedVersions) + $versionObj = $($installedVersions) } else { $currentVersion = Get-Current-PHP-Version - if ($currentVersion -and $currentVersion.version) { - $currentVersion = $currentVersion.version - } + $index = 0 Write-Host "`nInstalled versions :" $installedVersions | ForEach-Object { - $versionNumber = $_ + $_.index = $index $isCurrent = "" if (Is-Two-PHP-Versions-Equal -version1 $currentVersion -version2 $_) { $isCurrent = "(Current)" } - Write-Host " - $versionNumber $isCurrent" + $metaData = "" + if ($_.Arch) { + $metaData += $_.Arch + " " + } + if ($_.BuildType) { + $metaData += $_.BuildType + } + $versionNumber = "$($_.version) ".PadRight(15, '.') + Write-Host " [$index] $versionNumber $metaData $isCurrent" + $index++ } - $response = Read-Host "`nEnter the exact version to use. (or press Enter to cancel)" + $response = Read-Host "`nInsert the [number] matching the version to uninstall (or press Enter to cancel)" $response = $response.Trim() if (-not $response) { return @{ code = -1; message = "Operation cancelled."; color = "DarkYellow"} } - $version = $response + $versionObj = $installedVersions | Where-Object { $_.index -eq $response } } - $phpPath = Get-PHP-Path-By-Version -version $version - return @{ code = 0; version = $version; path = $phpPath } + return @{ code = 0; version = $versionObj.version; arch = $versionObj.arch; buildType = $versionObj.BuildType; path = $versionObj.InstallPath } } function Get-Matching-PHP-Versions { diff --git a/src/actions/uninstall.ps1 b/src/actions/uninstall.ps1 index 5ab7007..098e66d 100644 --- a/src/actions/uninstall.ps1 +++ b/src/actions/uninstall.ps1 @@ -5,14 +5,8 @@ function Uninstall-PHP { try { - $phpPath = Get-PHP-Path-By-Version -version $version - - if (-not $phpPath) { - $installedVersions = Get-Matching-PHP-Versions -version $version - $pathVersionObject = Get-UserSelected-PHP-Version -installedVersions $installedVersions - } else { - $pathVersionObject = @{ code = 0; version = $version; path = $phpPath } - } + $installedVersions = Get-Matching-PHP-Versions -version $version + $pathVersionObject = Get-UserSelected-PHP-Version -installedVersions $installedVersions if (-not $pathVersionObject) { return @{ code = -1; message = "PHP version $version was not found!"; color = "DarkYellow"} @@ -22,10 +16,6 @@ function Uninstall-PHP { return $pathVersionObject } - if (-not $pathVersionObject.path) { - return @{ code = -1; message = "PHP version $($pathVersionObject.version) was not found!"; color = "DarkYellow"} - } - $currentVersion = Get-Current-PHP-Version if (Is-Two-PHP-Versions-Equal -version1 $currentVersion -version2 $pathVersionObject) { $response = Read-Host "`nYou are trying to uninstall the currently active PHP version ($($pathVersionObject.version)). Are you sure? (y/n)" From 1cdda4a64241c270c7e49cd933756013d9dadcf4 Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 21:16:07 +0100 Subject: [PATCH 18/69] Refresh cached list after removing php version --- src/actions/uninstall.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/actions/uninstall.ps1 b/src/actions/uninstall.ps1 index 098e66d..128adfe 100644 --- a/src/actions/uninstall.ps1 +++ b/src/actions/uninstall.ps1 @@ -27,6 +27,8 @@ function Uninstall-PHP { Remove-Item -Path ($pathVersionObject.path) -Recurse -Force + $cacheRefreshed = Refresh-Installed-PHP-Versions-Cache + return @{ code = 0; message = "PHP version $($pathVersionObject.version) has been uninstalled successfully"; color = "DarkGreen" } } catch { $logged = Log-Data -data @{ From 418f2c477ae97fe29086fa3464135fb620a690f0 Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 21:29:36 +0100 Subject: [PATCH 19/69] Replace Count with Length --- src/actions/common.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/common.ps1 b/src/actions/common.ps1 index 0b7925a..ca1fac1 100644 --- a/src/actions/common.ps1 +++ b/src/actions/common.ps1 @@ -106,7 +106,7 @@ function Get-UserSelected-PHP-Version { if (-not $installedVersions -or $installedVersions.Count -eq 0) { return $null } - if ($installedVersions.Count -eq 1) { + if ($installedVersions.Length -eq 1) { $versionObj = $($installedVersions) } else { $currentVersion = Get-Current-PHP-Version From bb919cfeacdeef15d4b201476a2d6f7b9ed789d2 Mon Sep 17 00:00:00 2001 From: Driss B Date: Thu, 5 Feb 2026 21:30:29 +0100 Subject: [PATCH 20/69] Refactor use command --- src/actions/use.ps1 | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/actions/use.ps1 b/src/actions/use.ps1 index eb431c3..9b905f3 100644 --- a/src/actions/use.ps1 +++ b/src/actions/use.ps1 @@ -37,13 +37,8 @@ function Update-PHP-Version { param ($version) try { - $phpPath = Get-PHP-Path-By-Version -version $version - if (-not $phpPath) { - $installedVersions = Get-Matching-PHP-Versions -version $version - $pathVersionObject = Get-UserSelected-PHP-Version -installedVersions $installedVersions - } else { - $pathVersionObject = @{ code = 0; version = $version; path = $phpPath } - } + $installedVersions = Get-Matching-PHP-Versions -version $version + $pathVersionObject = Get-UserSelected-PHP-Version -installedVersions $installedVersions if (-not $pathVersionObject) { return @{ code = -1; message = "PHP version $version was not found!"; color = "DarkYellow"} @@ -55,26 +50,17 @@ function Update-PHP-Version { $currentVersion = Get-Current-PHP-Version if ($currentVersion -and $currentVersion.version) { - if ($pathVersionObject.version -eq $currentVersion.version -and - $pathVersionObject.arch -eq $currentVersion.arch) { + if (Is-Two-PHP-Versions-Equal -version1 $currentVersion -version2 $pathVersionObject) { return @{ code = 0; message = "Already using PHP $($pathVersionObject.version)"; color = "DarkCyan"} } } - if (-not $pathVersionObject.path) { - return @{ code = -1; message = "PHP version $($pathVersionObject.version) was not found!"; color = "DarkYellow"} - } $linkCreated = Make-Symbolic-Link -link $PHP_CURRENT_VERSION_PATH -target $pathVersionObject.path if ($linkCreated.code -ne 0) { return $linkCreated } - $text = "Now using PHP $($pathVersionObject.version)" - if ($pathVersionObject.arch) { - $text += " ($($pathVersionObject.arch))" - } else { - $dll = Get-ChildItem "$currentPhpVersionPath\php*nts.dll","$currentPhpVersionPath\php*ts.dll" | Select-Object -First 1 - $arch = Get-BinaryArchitecture $dll.FullName - } + $text = "Now using PHP $($pathVersionObject.version) $($pathVersionObject.buildType) $($pathVersionObject.arch)" + return @{ code = 0; message = $text; color = "DarkGreen"} } catch { $logged = Log-Data -data @{ From b4792574ad97df9424c5c2317c636a8d027ad131 Mon Sep 17 00:00:00 2001 From: Driss B Date: Fri, 6 Feb 2026 16:18:54 +0100 Subject: [PATCH 21/69] Convert hashtable to custom object --- src/actions/ini.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/actions/ini.ps1 b/src/actions/ini.ps1 index c44244e..f108435 100644 --- a/src/actions/ini.ps1 +++ b/src/actions/ini.ps1 @@ -1153,6 +1153,7 @@ function List-PHP-Extensions { } } else { $availableExtensions = Get-PHPExtensions-From-Source + $availableExtensions = [pscustomobject] $availableExtensions } if ($availableExtensions.Count -eq 0) { From c1374bc1f875fd0f363e136b5726d8a4ffd907d3 Mon Sep 17 00:00:00 2001 From: Driss B Date: Fri, 6 Feb 2026 17:31:45 +0100 Subject: [PATCH 22/69] Trim text --- src/actions/use.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/use.ps1 b/src/actions/use.ps1 index 9b905f3..8a1d995 100644 --- a/src/actions/use.ps1 +++ b/src/actions/use.ps1 @@ -59,7 +59,7 @@ function Update-PHP-Version { if ($linkCreated.code -ne 0) { return $linkCreated } - $text = "Now using PHP $($pathVersionObject.version) $($pathVersionObject.buildType) $($pathVersionObject.arch)" + $text = ("Now using PHP $($pathVersionObject.version) $($pathVersionObject.buildType) $($pathVersionObject.arch)").Trim() return @{ code = 0; message = $text; color = "DarkGreen"} } catch { From 9b94b109234392f5b0366cd8824e8ab73823d9fb Mon Sep 17 00:00:00 2001 From: Driss B Date: Fri, 6 Feb 2026 17:32:05 +0100 Subject: [PATCH 23/69] Remove unused variable --- src/functions/helpers.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index b65ca6f..6c84eb7 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -20,10 +20,8 @@ function Can-Use-Cache { function Get-Data-From-Cache { param ($cacheFileName) - $path = "$CACHE_PATH\$cacheFileName.json" - $list = @{} try { - $jsonData = Get-Content $path | ConvertFrom-Json + $jsonData = Get-Content "$CACHE_PATH\$cacheFileName.json" | ConvertFrom-Json return $jsonData } catch { $logged = Log-Data -data @{ From 8e79cfe4884dcde3462b3c5fe1409908b4e1f4b4 Mon Sep 17 00:00:00 2001 From: Driss B Date: Fri, 6 Feb 2026 17:36:13 +0100 Subject: [PATCH 24/69] Remove Get-PHP-Path-By-Version and related tests --- src/functions/helpers.ps1 | 16 -- tests/common.tests.ps1 | 1 - tests/helpers.tests.ps1 | 31 --- tests/uninstall.tests.ps1 | 397 ++++++++++++++++++------------------- tests/use.tests.ps1 | 398 ++++++++++++++++++-------------------- 5 files changed, 382 insertions(+), 461 deletions(-) diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index 6c84eb7..72f9d39 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -128,22 +128,6 @@ function Set-EnvVar { } } -function Get-PHP-Path-By-Version { - param ($version) - - if ([string]::IsNullOrWhiteSpace($version)) { - return $null - } - - $phpContainerPath = "$STORAGE_PATH\php" - $version = $version.Trim() - - if (-not(Is-Directory-Exists -path $phpContainerPath) -or -not(Is-Directory-Exists -path "$phpContainerPath\$version")) { - return $null - } - - return "$phpContainerPath\$version" -} function Make-Symbolic-Link { param($link, $target) diff --git a/tests/common.tests.ps1 b/tests/common.tests.ps1 index 31554ae..08521cd 100644 --- a/tests/common.tests.ps1 +++ b/tests/common.tests.ps1 @@ -181,7 +181,6 @@ Describe "Get-UserSelected-PHP-Version" { It "Should prompt user and return selected version when multiple are provided" { Mock Read-Host { return "8.1" } Mock Write-Host { } - Mock Get-PHP-Path-By-Version { return "C:\php\8.1" } $result = Get-UserSelected-PHP-Version -installedVersions @("7.4", "8.0", "8.1") $result.version | Should -Be "8.1" diff --git a/tests/helpers.tests.ps1 b/tests/helpers.tests.ps1 index 98df322..dc642ce 100644 --- a/tests/helpers.tests.ps1 +++ b/tests/helpers.tests.ps1 @@ -310,37 +310,6 @@ Describe "Set-EnvVar" { } } -Describe "Get-PHP-Path-By-Version" { - BeforeEach { - Mock Is-Directory-Exists { - param ($path) - return (Test-Path $path) - } - } - Context "When version exists" { - It "Returns correct path for existing version" { - $result = Get-PHP-Path-By-Version -version "8.1" - $result | Should -Be "$STORAGE_PATH\php\8.1" - } - } - - Context "When version doesn't exist" { - It "Returns null for non-existent version" { - $result = Get-PHP-Path-By-Version -version "5.6" - $result | Should -Be $null - } - - It "Returns null for empty version" { - $result = Get-PHP-Path-By-Version -version "" - $result | Should -Be $null - } - - It "Returns null for whitespace version" { - $result = Get-PHP-Path-By-Version -version " " - $result | Should -Be $null - } - } -} Describe "Make-Symbolic-Link" { Context "When creating symbolic links" { diff --git a/tests/uninstall.tests.ps1 b/tests/uninstall.tests.ps1 index 4b1c572..d29e47e 100644 --- a/tests/uninstall.tests.ps1 +++ b/tests/uninstall.tests.ps1 @@ -1,207 +1,192 @@ -# Load required modules and functions -. "$PSScriptRoot\..\src\actions\uninstall.ps1" - -BeforeAll { - # Create a test directory for PHP installations - $script:PHP_CURRENT_VERSION_PATH = "TestDrive:\php\current" - $script:LOG_ERROR_PATH = "TestDrive:\Logs\error.log" - $testPhpPath = "TestDrive:\PHP" - New-Item -Path "$testPhpPath\7.4" -ItemType Directory -Force - New-Item -Path "$testPhpPath\8.0" -ItemType Directory -Force - - function Log-Data { param($logPath, $message, $data) } - # Mock Log-Data globally - this will be available for all tests - Mock Log-Data -MockWith { - param($logPath, $message, $data) - return $true - } -} - -Describe "Uninstall-PHP" { - Context "When PHP version is found directly" { - BeforeEach { - Mock Get-PHP-Path-By-Version -ParameterFilter { $version -eq "7.4" } -MockWith { - "$testPhpPath\7.4" - } - - Mock Get-Matching-PHP-Versions -MockWith { } - Mock Get-UserSelected-PHP-Version -MockWith { } - Mock Remove-Item -MockWith { } - Mock Log-Data -MockWith { $true } - } - - It "Should successfully uninstall when version is found directly" { - $result = Uninstall-PHP -version "7.4" - - $result.code | Should -Be 0 - $result.message | Should -BeLike "*PHP version 7.4 has been uninstalled successfully*" - $result.color | Should -Be "DarkGreen" - - Should -Invoke Get-PHP-Path-By-Version -Exactly 1 - Should -Invoke Remove-Item -Exactly 1 -ParameterFilter { - $Path -eq "$testPhpPath\7.4" -and $Recurse -eq $true -and $Force -eq $true - } - } - - It "Should prompt user when trying to uninstall current version" { - Mock Get-Current-PHP-Version { @{ version = "7.4" } } - Mock Read-Host { } - $result = Uninstall-PHP -version "7.4" - $result.code | Should -Be -1 - - Assert-MockCalled Read-Host -ParameterFilter { $Prompt -like "*You are trying to uninstall the currently active PHP version*" } - } - - It "Should prompt user when trying to uninstall current version and handle 'n' response" { - Mock Get-PHP-Path-By-Version { "$testPhpPath\8.0" } - Mock Get-Current-PHP-Version { @{ version = "8.0" } } - Mock Read-Host { "n" } - $result = Uninstall-PHP -version "8.0" - $result.code | Should -Be -1 - $result.message | Should -Be "Uninstallation cancelled" - - Assert-MockCalled Read-Host -Times 1 - } - - It "Should prompt user when trying to uninstall current version and handle 'y' response" { - Mock Get-PHP-Path-By-Version { "$testPhpPath\8.0" } - Mock Get-Current-PHP-Version { @{ version = "8.0" } } - Mock Read-Host { "y" } - $result = Uninstall-PHP -version "8.0" - $result.code | Should -Be 0 - - Assert-MockCalled Read-Host -Times 1 - } - } - - Context "When PHP version is not found directly but matches exist" { - BeforeEach { - Mock Get-PHP-Path-By-Version -MockWith { $null } - Mock Get-Matching-PHP-Versions -ParameterFilter { $version -eq "8.*" } -MockWith { - @("8.0", "8.1") - } - Mock Get-UserSelected-PHP-Version -MockWith { - @{ code = 0; version = "8.0"; path = "$testPhpPath\8.0" } - } - Mock Remove-Item -MockWith { } - Mock Log-Data -MockWith { $true } - } - - It "Should successfully uninstall after user selection" { - $result = Uninstall-PHP -version "8.*" - - $result.code | Should -Be 0 - $result.message | Should -BeLike "*PHP version 8.* has been uninstalled successfully*" - $result.color | Should -Be "DarkGreen" - - Should -Invoke Get-Matching-PHP-Versions -Exactly 1 - Should -Invoke Get-UserSelected-PHP-Version -Exactly 1 - Should -Invoke Remove-Item -Exactly 1 -ParameterFilter { - $Path -eq "$testPhpPath\8.0" - } - } - } - - Context "When PHP version is not found at all" { - BeforeEach { - Mock Get-PHP-Path-By-Version -MockWith { $null } - Mock Get-Matching-PHP-Versions -ParameterFilter { $version -eq "5.6" } -MockWith { - @() - } - Mock Get-UserSelected-PHP-Version -MockWith { } - Mock Remove-Item -MockWith { } - Mock Log-Data -MockWith { $true } - } - - It "Should return version not found message" { - $result = Uninstall-PHP -version "5.6" - - $result.code | Should -Be -1 - $result.message | Should -BeExactly "PHP version 5.6 was not found!" - $result.color | Should -Be "DarkYellow" - - Should -Invoke Get-PHP-Path-By-Version -Exactly 1 - Should -Invoke Get-Matching-PHP-Versions -Exactly 1 - Should -Invoke Remove-Item -Exactly 0 - } - } - - Context "When user selection returns an error" { - BeforeEach { - Mock Get-PHP-Path-By-Version -MockWith { $null } - Mock Get-Matching-PHP-Versions -ParameterFilter { $version -eq "8.*" } -MockWith { - @("8.0", "8.1") - } - Mock Get-UserSelected-PHP-Version -MockWith { - @{ code = -1; message = "User cancelled the selection"; color = "DarkYellow" } - } - Mock Remove-Item -MockWith { } - Mock Log-Data -MockWith { $true } - } - - It "Should return the user selection error" { - $result = Uninstall-PHP -version "8.*" - - $result.code | Should -Be -1 - $result.message | Should -Be "User cancelled the selection" - $result.color | Should -Be "DarkYellow" - - Should -Invoke Get-Matching-PHP-Versions -Exactly 1 - Should -Invoke Get-UserSelected-PHP-Version -Exactly 1 - Should -Invoke Remove-Item -Exactly 0 - } - } - - Context "When user selection returns a version but no path" { - BeforeEach { - Mock Get-PHP-Path-By-Version -MockWith { $null } - Mock Get-Matching-PHP-Versions -ParameterFilter { $version -eq "8.*" } -MockWith { - @("8.0", "8.1") - } - Mock Get-UserSelected-PHP-Version -MockWith { - @{ code = 0; version = "8.2"; path = $null } - } - Mock Remove-Item -MockWith { } - Mock Log-Data -MockWith { $true } - } - - It "Should return version not found message" { - $result = Uninstall-PHP -version "8.*" - - $result.code | Should -Be -1 - $result.message | Should -BeExactly "PHP version 8.2 was not found!" - $result.color | Should -Be "DarkYellow" - - Should -Invoke Get-Matching-PHP-Versions -Exactly 1 - Should -Invoke Get-UserSelected-PHP-Version -Exactly 1 - Should -Invoke Remove-Item -Exactly 0 - } - } - - Context "When uninstallation fails with an exception" { - BeforeEach { - Mock Get-PHP-Path-By-Version -ParameterFilter { $version -eq "7.4" } -MockWith { - "$testPhpPath\7.4" - } - Mock Get-Current-PHP-Version { @{ version = $null } } - Mock Get-Matching-PHP-Versions -MockWith { } - Mock Get-UserSelected-PHP-Version -MockWith { } - Mock Remove-Item -MockWith { throw "Access denied" } - } - - It "Should catch the exception and return error message" { - $result = Uninstall-PHP -version "7.4" - - $result.code | Should -Be -1 - $result.message | Should -Be "Failed to uninstall PHP version '7.4'" - $result.color | Should -Be "DarkYellow" - - Should -Invoke Remove-Item -Exactly 1 - Should -Invoke Log-Data -Exactly 1 - } - } - - AfterAll { - Remove-Item -Path $testPhpPath -Recurse -Force -ErrorAction SilentlyContinue - } +# Load required modules and functions +. "$PSScriptRoot\..\src\actions\uninstall.ps1" + +BeforeAll { + # Create a test directory for PHP installations + $script:PHP_CURRENT_VERSION_PATH = "TestDrive:\php\current" + $script:LOG_ERROR_PATH = "TestDrive:\Logs\error.log" + $testPhpPath = "TestDrive:\PHP" + New-Item -Path "$testPhpPath\7.4" -ItemType Directory -Force + New-Item -Path "$testPhpPath\8.0" -ItemType Directory -Force + + function Log-Data { param($logPath, $message, $data) } + # Mock Log-Data globally - this will be available for all tests + Mock Log-Data -MockWith { + param($logPath, $message, $data) + return $true + } +} + +Describe "Uninstall-PHP" { + Context "When PHP version is found directly" { + BeforeEach { + Mock Get-Matching-PHP-Versions -MockWith { } + Mock Get-UserSelected-PHP-Version -MockWith { } + Mock Remove-Item -MockWith { } + Mock Log-Data -MockWith { $true } + } + + It "Should successfully uninstall when version is found directly" { + $result = Uninstall-PHP -version "7.4" + + $result.code | Should -Be 0 + $result.message | Should -BeLike "*PHP version 7.4 has been uninstalled successfully*" + $result.color | Should -Be "DarkGreen" + + Should -Invoke Remove-Item -Exactly 1 -ParameterFilter { + $Path -eq "$testPhpPath\7.4" -and $Recurse -eq $true -and $Force -eq $true + } + } + + It "Should prompt user when trying to uninstall current version" { + Mock Get-Current-PHP-Version { @{ version = "7.4" } } + Mock Read-Host { } + $result = Uninstall-PHP -version "7.4" + $result.code | Should -Be -1 + + Assert-MockCalled Read-Host -ParameterFilter { $Prompt -like "*You are trying to uninstall the currently active PHP version*" } + } + + It "Should prompt user when trying to uninstall current version and handle 'n' response" { + Mock Get-Current-PHP-Version { @{ version = "8.0" } } + Mock Read-Host { "n" } + $result = Uninstall-PHP -version "8.0" + $result.code | Should -Be -1 + $result.message | Should -Be "Uninstallation cancelled" + + Assert-MockCalled Read-Host -Times 1 + } + + It "Should prompt user when trying to uninstall current version and handle 'y' response" { + Mock Get-Current-PHP-Version { @{ version = "8.0" } } + Mock Read-Host { "y" } + $result = Uninstall-PHP -version "8.0" + $result.code | Should -Be 0 + + Assert-MockCalled Read-Host -Times 1 + } + } + + Context "When PHP version is not found directly but matches exist" { + BeforeEach { + Mock Get-Matching-PHP-Versions -ParameterFilter { $version -eq "8.*" } -MockWith { + @("8.0", "8.1") + } + Mock Get-UserSelected-PHP-Version -MockWith { + @{ code = 0; version = "8.0"; path = "$testPhpPath\8.0" } + } + Mock Remove-Item -MockWith { } + Mock Log-Data -MockWith { $true } + } + + It "Should successfully uninstall after user selection" { + $result = Uninstall-PHP -version "8.*" + + $result.code | Should -Be 0 + $result.message | Should -BeLike "*PHP version 8.* has been uninstalled successfully*" + $result.color | Should -Be "DarkGreen" + + Should -Invoke Get-Matching-PHP-Versions -Exactly 1 + Should -Invoke Get-UserSelected-PHP-Version -Exactly 1 + Should -Invoke Remove-Item -Exactly 1 -ParameterFilter { + $Path -eq "$testPhpPath\8.0" + } + } + } + + Context "When PHP version is not found at all" { + BeforeEach { + Mock Get-Matching-PHP-Versions -ParameterFilter { $version -eq "5.6" } -MockWith { + @() + } + Mock Get-UserSelected-PHP-Version -MockWith { } + Mock Remove-Item -MockWith { } + Mock Log-Data -MockWith { $true } + } + + It "Should return version not found message" { + $result = Uninstall-PHP -version "5.6" + + $result.code | Should -Be -1 + $result.message | Should -BeExactly "PHP version 5.6 was not found!" + $result.color | Should -Be "DarkYellow" + + Should -Invoke Get-Matching-PHP-Versions -Exactly 1 + Should -Invoke Remove-Item -Exactly 0 + } + } + + Context "When user selection returns an error" { + BeforeEach { + Mock Get-Matching-PHP-Versions -ParameterFilter { $version -eq "8.*" } -MockWith { + @("8.0", "8.1") + } + Mock Get-UserSelected-PHP-Version -MockWith { + @{ code = -1; message = "User cancelled the selection"; color = "DarkYellow" } + } + Mock Remove-Item -MockWith { } + Mock Log-Data -MockWith { $true } + } + + It "Should return the user selection error" { + $result = Uninstall-PHP -version "8.*" + + $result.code | Should -Be -1 + $result.message | Should -Be "User cancelled the selection" + $result.color | Should -Be "DarkYellow" + + Should -Invoke Get-Matching-PHP-Versions -Exactly 1 + Should -Invoke Get-UserSelected-PHP-Version -Exactly 1 + Should -Invoke Remove-Item -Exactly 0 + } + } + + Context "When user selection returns a version but no path" { + BeforeEach { + Mock Get-Matching-PHP-Versions -ParameterFilter { $version -eq "8.*" } -MockWith { + @("8.0", "8.1") + } + Mock Get-UserSelected-PHP-Version -MockWith { + @{ code = 0; version = "8.2"; path = $null } + } + Mock Remove-Item -MockWith { } + Mock Log-Data -MockWith { $true } + } + + It "Should return version not found message" { + $result = Uninstall-PHP -version "8.*" + + $result.code | Should -Be -1 + $result.message | Should -BeExactly "PHP version 8.2 was not found!" + $result.color | Should -Be "DarkYellow" + + Should -Invoke Get-Matching-PHP-Versions -Exactly 1 + Should -Invoke Get-UserSelected-PHP-Version -Exactly 1 + Should -Invoke Remove-Item -Exactly 0 + } + } + + Context "When uninstallation fails with an exception" { + BeforeEach { + Mock Get-Current-PHP-Version { @{ version = $null } } + Mock Get-Matching-PHP-Versions -MockWith { } + Mock Get-UserSelected-PHP-Version -MockWith { } + Mock Remove-Item -MockWith { throw "Access denied" } + } + + It "Should catch the exception and return error message" { + $result = Uninstall-PHP -version "7.4" + + $result.code | Should -Be -1 + $result.message | Should -Be "Failed to uninstall PHP version '7.4'" + $result.color | Should -Be "DarkYellow" + + Should -Invoke Remove-Item -Exactly 1 + Should -Invoke Log-Data -Exactly 1 + } + } + + AfterAll { + Remove-Item -Path $testPhpPath -Recurse -Force -ErrorAction SilentlyContinue + } } \ No newline at end of file diff --git a/tests/use.tests.ps1 b/tests/use.tests.ps1 index 607ad95..0f97bc7 100644 --- a/tests/use.tests.ps1 +++ b/tests/use.tests.ps1 @@ -1,207 +1,191 @@ -# Load required modules and functions -. "$PSScriptRoot\..\src\actions\use.ps1" - -BeforeAll { - - # Mock data and helper functions for testing - $PHP_CURRENT_VERSION_PATH = "C:\pvm\php" - $LOG_ERROR_PATH = "C:\logs\error.log" - - Mock Write-Host {} - function Get-PHP-Path-By-Version { - param($version) - # Mock implementation - if ($version -eq "8.1") { return "C:\php\8.1" } - if ($version -eq "8.2") { return "C:\php\8.2" } - return $null - } - - function Get-Matching-PHP-Versions { - param($version) - # Mock implementation - if ($version -like "8.*") { - return @( - @{version="8.1"; path="C:\php\8.1"}, - @{version="8.2"; path="C:\php\8.2"} - ) - } - return @() - } - - Mock Get-UserSelected-PHP-Version { - param($installedVersions) - # If we're in the Auto-Select test and a specific version was detected - if ($global:TestScenario -eq "composer" -or $global:TestScenario -eq ".php-version" -and $installedVersions) { - # Find the version that matches what we detected (8.2) - $selected = $installedVersions | Where-Object { $_.version -eq "8.2" } - if ($selected) { - return @{code=0; version=$selected.version; path=$selected.path} - } - } - - # Default behavior - select first version - if ($installedVersions -and $installedVersions.Count -gt 0) { - return @{code=0; version=$installedVersions[0].version; path=$installedVersions[0].path} - } - return $null - } - - function Make-Symbolic-Link { - param($link, $target) - # Mock implementation - return @{ code = 0 } - } - - function Log-Data { - param($logPath, $message, $data) - # Mock implementation - return $true - } - -} - -Describe "Detect-PHP-VersionFromProject" { - It "Should detect PHP version from .php-version" { - Mock Test-Path { return $true } - Mock Get-Content { return "7.4" } - $result = Detect-PHP-VersionFromProject - $result | Should -Be "7.4" - } - - It "Should detect PHP version from composer.json" { - Mock Test-Path { - param($path) - if ($path -eq "composer.json") { return $true } - return $false - } - Mock Get-Content { return '{"require": {"php": "^8.4"}}' } - $result = Detect-PHP-VersionFromProject - $result | Should -Be "8.4" - } - - It "Handles parser exceptions gracefully" { - Mock Test-Path { - param($path) - if ($path -eq "composer.json") { return $true } - return $false - } - Mock Get-Content { throw "Simulated parse error" } - { Detect-PHP-VersionFromProject } | Should -Not -Throw - } - -} - - -# Test Cases for Update-PHP-Version -Describe "Update-PHP-Version" { - BeforeEach { - $global:TestScenario = $null - } - - It "Should successfully update to an exact version match" { - $result = Update-PHP-Version -version "8.1" - $result.code | Should -Be 0 - $result.message | Should -BeExactly "Now using PHP 8.1" - } - - It "Should handle version not found when exact path doesn't exist" { - $result = Update-PHP-Version -version "7.4" - $result.code | Should -Be -1 - $result.message | Should -BeExactly "PHP version 7.4 was not found!" - } - - It "Should handle when Get-PHP-Path-By-Version returns null but matching versions exist" { - $result = Update-PHP-Version -version "8.x" - $result.code | Should -Be 0 - $result.message | Should -BeExactly "Now using PHP 8.1" # Assuming it selects the first match - } - - It "Should handle when no matching versions are found" { - $result = Update-PHP-Version -version "5.6" - $result.code | Should -Be -1 - $result.message | Should -BeExactly "PHP version 5.6 was not found!" - } - - It "Should return when switching to same current version" { - Mock Get-PHP-Path-By-Version { return "TestDrive:\php\8.2.0" } - Mock Get-Current-PHP-Version { return @{ version = "8.2.0"; path = "TestDrive:\php\8.2.0" } } - $result = Update-PHP-Version -version "8.2.0" - $result.code | Should -Be 0 - $result.message | Should -BeExactly "Already using PHP 8.2.0" - } - - It "Should handle when Make-Symbolic-Link fails" { - Mock Make-Symbolic-Link { return @{ code = -1; message = "Failed to create link"; color = "DarkYellow" } } - $result = Update-PHP-Version -version "8.1" - $result.code | Should -Be -1 - $result.message | Should -BeExactly "Failed to create link" - $result.color | Should -Be "DarkYellow" - } - - It "Should handle exceptions gracefully" { - # Force an exception by mocking Get-PHP-Path-By-Version to throw - Mock Get-PHP-Path-By-Version { throw "Test exception" } - $result = Update-PHP-Version -version "8.1" - $result.code | Should -Be -1 - $result.message | Should -Match "No matching PHP versions found" - } - - It "Should return error when pathVersionObject is null" { - Mock Get-UserSelected-PHP-Version { return $null } - $result = Update-PHP-Version -version "8.x" - $result.code | Should -Be -1 - $result.message | Should -Match "was not found" - } - - It "Should return error when pathVersionObject has non-zero code" { - Mock Get-UserSelected-PHP-Version { return @{code=-1; message="Test error"} } - $result = Update-PHP-Version -version "8.x" - $result.code | Should -Be -1 - } - - It "Should return error when path is missing in pathVersionObject" { - Mock Get-UserSelected-PHP-Version { return @{code=0; version="8.1"; path=$null} } - $result = Update-PHP-Version -version "8.x" - $result.code | Should -Be -1 - $result.message | Should -Match "was not found" - } -} - -# Test Cases for Auto-Select-PHP-Version -Describe "Auto-Select-PHP-Version" { - BeforeEach { - $global:TestScenario = $null - Mock Detect-PHP-VersionFromProject { - return "8.1" - } - } - - It "Should detect version from .php-version file" { - $global:TestScenario = ".php-version" - $result = Auto-Select-PHP-Version - $result.code | Should -Be 0 - $result.version | Should -Be "8.1" - } - - It "Should detect version from composer.json" { - $global:TestScenario = "composer" - $result = Auto-Select-PHP-Version - $result.code | Should -Be 0 - $result.version | Should -Be "8.1" - } - - It "Should return error when no version can be detected" { - Mock Detect-PHP-VersionFromProject { return $null } - $result = Auto-Select-PHP-Version - $result.code | Should -Be -1 - $result.message | Should -Match "Could not detect PHP version" - } - - It "Should return error when detected version is not installed" { - $global:TestScenario = ".php-version" - Mock Get-Matching-PHP-Versions { return @() } - $result = Auto-Select-PHP-Version - $result.code | Should -Be -1 - $result.message | Should -Match "PHP '8.1' is not installed" - } -} +# Load required modules and functions +. "$PSScriptRoot\..\src\actions\use.ps1" + +BeforeAll { + + # Mock data and helper functions for testing + $PHP_CURRENT_VERSION_PATH = "C:\pvm\php" + $LOG_ERROR_PATH = "C:\logs\error.log" + + Mock Write-Host {} + + function Get-Matching-PHP-Versions { + param($version) + # Mock implementation + if ($version -like "8.*") { + return @( + @{version="8.1"; path="C:\php\8.1"}, + @{version="8.2"; path="C:\php\8.2"} + ) + } + return @() + } + + Mock Get-UserSelected-PHP-Version { + param($installedVersions) + # If we're in the Auto-Select test and a specific version was detected + if ($global:TestScenario -eq "composer" -or $global:TestScenario -eq ".php-version" -and $installedVersions) { + # Find the version that matches what we detected (8.2) + $selected = $installedVersions | Where-Object { $_.version -eq "8.2" } + if ($selected) { + return @{code=0; version=$selected.version; path=$selected.path} + } + } + + # Default behavior - select first version + if ($installedVersions -and $installedVersions.Count -gt 0) { + return @{code=0; version=$installedVersions[0].version; path=$installedVersions[0].path} + } + return $null + } + + function Make-Symbolic-Link { + param($link, $target) + # Mock implementation + return @{ code = 0 } + } + + function Log-Data { + param($logPath, $message, $data) + # Mock implementation + return $true + } + +} + +Describe "Detect-PHP-VersionFromProject" { + It "Should detect PHP version from .php-version" { + Mock Test-Path { return $true } + Mock Get-Content { return "7.4" } + $result = Detect-PHP-VersionFromProject + $result | Should -Be "7.4" + } + + It "Should detect PHP version from composer.json" { + Mock Test-Path { + param($path) + if ($path -eq "composer.json") { return $true } + return $false + } + Mock Get-Content { return '{"require": {"php": "^8.4"}}' } + $result = Detect-PHP-VersionFromProject + $result | Should -Be "8.4" + } + + It "Handles parser exceptions gracefully" { + Mock Test-Path { + param($path) + if ($path -eq "composer.json") { return $true } + return $false + } + Mock Get-Content { throw "Simulated parse error" } + { Detect-PHP-VersionFromProject } | Should -Not -Throw + } + +} + + +# Test Cases for Update-PHP-Version +Describe "Update-PHP-Version" { + BeforeEach { + $global:TestScenario = $null + } + + It "Should successfully update to an exact version match" { + $result = Update-PHP-Version -version "8.1" + $result.code | Should -Be 0 + $result.message | Should -BeExactly "Now using PHP 8.1" + } + + It "Should handle version not found when exact path doesn't exist" { + $result = Update-PHP-Version -version "7.4" + $result.code | Should -Be -1 + $result.message | Should -BeExactly "PHP version 7.4 was not found!" + } + + It "Should handle when no matching versions are found" { + $result = Update-PHP-Version -version "5.6" + $result.code | Should -Be -1 + $result.message | Should -BeExactly "PHP version 5.6 was not found!" + } + + It "Should return when switching to same current version" { + Mock Get-Current-PHP-Version { return @{ version = "8.2.0"; path = "TestDrive:\php\8.2.0" } } + $result = Update-PHP-Version -version "8.2.0" + $result.code | Should -Be 0 + $result.message | Should -BeExactly "Already using PHP 8.2.0" + } + + It "Should handle when Make-Symbolic-Link fails" { + Mock Make-Symbolic-Link { return @{ code = -1; message = "Failed to create link"; color = "DarkYellow" } } + $result = Update-PHP-Version -version "8.1" + $result.code | Should -Be -1 + $result.message | Should -BeExactly "Failed to create link" + $result.color | Should -Be "DarkYellow" + } + + It "Should handle exceptions gracefully" { + $result = Update-PHP-Version -version "8.1" + $result.code | Should -Be -1 + $result.message | Should -Match "No matching PHP versions found" + } + + It "Should return error when pathVersionObject is null" { + Mock Get-UserSelected-PHP-Version { return $null } + $result = Update-PHP-Version -version "8.x" + $result.code | Should -Be -1 + $result.message | Should -Match "was not found" + } + + It "Should return error when pathVersionObject has non-zero code" { + Mock Get-UserSelected-PHP-Version { return @{code=-1; message="Test error"} } + $result = Update-PHP-Version -version "8.x" + $result.code | Should -Be -1 + } + + It "Should return error when path is missing in pathVersionObject" { + Mock Get-UserSelected-PHP-Version { return @{code=0; version="8.1"; path=$null} } + $result = Update-PHP-Version -version "8.x" + $result.code | Should -Be -1 + $result.message | Should -Match "was not found" + } +} + +# Test Cases for Auto-Select-PHP-Version +Describe "Auto-Select-PHP-Version" { + BeforeEach { + $global:TestScenario = $null + Mock Detect-PHP-VersionFromProject { + return "8.1" + } + } + + It "Should detect version from .php-version file" { + $global:TestScenario = ".php-version" + $result = Auto-Select-PHP-Version + $result.code | Should -Be 0 + $result.version | Should -Be "8.1" + } + + It "Should detect version from composer.json" { + $global:TestScenario = "composer" + $result = Auto-Select-PHP-Version + $result.code | Should -Be 0 + $result.version | Should -Be "8.1" + } + + It "Should return error when no version can be detected" { + Mock Detect-PHP-VersionFromProject { return $null } + $result = Auto-Select-PHP-Version + $result.code | Should -Be -1 + $result.message | Should -Match "Could not detect PHP version" + } + + It "Should return error when detected version is not installed" { + $global:TestScenario = ".php-version" + Mock Get-Matching-PHP-Versions { return @() } + $result = Auto-Select-PHP-Version + $result.code | Should -Be -1 + $result.message | Should -Match "PHP '8.1' is not installed" + } +} From 65c26839dad4a645e0f860ad555d6d8c986e7960 Mon Sep 17 00:00:00 2001 From: Driss B Date: Fri, 6 Feb 2026 17:37:04 +0100 Subject: [PATCH 25/69] Fix use.tests.ps1 --- tests/use.tests.ps1 | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/use.tests.ps1 b/tests/use.tests.ps1 index 0f97bc7..f0d2413 100644 --- a/tests/use.tests.ps1 +++ b/tests/use.tests.ps1 @@ -110,7 +110,16 @@ Describe "Update-PHP-Version" { } It "Should return when switching to same current version" { - Mock Get-Current-PHP-Version { return @{ version = "8.2.0"; path = "TestDrive:\php\8.2.0" } } + Mock Get-UserSelected-PHP-Version { return @{ + code=0; version="8.2.0"; arch = 'x64'; + buildType = 'TS'; path="TestDrive:\php\8.2.0" + }} + Mock Get-Current-PHP-Version { return @{ + version = "8.2.0"; + path = "TestDrive:\php\8.2.0" + arch = 'x64' + buildType = 'TS' + }} $result = Update-PHP-Version -version "8.2.0" $result.code | Should -Be 0 $result.message | Should -BeExactly "Already using PHP 8.2.0" @@ -125,6 +134,8 @@ Describe "Update-PHP-Version" { } It "Should handle exceptions gracefully" { + # Force an exception by mocking Get-Matching-PHP-Versions to throw + Mock Get-Matching-PHP-Versions { throw "Test exception" } $result = Update-PHP-Version -version "8.1" $result.code | Should -Be -1 $result.message | Should -Match "No matching PHP versions found" @@ -142,13 +153,6 @@ Describe "Update-PHP-Version" { $result = Update-PHP-Version -version "8.x" $result.code | Should -Be -1 } - - It "Should return error when path is missing in pathVersionObject" { - Mock Get-UserSelected-PHP-Version { return @{code=0; version="8.1"; path=$null} } - $result = Update-PHP-Version -version "8.x" - $result.code | Should -Be -1 - $result.message | Should -Match "was not found" - } } # Test Cases for Auto-Select-PHP-Version From 76a522435fd6e475d9d697b28c2de24cd4d1b226 Mon Sep 17 00:00:00 2001 From: Driss B Date: Fri, 6 Feb 2026 17:37:49 +0100 Subject: [PATCH 26/69] Fix current.tests.ps1 --- tests/current.tests.ps1 | 780 ++++++++++++++++++++-------------------- 1 file changed, 399 insertions(+), 381 deletions(-) diff --git a/tests/current.tests.ps1 b/tests/current.tests.ps1 index 1957242..099ec48 100644 --- a/tests/current.tests.ps1 +++ b/tests/current.tests.ps1 @@ -1,381 +1,399 @@ -# Comprehensive Tests for Get-PHP-Status and Get-Current-PHP-Version Functions - -BeforeAll { - # Mock dependencies - $global:LOG_ERROR_PATH = "TestDrive:\logs\error.log" - $global:PHP_CURRENT_VERSION_PATH = "C:\php\current" - - # Mock Log-Data function - Mock Write-Host {} - function Log-Data { - param($logPath, $message, $data) - return $true - } -} - -Describe "Get-PHP-Status Function Tests" { - Context "When php.ini file exists and is valid" { - It "Should detect enabled opcache extension" { - # Arrange - $testPath = "TestDrive:\php" - New-Item -Path $testPath -ItemType Directory -Force - $phpIniContent = @( - "# PHP Configuration", - "zend_extension=opcache.dll", - "zend_extension=some_other.dll" - ) - $phpIniContent | Out-File -FilePath "$testPath\php.ini" - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $true - $result.xdebug | Should -Be $false - } - - It "Should detect enabled xdebug extension" { - # Arrange - $testPath = "TestDrive:\php" - New-Item -Path $testPath -ItemType Directory -Force - $phpIniContent = @( - "# PHP Configuration", - "zend_extension=xdebug.dll", - "extension=mysqli.dll" - ) - $phpIniContent | Out-File -FilePath "$testPath\php.ini" - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $false - $result.xdebug | Should -Be $true - } - - It "Should detect both opcache and xdebug when enabled" { - # Arrange - $testPath = "TestDrive:\php" - New-Item -Path $testPath -ItemType Directory -Force - $phpIniContent = @( - "zend_extension=opcache.dll", - "zend_extension=xdebug.dll" - ) - $phpIniContent | Out-File -FilePath "$testPath\php.ini" - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $true - $result.xdebug | Should -Be $true - } - - It "Should detect disabled (commented) opcache extension" { - # Arrange - $testPath = "TestDrive:\php" - New-Item -Path $testPath -ItemType Directory -Force - $phpIniContent = @( - "; Disabled opcache", - ";zend_extension=opcache.dll", - "extension=mysqli.dll" - ) - $phpIniContent | Out-File -FilePath "$testPath\php.ini" - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $false - $result.xdebug | Should -Be $false - } - - It "Should detect disabled (commented) xdebug extension" { - # Arrange - $testPath = "TestDrive:\php" - New-Item -Path $testPath -ItemType Directory -Force - $phpIniContent = @( - "; Disabled xdebug", - ";zend_extension=xdebug.dll" - ) - $phpIniContent | Out-File -FilePath "$testPath\php.ini" - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $false - $result.xdebug | Should -Be $false - } - - It "Should handle mixed enabled/disabled extensions" { - # Arrange - $testPath = "TestDrive:\php" - New-Item -Path $testPath -ItemType Directory -Force - $phpIniContent = @( - "zend_extension=opcache.dll", - ";zend_extension=xdebug.dll" - ) - $phpIniContent | Out-File -FilePath "$testPath\php.ini" - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $true - $result.xdebug | Should -Be $false - } - - It "Should handle extensions with full paths" { - # Arrange - $testPath = "TestDrive:\php" - New-Item -Path $testPath -ItemType Directory -Force - $phpIniContent = @( - 'zend_extension="C:\php\ext\opcache.dll"', - 'zend_extension="C:\php\ext\xdebug.dll"' - ) - $phpIniContent | Out-File -FilePath "$testPath\php.ini" - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $true - $result.xdebug | Should -Be $true - } - - It "Should handle extensions with spaces in configuration" { - # Arrange - $testPath = "TestDrive:\php" - New-Item -Path $testPath -ItemType Directory -Force - $phpIniContent = @( - " zend_extension = opcache.dll ", - " ; zend_extension = xdebug.dll " - ) - $phpIniContent | Out-File -FilePath "$testPath\php.ini" - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $true - $result.xdebug | Should -Be $false - } - - It "Should return false for both when no zend_extensions found" { - # Arrange - $testPath = "TestDrive:\php" - New-Item -Path $testPath -ItemType Directory -Force - $phpIniContent = @( - "# PHP Configuration", - "extension=mysqli.dll", - "memory_limit=128M" - ) - $phpIniContent | Out-File -FilePath "$testPath\php.ini" - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $false - $result.xdebug | Should -Be $false - } - - It "Should handle empty php.ini file" { - # Arrange - $testPath = "TestDrive:\php" - New-Item -Path $testPath -ItemType Directory -Force - "" | Out-File -FilePath "$testPath\php.ini" - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $false - $result.xdebug | Should -Be $false - } - } - - Context "When php.ini file does not exist" { - It "Should return -1 when php.ini is missing" { - # Arrange - $testPath = "TestDrive:\nonexistent" - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $false - $result.xdebug | Should -Be $false - } - } - - Context "When exceptions occur" { - It "Should handle Get-Content exceptions gracefully" { - # Arrange - Create a directory instead of a file to cause Get-Content to fail - $testPath = "TestDrive:\php" - New-Item -Path $testPath -ItemType Directory -Force - New-Item -Path "$testPath\php.ini" -ItemType Directory -Force - - # Act - $result = Get-PHP-Status -phpPath $testPath - - # Assert - $result.opcache | Should -Be $false - $result.xdebug | Should -Be $false - } - } -} - -Describe "Get-Current-PHP-Version Function Tests" { - Context "When PHP current version symlink exists and is valid" { - BeforeEach { - # Mock Get-Item to return a symlink object - Mock Get-Item { - return @{ - Target = "C:\php\8.2.0" - } - } -ParameterFilter { $Path -eq $PHP_CURRENT_VERSION_PATH } - - # Mock Get-PHP-Status - Mock Get-PHP-Status { - return @{ opcache = $true; xdebug = $false } - } - } - - It "Should return correct version information when symlink is valid" { - # Act - Mock Is-Directory-Exists { return $true } - $result = Get-Current-PHP-Version - - # Assert - $result.version | Should -Be "8.2.0" - $result.path | Should -Be "C:\php\8.2.0" - $result.status.opcache | Should -Be $true - $result.status.xdebug | Should -Be $false - } - - It "Should call Get-PHP-Status with correct path" { - Mock Is-Directory-Exists { return $true } - # Act - $result = Get-Current-PHP-Version - - # Assert - Assert-MockCalled Get-PHP-Status -Times 1 -ParameterFilter { $phpPath -eq "C:\php\8.2.0" } - } - } - - Context "When PHP current version path does not exist" { - BeforeEach { - # Mock Get-Item to throw an exception - Mock Get-Item { - throw "Path does not exist" - } -ParameterFilter { $Path -eq $PHP_CURRENT_VERSION_PATH } - } - - It "Should return null values when path does not exist" { - # Act - $result = Get-Current-PHP-Version - - # Assert - $result.version | Should -Be $null - $result.path | Should -Be $null - $result.status.opcache | Should -Be $false - $result.status.xdebug | Should -Be $false - } - - It "Should call Log-Data when exception occurs" { - # Arrange - Mock Log-Data { return $true } - - # Act - $result = Get-Current-PHP-Version - - # Assert - Assert-MockCalled Log-Data -Times 1 - } - } - - Context "When Get-Item returns null" { - BeforeEach { - Mock Get-Item { - return $null - } -ParameterFilter { $Path -eq $PHP_CURRENT_VERSION_PATH } - } - - It "Should handle null Get-Item result" { - # Act - $result = Get-Current-PHP-Version - - # Assert - $result.version | Should -Be $null - $result.path | Should -Be $null - $result.status.opcache | Should -Be $false - $result.status.xdebug | Should -Be $false - } - } - - Context "When Get-PHP-Status fails" { - BeforeEach { - Mock Get-Item { - return @{ - Target = "C:\php\8.1.0" - } - } -ParameterFilter { $Path -eq $PHP_CURRENT_VERSION_PATH } - - # Mock Get-PHP-Status to return -1 (error case) - Mock Get-PHP-Status { - return @{ opcache = $false; xdebug = $false } - } - } - - It "Should handle Get-PHP-Status error gracefully" { - Mock Is-Directory-Exists { return $true } - # Act - $result = Get-Current-PHP-Version - - # Assert - $result.version | Should -Be "8.1.0" - $result.path | Should -Be "C:\php\8.1.0" - $result.status.opcache | Should -Be $false - $result.status.xdebug | Should -Be $false - } - } -} - -Describe "Integration Tests" { - Context "Real-world scenarios" { - It "Should work end-to-end with actual file system" { - # Arrange - $testPhpPath = "TestDrive:\php\8.2.0" - $testCurrentPath = "TestDrive:\php\current" - - New-Item -Path $testPhpPath -ItemType Directory -Force - - $phpIniContent = @( - "zend_extension=opcache.dll", - ";zend_extension=xdebug.dll", - "memory_limit=256M" - ) - $phpIniContent | Out-File -FilePath "$testPhpPath\php.ini" - - # Mock the global variable and Get-Item for this test - $global:PHP_CURRENT_VERSION_PATH = $testCurrentPath - - Mock Get-Item { - return @{ - Target = $testPhpPath - } - } -ParameterFilter { $Path -eq $testCurrentPath } - - # Act - $result = Get-Current-PHP-Version - - # Assert - $result.version | Should -Be "8.2.0" - $result.path | Should -Be $testPhpPath - $result.status.opcache | Should -Be $true - $result.status.xdebug | Should -Be $false - } - } -} +# Comprehensive Tests for Get-PHP-Status and Get-Current-PHP-Version Functions + +BeforeAll { + # Mock dependencies + $global:LOG_ERROR_PATH = "TestDrive:\logs\error.log" + $global:PHP_CURRENT_VERSION_PATH = "C:\php\current" + + # Mock Log-Data function + Mock Write-Host {} + function Log-Data { + param($logPath, $message, $data) + return $true + } +} + +Describe "Get-PHP-Status Function Tests" { + Context "When php.ini file exists and is valid" { + It "Should detect enabled opcache extension" { + # Arrange + $testPath = "TestDrive:\php" + New-Item -Path $testPath -ItemType Directory -Force + $phpIniContent = @( + "# PHP Configuration", + "zend_extension=opcache.dll", + "zend_extension=some_other.dll" + ) + $phpIniContent | Out-File -FilePath "$testPath\php.ini" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $true + $result.xdebug | Should -Be $false + } + + It "Should detect enabled xdebug extension" { + # Arrange + $testPath = "TestDrive:\php" + New-Item -Path $testPath -ItemType Directory -Force + $phpIniContent = @( + "# PHP Configuration", + "zend_extension=xdebug.dll", + "extension=mysqli.dll" + ) + $phpIniContent | Out-File -FilePath "$testPath\php.ini" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $false + $result.xdebug | Should -Be $true + } + + It "Should detect both opcache and xdebug when enabled" { + # Arrange + $testPath = "TestDrive:\php" + New-Item -Path $testPath -ItemType Directory -Force + $phpIniContent = @( + "zend_extension=opcache.dll", + "zend_extension=xdebug.dll" + ) + $phpIniContent | Out-File -FilePath "$testPath\php.ini" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $true + $result.xdebug | Should -Be $true + } + + It "Should detect disabled (commented) opcache extension" { + # Arrange + $testPath = "TestDrive:\php" + New-Item -Path $testPath -ItemType Directory -Force + $phpIniContent = @( + "; Disabled opcache", + ";zend_extension=opcache.dll", + "extension=mysqli.dll" + ) + $phpIniContent | Out-File -FilePath "$testPath\php.ini" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $false + $result.xdebug | Should -Be $false + } + + It "Should detect disabled (commented) xdebug extension" { + # Arrange + $testPath = "TestDrive:\php" + New-Item -Path $testPath -ItemType Directory -Force + $phpIniContent = @( + "; Disabled xdebug", + ";zend_extension=xdebug.dll" + ) + $phpIniContent | Out-File -FilePath "$testPath\php.ini" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $false + $result.xdebug | Should -Be $false + } + + It "Should handle mixed enabled/disabled extensions" { + # Arrange + $testPath = "TestDrive:\php" + New-Item -Path $testPath -ItemType Directory -Force + $phpIniContent = @( + "zend_extension=opcache.dll", + ";zend_extension=xdebug.dll" + ) + $phpIniContent | Out-File -FilePath "$testPath\php.ini" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $true + $result.xdebug | Should -Be $false + } + + It "Should handle extensions with full paths" { + # Arrange + $testPath = "TestDrive:\php" + New-Item -Path $testPath -ItemType Directory -Force + $phpIniContent = @( + 'zend_extension="C:\php\ext\opcache.dll"', + 'zend_extension="C:\php\ext\xdebug.dll"' + ) + $phpIniContent | Out-File -FilePath "$testPath\php.ini" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $true + $result.xdebug | Should -Be $true + } + + It "Should handle extensions with spaces in configuration" { + # Arrange + $testPath = "TestDrive:\php" + New-Item -Path $testPath -ItemType Directory -Force + $phpIniContent = @( + " zend_extension = opcache.dll ", + " ; zend_extension = xdebug.dll " + ) + $phpIniContent | Out-File -FilePath "$testPath\php.ini" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $true + $result.xdebug | Should -Be $false + } + + It "Should return false for both when no zend_extensions found" { + # Arrange + $testPath = "TestDrive:\php" + New-Item -Path $testPath -ItemType Directory -Force + $phpIniContent = @( + "# PHP Configuration", + "extension=mysqli.dll", + "memory_limit=128M" + ) + $phpIniContent | Out-File -FilePath "$testPath\php.ini" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $false + $result.xdebug | Should -Be $false + } + + It "Should handle empty php.ini file" { + # Arrange + $testPath = "TestDrive:\php" + New-Item -Path $testPath -ItemType Directory -Force + "" | Out-File -FilePath "$testPath\php.ini" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $false + $result.xdebug | Should -Be $false + } + } + + Context "When php.ini file does not exist" { + It "Should return -1 when php.ini is missing" { + # Arrange + $testPath = "TestDrive:\nonexistent" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $false + $result.xdebug | Should -Be $false + } + } + + Context "When exceptions occur" { + It "Should handle Get-Content exceptions gracefully" { + # Arrange - Create a directory instead of a file to cause Get-Content to fail + $testPath = "TestDrive:\php" + New-Item -Path $testPath -ItemType Directory -Force + New-Item -Path "$testPath\php.ini" -ItemType Directory -Force + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + $result.opcache | Should -Be $false + $result.xdebug | Should -Be $false + } + } +} + +Describe "Get-Current-PHP-Version Function Tests" { + Context "When PHP current version symlink exists and is valid" { + BeforeEach { + # Mock Get-Item to return a symlink object + Mock Get-Item { + return @{ + Target = "C:\php\8.2.0" + } + } -ParameterFilter { $Path -eq $PHP_CURRENT_VERSION_PATH } + + # Mock Get-PHP-Status + Mock Get-PHP-Status { + return @{ opcache = $true; xdebug = $false } + } + } + + It "Should return correct version information when symlink is valid" { + # Act + Mock Get-PHPInstallInfo {@{ + Version = '8.2.0' + Arch = 'x64' + BuildType = 'ts' + InstallPath = 'C:\php\8.2.0' + }} + Mock Is-Directory-Exists { return $true } + $result = Get-Current-PHP-Version + + # Assert + $result.version | Should -Be "8.2.0" + $result.path | Should -Be "C:\php\8.2.0" + $result.status.opcache | Should -Be $true + $result.status.xdebug | Should -Be $false + } + + It "Should call Get-PHP-Status with correct path" { + Mock Is-Directory-Exists { return $true } + # Act + $result = Get-Current-PHP-Version + + # Assert + Assert-MockCalled Get-PHP-Status -Times 1 -ParameterFilter { $phpPath -eq "C:\php\8.2.0" } + } + } + + Context "When PHP current version path does not exist" { + BeforeEach { + # Mock Get-Item to throw an exception + Mock Get-Item { + throw "Path does not exist" + } -ParameterFilter { $Path -eq $PHP_CURRENT_VERSION_PATH } + } + + It "Should return null values when path does not exist" { + # Act + $result = Get-Current-PHP-Version + + # Assert + $result.version | Should -Be $null + $result.path | Should -Be $null + $result.status.opcache | Should -Be $false + $result.status.xdebug | Should -Be $false + } + + It "Should call Log-Data when exception occurs" { + # Arrange + Mock Log-Data { return $true } + + # Act + $result = Get-Current-PHP-Version + + # Assert + Assert-MockCalled Log-Data -Times 1 + } + } + + Context "When Get-Item returns null" { + BeforeEach { + Mock Get-Item { + return $null + } -ParameterFilter { $Path -eq $PHP_CURRENT_VERSION_PATH } + } + + It "Should handle null Get-Item result" { + # Act + $result = Get-Current-PHP-Version + + # Assert + $result.version | Should -Be $null + $result.path | Should -Be $null + $result.status.opcache | Should -Be $false + $result.status.xdebug | Should -Be $false + } + } + + Context "When Get-PHP-Status fails" { + BeforeEach { + Mock Get-Item { + return @{ + Target = "C:\php\8.1.0" + } + } -ParameterFilter { $Path -eq $PHP_CURRENT_VERSION_PATH } + + # Mock Get-PHP-Status to return -1 (error case) + Mock Get-PHP-Status { + return @{ opcache = $false; xdebug = $false } + } + } + + It "Should handle Get-PHP-Status error gracefully" { + Mock Get-PHPInstallInfo {@{ + Version = '8.1.0' + Arch = 'x64' + BuildType = 'ts' + InstallPath = 'C:\php\8.1.0' + }} + Mock Is-Directory-Exists { return $true } + # Act + $result = Get-Current-PHP-Version + + # Assert + $result.version | Should -Be "8.1.0" + $result.path | Should -Be "C:\php\8.1.0" + $result.status.opcache | Should -Be $false + $result.status.xdebug | Should -Be $false + } + } +} + +Describe "Integration Tests" { + Context "Real-world scenarios" { + It "Should work end-to-end with actual file system" { + Mock Get-PHPInstallInfo {@{ + Version = '8.2.0' + Arch = 'x64' + BuildType = 'ts' + InstallPath = 'TestDrive:\php\8.2.0' + }} + # Arrange + $testPhpPath = "TestDrive:\php\8.2.0" + $testCurrentPath = "TestDrive:\php\current" + + New-Item -Path $testPhpPath -ItemType Directory -Force + + $phpIniContent = @( + "zend_extension=opcache.dll", + ";zend_extension=xdebug.dll", + "memory_limit=256M" + ) + $phpIniContent | Out-File -FilePath "$testPhpPath\php.ini" + + # Mock the global variable and Get-Item for this test + $global:PHP_CURRENT_VERSION_PATH = $testCurrentPath + + Mock Get-Item { + return @{ + Target = $testPhpPath + } + } -ParameterFilter { $Path -eq $testCurrentPath } + + # Act + $result = Get-Current-PHP-Version + + # Assert + $result.version | Should -Be "8.2.0" + $result.path | Should -Be $testPhpPath + $result.status.opcache | Should -Be $true + $result.status.xdebug | Should -Be $false + } + } +} From 5666b0430ec50dab84d1fe96975beabac3cc4b31 Mon Sep 17 00:00:00 2001 From: Driss B Date: Fri, 6 Feb 2026 17:39:09 +0100 Subject: [PATCH 27/69] Fix uninstall.tests.ps1 --- tests/uninstall.tests.ps1 | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/tests/uninstall.tests.ps1 b/tests/uninstall.tests.ps1 index d29e47e..5c07cb3 100644 --- a/tests/uninstall.tests.ps1 +++ b/tests/uninstall.tests.ps1 @@ -27,6 +27,9 @@ Describe "Uninstall-PHP" { } It "Should successfully uninstall when version is found directly" { + Mock Get-UserSelected-PHP-Version -MockWith { + return @{ code = 0; version = '7.4'; arch = 'x86'; buildType = 'nts'; path = "$testPhpPath\7.4" } + } $result = Uninstall-PHP -version "7.4" $result.code | Should -Be 0 @@ -39,7 +42,10 @@ Describe "Uninstall-PHP" { } It "Should prompt user when trying to uninstall current version" { - Mock Get-Current-PHP-Version { @{ version = "7.4" } } + Mock Get-UserSelected-PHP-Version -MockWith { + return @{ code = 0; version = '7.4'; arch = 'x64'; buildType = 'nts'; path = "$testPhpPath\7.4" } + } + Mock Get-Current-PHP-Version { @{ version = "7.4"; arch = 'x64'; buildType = 'nts' } } Mock Read-Host { } $result = Uninstall-PHP -version "7.4" $result.code | Should -Be -1 @@ -48,7 +54,10 @@ Describe "Uninstall-PHP" { } It "Should prompt user when trying to uninstall current version and handle 'n' response" { - Mock Get-Current-PHP-Version { @{ version = "8.0" } } + Mock Get-UserSelected-PHP-Version -MockWith { + return @{ code = 0; version = '8.0'; arch = 'x64'; buildType = 'nts'; path = "$testPhpPath\8.0" } + } + Mock Get-Current-PHP-Version { @{ version = "8.0"; arch = 'x64'; buildType = 'nts' } } Mock Read-Host { "n" } $result = Uninstall-PHP -version "8.0" $result.code | Should -Be -1 @@ -58,7 +67,10 @@ Describe "Uninstall-PHP" { } It "Should prompt user when trying to uninstall current version and handle 'y' response" { - Mock Get-Current-PHP-Version { @{ version = "8.0" } } + Mock Get-UserSelected-PHP-Version -MockWith { + return @{ code = 0; version = '8.0'; arch = 'x64'; buildType = 'nts'; path = "$testPhpPath\8.0" } + } + Mock Get-Current-PHP-Version { @{ version = "8.0"; arch = 'x64'; buildType = 'nts' } } Mock Read-Host { "y" } $result = Uninstall-PHP -version "8.0" $result.code | Should -Be 0 @@ -154,7 +166,9 @@ Describe "Uninstall-PHP" { } It "Should return version not found message" { - $result = Uninstall-PHP -version "8.*" + Mock Get-Matching-PHP-Versions { return $null } + Mock Get-UserSelected-PHP-Version { return $null } + $result = Uninstall-PHP -version "8.2" $result.code | Should -Be -1 $result.message | Should -BeExactly "PHP version 8.2 was not found!" @@ -175,6 +189,10 @@ Describe "Uninstall-PHP" { } It "Should catch the exception and return error message" { + Mock Get-UserSelected-PHP-Version -MockWith { + return @{ code = 0; version = '7.4'; arch = 'x64'; buildType = 'nts'; path = "$testPhpPath\7.4" } + } + Mock Refresh-Installed-PHP-Versions-Cache { throw 'Error' } $result = Uninstall-PHP -version "7.4" $result.code | Should -Be -1 From 32c722322cbb8d39956660bca386779c5ff2df12 Mon Sep 17 00:00:00 2001 From: Driss B Date: Fri, 6 Feb 2026 18:59:12 +0100 Subject: [PATCH 28/69] Fix list.tests.ps1 --- tests/list.tests.ps1 | 55 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/tests/list.tests.ps1 b/tests/list.tests.ps1 index 2414a29..d3a69d4 100644 --- a/tests/list.tests.ps1 +++ b/tests/list.tests.ps1 @@ -91,11 +91,11 @@ Describe "Get-From-Source" { Mock Cache-Data { } - $result = Get-From-Source + $result = Get-From-Source -arch x86 $allVersions = $result['Archives'] + $result['Releases'] - $allVersions | Should -Contain 'php-8.2.0-Win32-x86.zip' - $allVersions | Should -Not -Contain 'php-8.2.0-Win32-x64.zip' + $allVersions | Where-Object { $_.link -eq 'php-8.2.0-Win32-x86.zip' } | Should -Not -BeNullOrEmpty + $allVersions | Where-Object { $_.link -eq 'php-8.2.0-Win32-x64.zip' } | Should -BeNullOrEmpty } It "Should return empty list" { @@ -263,8 +263,18 @@ Describe "Get-Available-PHP-Versions" { It "Should display versions in correct format" { Mock Get-Data-From-Cache { return @{ - 'Archives' = @('php-8.1.0-Win32-x64.zip') - 'Releases' = @('php-8.2.0-Win32-x64.zip') + 'Archives' = @(@{ + BuildType = "NTS"; + Version = "8.1.0"; + Link = "php-8.1.0-Win32-x64.zip"; + Arch = "x86" + }) + 'Releases' = @(@{ + BuildType = "NTS"; + Version = "8.2.0"; + Link = "php-8.2.0-Win32-x64.zip"; + Arch = "x64"; + }) } } Mock Test-Path { return $true } @@ -310,8 +320,16 @@ Describe "Display-Installed-PHP-Versions" { } It "Should display installed versions with current version marked" { - Mock Get-Current-PHP-Version { return @{ version = "8.2.0" } } - Mock Get-Installed-PHP-Versions { return @("8.2.0", "8.1.5", "7.4.33") } + Mock Get-Current-PHP-Version { return @{ + version = "8.2.0" + arch = "x64" + buildType = "nts" + }} + Mock Get-Installed-PHP-Versions { return @( + @{Version = "8.2.0"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1.5"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "7.4.33"; Arch = "x64"; BuildType = 'NTS'} + )} Display-Installed-PHP-Versions @@ -322,7 +340,11 @@ Describe "Display-Installed-PHP-Versions" { } It "Display installed versions matching filter" { - Mock Get-Installed-PHP-Versions { return @("8.2.0", "8.2.0", "8.1.5") } + Mock Get-Installed-PHP-Versions { return @( + @{Version = "8.2.0"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.2.0"; Arch = "x64"; BuildType = 'TS'} + @{Version = "8.1.5"; Arch = "x64"; BuildType = 'NTS'} + )} $code = Display-Installed-PHP-Versions -term "8.2" $code | Should -Be 0 } @@ -337,8 +359,16 @@ Describe "Display-Installed-PHP-Versions" { } It "Should handle duplicate versions" { - Mock Get-Current-PHP-Version { return @{ version = "8.2.0" } } - Mock Get-Installed-PHP-Versions { return @("php8.2.0", "php8.2.0", "php8.1.5") } + Mock Get-Current-PHP-Version { return @{ + version = "8.2.0" + arch = "x64" + buildType = "NTS" + }} + Mock Get-Installed-PHP-Versions { return @( + @{Version = "8.2.0"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.2.0"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1.5"; Arch = "x64"; BuildType = 'NTS'} + )} Display-Installed-PHP-Versions @@ -348,7 +378,10 @@ Describe "Display-Installed-PHP-Versions" { It "Should handle no current version set" { Mock Get-Current-PHP-Version { return @{ version = "" } } - Mock Get-Installed-PHP-Versions { return @("php8.2.0", "php8.1.5") } + Mock Get-Installed-PHP-Versions { return @( + @{Version = "8.2.0"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1.5"; Arch = "x64"; BuildType = 'NTS'} + )} Display-Installed-PHP-Versions From 572d500cded84ba27fbc028e033562b8e947f7cb Mon Sep 17 00:00:00 2001 From: Driss B Date: Fri, 6 Feb 2026 20:19:55 +0100 Subject: [PATCH 29/69] Fix install.tests.ps1 --- src/actions/install.ps1 | 2 +- tests/install.tests.ps1 | 61 +++++++++++++++++++---------------------- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/actions/install.ps1 b/src/actions/install.ps1 index 681b428..f55815e 100644 --- a/src/actions/install.ps1 +++ b/src/actions/install.ps1 @@ -58,7 +58,7 @@ function Get-PHP-Versions { } $fetchedVersions[$key] = @() - $fetched | ForEach-Object { + $fetched | ForEach-Object { if ($found -notcontains $_.fileName) { $fetchedVersions[$key] += $_ $found += $_.fileName diff --git a/tests/install.tests.ps1 b/tests/install.tests.ps1 index 729b1b2..fd8b894 100644 --- a/tests/install.tests.ps1 +++ b/tests/install.tests.ps1 @@ -303,9 +303,10 @@ Describe "Get-PHP-Versions-From-Url Tests" { $result = Get-PHP-Versions-From-Url -url "https://test.com" -version "8.1" - $result.Count | Should -Be 2 + $result.Count | Should -Be 3 $result[0].version | Should -Be "8.1.0" $result[1].version | Should -Be "8.1.1" + $result[2].version | Should -Be "8.1.0" } It "Should handle network errors gracefully" { @@ -327,8 +328,9 @@ Describe "Get-PHP-Versions-From-Url Tests" { $result = Get-PHP-Versions-From-Url -url "https://test.com" -version "8.1" - $result.Length | Should -Be 1 - $result.version | Should -Be "8.1.0" + $result.Length | Should -Be 2 + $result[0].version | Should -Be "8.1.0" + $result[1].version | Should -Be "8.1.0" } } @@ -353,13 +355,7 @@ Describe "Get-PHP-Versions Tests" { } It "Should handle exception gracefully" { - Mock Get-PHP-Versions-From-Url { - return @( - @{ version = "8.1.0"; fileName = "php-8.1.0-Win32-vs16-x64.zip" }, - @{ version = "8.1.1"; fileName = "php-8.1.1-Win32-vs16-x64.zip" } - ) - } - Mock Where-Object { throw "Test exception" } + Mock Get-Source-Urls { throw 'Error' } $result = Get-PHP-Versions -version "8.1" @@ -547,7 +543,7 @@ Describe "Select-Version Tests" { @{ version = "8.1.1"; fileName = "php-8.1.1.zip" } ) } - $global:MockUserInput = "8.1.1" + $global:MockUserInput = "1" $result = Select-Version -matchingVersions $versions @@ -557,7 +553,7 @@ Describe "Select-Version Tests" { Describe "Install-PHP Integration Tests" { BeforeEach { - # Mock Write-Host { } + Mock Write-Host { } Reset-MockState $global:MockUserInput = "" $global:MockFileSystem.Files["TestDrive:\pvm\pvm"] = "PVM executable" @@ -622,11 +618,11 @@ Describe "Install-PHP Integration Tests" { Mock Get-Matching-PHP-Versions { return $null } Mock Download-PHP-From-Url { return "TestDrive:\php"} Mock Select-Version { return @{ version = "8.1.15"; fileName = "php-8.1.15-Win32-vs16-x64.zip" } } - Mock Is-PHP-Version-Installed -ParameterFilter { $version -eq "8.1.15" } -MockWith { return $true } + Mock Is-PHP-Version-Installed { return $true } $result = Install-PHP -version "8.1" - $result.code | Should -Be -1 + $result.code | Should -Be -1 } It "Handles exception gracefully" { @@ -757,21 +753,23 @@ Describe "Environment Variable Tests" { } It "Get-Installed-PHP-Versions should return sorted versions" { - Mock Test-Path { return $true } - Mock Get-All-Subdirectories { - param ($path) + Mock Cache-Data { return 0 } + Mock Can-Use-Cache { return $false } + Mock Get-Installed-PHP-Versions-From-Directory { return @( - @{ Name = "8.1"; FullName = "path\php\8.1" } - @{ Name = "7.4"; FullName = "path\php\7.4" } - @{ Name = "8.2"; FullName = "path\php\8.2" } - @{ Name = "8.0"; FullName = "path\php\8.0" } - @{ Name = "5.6"; FullName = "path\php\5.6" } + @{version = "8.2"; arch = "x64"; buildType = "nts"} + @{version = "8.1"; arch = "x64"; buildType = "nts"} + @{version = "8.0"; arch = "x64"; buildType = "nts"} + @{version = "7.4"; arch = "x64"; buildType = "nts"} + @{version = "5.6"; arch = "x64"; buildType = "nts"} ) } $result = Get-Installed-PHP-Versions + Write-Host ($result | ConvertTo-Json) - $result | Should -Be @("5.6", "7.4", "8.0", "8.1", "8.2") + $result[0].version | Should -Be "8.2" + $result[1].version | Should -Be "8.1" } It "Get-Installed-PHP-Versions should handle registry errors" { @@ -783,21 +781,18 @@ Describe "Environment Variable Tests" { } It "Get-Matching-PHP-Versions should find matching versions" { - Mock Test-Path { return $true } - Mock Get-All-Subdirectories { - param ($path) + Mock Get-Installed-PHP-Versions { return @( - @{ Name = "8.1.0"; FullName = "path\php\8.1.0" } - @{ Name = "8.2.0"; FullName = "path\php\8.2.0" } - @{ Name = "8.1.5"; FullName = "path\php\8.1.5" } + @{version = "8.1.0"; arch = "x64"; buildType = "nts"} + @{version = "8.2.0"; arch = "x64"; buildType = "nts"} + @{version = "8.1.5"; arch = "x64"; buildType = "nts"} ) } - $result = Get-Matching-PHP-Versions -version "8.1" - $result | Should -Contain "8.1.0" - $result | Should -Contain "8.1.5" - $result | Should -Not -Contain "8.2.0" + $result | Where-Object { $_.version -eq '8.1.0' } | Should -Not -BeNullOrEmpty + $result | Where-Object { $_.version -eq '8.1.5' } | Should -Not -BeNullOrEmpty + $result | Where-Object { $_.link -eq '8.2.0' } | Should -BeNullOrEmpty } } From cf8e718c218ad56c2f9db64f32ae6ad136359c8f Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 01:01:26 +0100 Subject: [PATCH 30/69] Remove unused function --- tests/install.tests.ps1 | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/install.tests.ps1 b/tests/install.tests.ps1 index fd8b894..5daea63 100644 --- a/tests/install.tests.ps1 +++ b/tests/install.tests.ps1 @@ -266,13 +266,6 @@ BeforeAll { return -1 } } - - function Is-PHP-Version-Installed { - param($version) - - $envVars = Get-All-EnvVars - return $envVars.ContainsKey("php$version") - } } # Test Suites From bbae42ef8e7be21658b47db8b41c86f0f5b50e9f Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 01:02:05 +0100 Subject: [PATCH 31/69] Fix common.tests.ps1 --- tests/common.tests.ps1 | 149 +++++++++++++++++++---------------------- 1 file changed, 68 insertions(+), 81 deletions(-) diff --git a/tests/common.tests.ps1 b/tests/common.tests.ps1 index 08521cd..3f5c4bc 100644 --- a/tests/common.tests.ps1 +++ b/tests/common.tests.ps1 @@ -91,25 +91,24 @@ Describe "Get-Installed-PHP-Versions" { It "Should return sorted PHP versions" { $script:STORAGE_PATH = "C:\mock\path" $script:LOG_ERROR_PATH = "C:\mock\error" - Mock Test-Path { return $true } - Mock Get-All-Subdirectories { - param ($path) + Mock Cache-Data { return 0 } + Mock Can-Use-Cache { return $false } + Mock Get-Installed-PHP-Versions-From-Directory { return @( - @{ Name = "8.1"; FullName = "path\php\8.1" } - @{ Name = "7.4"; FullName = "path\php\7.4" } - @{ Name = "8.2"; FullName = "path\php\8.2" } - @{ Name = "8.0"; FullName = "path\php\8.0" } - @{ Name = "5.6"; FullName = "path\php\5.6" } + @{version = "5.6"; arch = "x64"; buildType = "nts"} + @{version = "7.4"; arch = "x64"; buildType = "nts"} + @{version = "8.0"; arch = "x64"; buildType = "nts"} + @{version = "8.1"; arch = "x64"; buildType = "nts"} + @{version = "8.2"; arch = "x64"; buildType = "nts"} ) } - Mock Log-Data { return $true } $result = Get-Installed-PHP-Versions $expected = @("5.6", "7.4", "8.0", "8.1", "8.2") $result.Count | Should -Be $expected.Count for ($i = 0; $i -lt $result.Count; $i++) { - $result[$i] | Should -Be $expected[$i] + $result[$i].version | Should -Be $expected[$i] } } @@ -127,20 +126,19 @@ Describe "Get-Installed-PHP-Versions" { } It "Should handle single digit versions" { - Mock Test-Path { return $true } - Mock Get-All-Subdirectories { - param ($path) + Mock Cache-Data { return 0 } + Mock Can-Use-Cache { return $false } + Mock Get-Installed-PHP-Versions-From-Directory { return @( - @{ Name = "8.1"; FullName = "path\php\8.1" } - @{ Name = "7.4"; FullName = "path\php\7.4" } + @{version = "7.4"; arch = "x64"; buildType = "nts"} + @{version = "8.1"; arch = "x64"; buildType = "nts"} ) } - Mock Log-Data { return $true } $result = Get-Installed-PHP-Versions $result.Count | Should -Be 2 - $result[0] | Should -Be "7.4" - $result[1] | Should -Be "8.1" + $result[0].version | Should -Be "7.4" + $result[1].version | Should -Be "8.1" } } @@ -166,7 +164,7 @@ Describe "Get-UserSelected-PHP-Version" { } It "Should return first version when only one is provided" { - $result = Get-UserSelected-PHP-Version -installedVersions @("8.1") + $result = Get-UserSelected-PHP-Version -installedVersions @(@{ version = '8.1'; Arch = 'x64'; BuildType = 'ts'}) $result.version | Should -Be "8.1" } @@ -174,15 +172,23 @@ Describe "Get-UserSelected-PHP-Version" { Mock Read-Host { return "" } Mock Write-Host { } - $result = Get-UserSelected-PHP-Version -installedVersions @("7.4", "8.0", "8.1") + $result = Get-UserSelected-PHP-Version -installedVersions @( + @{ version = '7.4'; Arch = 'x64'; BuildType = 'ts'} + @{ version = '8.0'; Arch = 'x64'; BuildType = 'ts'} + @{ version = '8.1'; Arch = 'x64'; BuildType = 'ts'} + ) $result.code | Should -Be -1 } It "Should prompt user and return selected version when multiple are provided" { - Mock Read-Host { return "8.1" } + Mock Read-Host { return "2" } Mock Write-Host { } - $result = Get-UserSelected-PHP-Version -installedVersions @("7.4", "8.0", "8.1") + $result = Get-UserSelected-PHP-Version -installedVersions @( + @{ version = '7.4'; Arch = 'x64'; BuildType = 'ts'; InstallPath = "C:\php\7.4"} + @{ version = '8.0'; Arch = 'x64'; BuildType = 'ts'; InstallPath = "C:\php\8.0"} + @{ version = '8.1'; Arch = 'x64'; BuildType = 'ts'; InstallPath = "C:\php\8.1"} + ) $result.version | Should -Be "8.1" $result.code | Should -Be 0 $result.path | Should -Be "C:\php\8.1" @@ -192,40 +198,52 @@ Describe "Get-UserSelected-PHP-Version" { Describe "Get-Matching-PHP-Versions" { Context "When matching versions exist" { It "Should return matching versions for partial version number" { - Mock Get-Installed-PHP-Versions { - return @("7.4", "8.0", "8.1", "8.2") - } - Mock Log-Data { return $true } + Mock Get-Installed-PHP-Versions { return @( + @{Version = "8.2"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.0"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "7.4"; Arch = "x64"; BuildType = 'NTS'} + )} $result = Get-Matching-PHP-Versions -version "8" $expected = @("8.0", "8.1", "8.2") $result.Count | Should -Be $expected.Count - $result -contains "8.0" | Should -Be $true - $result -contains "8.1" | Should -Be $true - $result -contains "8.2" | Should -Be $true + $result | Where-Object { $_.version -eq '8.2' } | Should -Not -BeNullOrEmpty + $result | Where-Object { $_.version -eq '8.1' } | Should -Not -BeNullOrEmpty + $result | Where-Object { $_.version -eq '8.0' } | Should -Not -BeNullOrEmpty } It "Should return exact match for pattern version number" { Mock Get-Installed-PHP-Versions { - return @("7.4", "8.0", "8.1", "8.1.9", "8.2") + return @( + @{Version = "8.2"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1.9"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.0"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "7.4"; Arch = "x64"; BuildType = 'NTS'} + ) } - Mock Log-Data { return $true } $result = Get-Matching-PHP-Versions -version "8.1" $result.Count | Should -Be 2 - $result[0] | Should -Be "8.1" + $result[0].version | Should -Be "8.1.9" } - It "Should return exact match for full version number" { + It "Should return exact match for full version number" { Mock Get-Installed-PHP-Versions { - return @("7.4", "8.0", "8.1", "8.1.9", "8.2") + return @( + @{Version = "7.4"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.0"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1.9"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.2"; Arch = "x64"; BuildType = 'NTS'} + ) } - Mock Log-Data { return $true } $result = Get-Matching-PHP-Versions -version "8.1.9" - $result.Count | Should -Be 1 - $result | Should -Be "8.1.9" + $result.Length | Should -Be 1 + $result.version | Should -Be "8.1.9" } It "Should return empty array when no matches found" { @@ -259,39 +277,37 @@ Describe "Is-PHP-Version-Installed" { It "Should return true for installed version" { Mock Get-Matching-PHP-Versions { param($version) - if ($version -eq "8.1") { - return @("8.1", "8.1.1", "8.1.2") - } - return @() + return @( + @{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1.1"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1.2"; Arch = "x64"; BuildType = 'NTS'} + ) } - Mock Log-Data { return $true } - $result = Is-PHP-Version-Installed -version "8.1" + $result = Is-PHP-Version-Installed -version @{version = "8.1"; Arch = "x64"; BuildType = 'NTS'} $result | Should -Be $true } It "Should return false for non-installed version" { Mock Get-Matching-PHP-Versions { param($version) - if ($version -eq "8.1") { - return @("8.1.1", "8.1.2") # 8.1 exact match not included - } - return @() + return @( + @{Version = "8.1.1"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1.2"; Arch = "x64"; BuildType = 'NTS'} + ) } - Mock Log-Data { return $true } - $result = Is-PHP-Version-Installed -version "8.1" - $result | Should -Be $false + $result = Is-PHP-Version-Installed -version @{version = "8.1"; Arch = "x64"; BuildType = 'NTS'} + $result | Should -Be $null } It "Should return false when no matching versions found" { Mock Get-Matching-PHP-Versions { return @() } - Mock Log-Data { return $true } $result = Is-PHP-Version-Installed -version "9.0" - $result | Should -Be $false + $result | Should -Be $null } } @@ -309,32 +325,3 @@ Describe "Is-PHP-Version-Installed" { } } } - -Describe "Integration Tests" { - Context "When testing function interactions" { - It "Should work together for a complete workflow" { - # Mock the environment to simulate a working PVM setup - Mock Get-EnvVar-ByName -ParameterFilter { $name -eq "Path" } -MockWith { - return "C:\pvm;C:\php\8.1;C:\other\paths" - } - Mock Test-Path { return $true } - - Mock Get-Installed-PHP-Versions { - return @("7.4", "8.0", "8.1", "8.1.9", "8.2") - } - - Mock Log-Data { return $true } - - # Test the complete workflow - $pvmSetup = Is-PVM-Setup - $installedVersions = Get-Installed-PHP-Versions - $matchingVersions = Get-Matching-PHP-Versions -version "8" - $isInstalled = Is-PHP-Version-Installed -version "8.1" - - $pvmSetup | Should -Be $true - $installedVersions -contains "8.1" | Should -Be $true - $matchingVersions -contains "8.1" | Should -Be $true - $isInstalled | Should -Be $true - } - } -} \ No newline at end of file From 076ce5b43868a69df83199eb8d1ef372f4964a27 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 12:34:09 +0100 Subject: [PATCH 32/69] Handle errors for Can-Use-Cache --- src/functions/helpers.ps1 | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index 72f9d39..efaa04e 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -6,15 +6,24 @@ function Get-Zend-Extensions-List { function Can-Use-Cache { param ($cacheFileName) - $path = "$CACHE_PATH\$cacheFileName.json" - $useCache = $false + try { + $path = "$CACHE_PATH\$cacheFileName.json" + $useCache = $false - if (Test-Path $cacheFileName) { - $fileAgeHours = (New-TimeSpan -Start (Get-Item $cacheFileName).LastWriteTime -End (Get-Date)).TotalHours - $useCache = ($fileAgeHours -lt $CACHE_MAX_HOURS) + if (Test-Path $path) { + $fileAgeHours = (New-TimeSpan -Start (Get-Item $path).LastWriteTime -End (Get-Date)).TotalHours + $useCache = ($fileAgeHours -lt $CACHE_MAX_HOURS) + } + + return $useCache + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to get data from cache" + exception = $_ + } + + return $false } - - return $useCache } function Get-Data-From-Cache { From 6a79335b3784b590252f4b1540bb3ef78fc20a26 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 12:34:47 +0100 Subject: [PATCH 33/69] Add tests for Can-Use-Cache --- tests/helpers.tests.ps1 | 107 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/tests/helpers.tests.ps1 b/tests/helpers.tests.ps1 index dc642ce..ea53c6e 100644 --- a/tests/helpers.tests.ps1 +++ b/tests/helpers.tests.ps1 @@ -677,3 +677,110 @@ Describe "Format-Seconds" { } } } + +Describe "Can-Use-Cache" { + BeforeAll { + $global:CACHE_PATH = "TestDrive:\cache" + $global:CACHE_MAX_HOURS = 168 + + New-Item -ItemType Directory -Path $CACHE_PATH -Force | Out-Null + } + Context "When cache file exists" { + It "Returns true when cache file is within max age" { + $cacheFileName = "test_cache" + $cacheFile = "$cacheFileName.json" + + # Create a cache file with recent timestamp + New-Item -Path "$CACHE_PATH\$cacheFile" -ItemType File -Force | Out-Null + Set-Content -Path "$CACHE_PATH\$cacheFile" -Value '{"test": "data"}' + + $result = Can-Use-Cache -cacheFileName $cacheFileName + $result | Should -Be $true + } + + It "Returns false when cache file is older than max age" { + $cacheFileName = "old_cache" + $cacheFile = "$cacheFileName.json" + + # Create a cache file with old timestamp (older than CACHE_MAX_HOURS) + New-Item -Path "$CACHE_PATH\$cacheFile" -ItemType File -Force | Out-Null + Set-Content -Path "$CACHE_PATH\$cacheFile" -Value '{"test": "data"}' + + # Set file modification time to be older than CACHE_MAX_HOURS (168 hours) + $oldTime = (Get-Date).AddHours(-200) + (Get-Item "$CACHE_PATH\$cacheFile").LastWriteTime = $oldTime + + $result = Can-Use-Cache -cacheFileName $cacheFileName + $result | Should -Be $false + } + + It "Returns false when cache file is exactly at max age boundary" { + $cacheFileName = "boundary_cache" + $cacheFile = "$cacheFileName.json" + + # Create a cache file + New-Item -Path "$CACHE_PATH\$cacheFile" -ItemType File -Force | Out-Null + Set-Content -Path "$CACHE_PATH\$cacheFile" -Value '{"test": "data"}' + + # Set file modification time to be exactly at CACHE_MAX_HOURS + $boundaryTime = (Get-Date).AddHours(-$CACHE_MAX_HOURS) + (Get-Item "$CACHE_PATH\$cacheFile").LastWriteTime = $boundaryTime + + $result = Can-Use-Cache -cacheFileName $cacheFileName + # Since the function uses -lt (less than), equality should return false + $result | Should -Be $false + } + } + + Context "When cache file does not exist" { + It "Returns false when cache file does not exist" { + $cacheFileName = "nonexistent_cache" + + $result = Can-Use-Cache -cacheFileName $cacheFileName + $result | Should -Be $false + } + } + + Context "With edge cases" { + It "Returns false for empty cache file name" { + $result = Can-Use-Cache -cacheFileName "" + $result | Should -Be $false + } + + It "Returns false for null cache file name" { + $result = Can-Use-Cache -cacheFileName $null + $result | Should -Be $false + } + + It "Handles exceptions gracefully" { + Mock Test-Path { throw "Simulated exception" } + { Can-Use-Cache -cacheFileName "test" } | Should -Not -Throw + $result = Can-Use-Cache -cacheFileName "test" + $result | Should -Be $false + } + } + + Context "With special file names" { + It "Works with file names containing special characters" { + $cacheFileName = "cache-with_special.chars" + $cacheFile = "$cacheFileName.json" + + New-Item -Path "$CACHE_PATH\$cacheFile" -ItemType File -Force | Out-Null + Set-Content -Path "$CACHE_PATH\$cacheFile" -Value '{"test": "data"}' + + $result = Can-Use-Cache -cacheFileName $cacheFileName + $result | Should -Be $true + } + + It "Works with file names containing numbers" { + $cacheFileName = "cache123available_versions456" + $cacheFile = "$cacheFileName.json" + + New-Item -Path "$CACHE_PATH\$cacheFile" -ItemType File -Force | Out-Null + Set-Content -Path "$CACHE_PATH\$cacheFile" -Value '{"test": "data"}' + + $result = Can-Use-Cache -cacheFileName $cacheFileName + $result | Should -Be $true + } + } +} From bb06d0d370522c7eb683446ed579e2b2cbd112c0 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 12:51:12 +0100 Subject: [PATCH 34/69] Remove unused assertion --- tests/helpers.tests.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/helpers.tests.ps1 b/tests/helpers.tests.ps1 index ea53c6e..608438e 100644 --- a/tests/helpers.tests.ps1 +++ b/tests/helpers.tests.ps1 @@ -178,7 +178,6 @@ Describe "Get-Data-From-Cache" { '@ } $list = Get-Data-From-Cache -cacheFileName "test.json" - $list.Count | Should -Be 2 $list.Releases[0] | Should -Be "/downloads/releases/php-7.4.33-Win32-vc15-x64.zip" $list.Archives[0] | Should -Be "/downloads/releases/archives/php-5.5.0-Win32-VC11-x64.zip" } From c6fda90d8370d9ad8ff5088b5050bd7e84c19254 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 12:51:40 +0100 Subject: [PATCH 35/69] Make sure first match is selected --- src/functions/helpers.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index efaa04e..a0837ce 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -385,7 +385,7 @@ function Is-OS-64Bit { function Resolve-Arch { param ($arguments, $choseDefault = $false) - $arch = $arguments | Where-Object { @('x86', 'x64') -contains $_ } + $arch = $arguments | Where-Object { @('x86', 'x64') -contains $_ } | Select-Object -First 1 if ($null -eq $arch -and $choseDefault) { $arch = if (Is-OS-64Bit) { 'x64' } else { 'x86' } From ea80fc1989bf3c8cd3dd72c6c097d04e72029e6f Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 12:52:29 +0100 Subject: [PATCH 36/69] Add tests for Resolve-Arch --- tests/helpers.tests.ps1 | 89 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/helpers.tests.ps1 b/tests/helpers.tests.ps1 index 608438e..254949e 100644 --- a/tests/helpers.tests.ps1 +++ b/tests/helpers.tests.ps1 @@ -783,3 +783,92 @@ Describe "Can-Use-Cache" { } } } + +Describe "Resolve-Arch" { + Context "When searching in arguments" { + It "Returns x86 when x86 is in arguments" { + $arguments = @("some_arg", "x86", "another_arg") + $result = Resolve-Arch -arguments $arguments + $result | Should -Be "x86" + } + + It "Returns x64 when x64 is in arguments" { + $arguments = @("some_arg", "x64", "another_arg") + $result = Resolve-Arch -arguments $arguments + $result | Should -Be "x64" + } + + It "Returns first matching architecture when multiple are present" { + $arguments = @("x86", "x64", "other") + $result = Resolve-Arch -arguments $arguments + $result | Should -Be "x86" + } + + It "Returns null when no matching architecture in arguments" { + $arguments = @("some_arg", "another_arg", "third_arg") + $result = Resolve-Arch -arguments $arguments + $result | Should -BeNullOrEmpty + } + } + + Context "Case insensitivity" { + It "Returns lowercase x86 when uppercase X86 provided" { + $arguments = @("X86") + $result = Resolve-Arch -arguments $arguments + $result | Should -Be "x86" + } + + It "Returns lowercase x64 when mixed case X64 provided" { + $arguments = @("X64") + $result = Resolve-Arch -arguments $arguments + $result | Should -Be "x64" + } + } + + Context "With default choice" { + It "Returns x64 as default when 64-bit OS and choseDefault is true" { + Mock Is-OS-64Bit { return $true } + $arguments = @("some_arg", "other_arg") + + $result = Resolve-Arch -arguments $arguments -choseDefault $true + $result | Should -Be "x64" + } + + It "Returns x86 as default when 32-bit OS and choseDefault is true" { + Mock Is-OS-64Bit { return $false } + $arguments = @("some_arg", "other_arg") + + $result = Resolve-Arch -arguments $arguments -choseDefault $true + $result | Should -Be "x86" + } + + It "Returns argument arch even when choseDefault is true" { + Mock Is-OS-64Bit { return $true } + $arguments = @("x86", "some_arg") + + $result = Resolve-Arch -arguments $arguments -choseDefault $true + $result | Should -Be "x86" + } + } + + Context "With empty or null inputs" { + It "Returns null when arguments array is empty and choseDefault is false" { + $arguments = @() + $result = Resolve-Arch -arguments $arguments -choseDefault $false + $result | Should -BeNullOrEmpty + } + + It "Returns default when arguments array is empty and choseDefault is true" { + Mock Is-OS-64Bit { return $true } + $arguments = @() + + $result = Resolve-Arch -arguments $arguments -choseDefault $true + $result | Should -Be "x64" + } + + It "Returns null when arguments is null" { + $result = Resolve-Arch -arguments $null + $result | Should -BeNullOrEmpty + } + } +} From 3d8e6b8385fecef03e14872a97f90b0fc5cf976f Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 13:43:17 +0100 Subject: [PATCH 37/69] Fix typo --- tests/setup.tests.ps1 | 448 +++++++++++++++++++++--------------------- 1 file changed, 224 insertions(+), 224 deletions(-) diff --git a/tests/setup.tests.ps1 b/tests/setup.tests.ps1 index d002810..40b92f8 100644 --- a/tests/setup.tests.ps1 +++ b/tests/setup.tests.ps1 @@ -1,225 +1,225 @@ -# Load required modules and functions -. "$PSScriptRoot\..\src\actions\setup.ps1" - -Describe "Setup-PVM" { - BeforeAll { - Mock Write-Host {} - # Mock global variables that the function depends on - $global:PHP_CURRENT_VERSION_PATH = "C:\php\8.2" - $global:PVMRoot = "C:\PVM" - $global:LOG_ERROR_PATH = "TestDrive:\logs\error.log" - - # Initialize mock registry - $global:MockRegistry = @{ - Machine = @{ - "Path" = "C:\Windows\System32" - "PHP" = $null - "pvm" = $null - } - Process = @{} - User = @{} - } - - # Mock Log-Data function - Mock Log-Data { return $true } - - # Mock the System.Environment methods - function Get-EnvironmentVariableWrapper { - param($name, $target) - - if ($global:MockRegistryThrowException) { - throw $global:MockRegistryException - } - - switch ($target) { - ([System.EnvironmentVariableTarget]::Machine) { return $global:MockRegistry.Machine[$name] } - ([System.EnvironmentVariableTarget]::Process) { return $global:MockRegistry.Process[$name] } - ([System.EnvironmentVariableTarget]::User) { return $global:MockRegistry.User[$name] } - default { return $null } - } - } - function Get-EnvVar-ByName { - param ($name) - try { - if ([string]::IsNullOrWhiteSpace($name)) { - return $null - } - $name = $name.Trim() - return Get-EnvironmentVariableWrapper -name $name -target ([System.EnvironmentVariableTarget]::Machine) - } catch { - $logged = Log-Data -data @{ - header = "Get-EnvVar-ByName: Failed to get environment variable '$name'" - exception = $_ - } - return $null - } - } - function Set-EnvironmentVariableWrapper { - param($name, $value, $target) - - if ($global:MockRegistryThrowException) { - throw $global:MockRegistryException - } - - switch ($target) { - ([System.EnvironmentVariableTarget]::Machine) { - if ($value -eq $null) { - $global:MockRegistry.Machine.Remove($name) - } else { - $global:MockRegistry.Machine[$name] = $value - } - } - ([System.EnvironmentVariableTarget]::Process) { - if ($value -eq $null) { - $global:MockRegistry.Process.Remove($name) - } else { - $global:MockRegistry.Process[$name] = $value - } - } - ([System.EnvironmentVariableTarget]::User) { - if ($value -eq $null) { - $global:MockRegistry.User.Remove($name) - } else { - $global:MockRegistry.User[$name] = $value - } - } - } - } - function Set-EnvVar { - param ($name, $value) - try { - if ([string]::IsNullOrWhiteSpace($name)) { - return -1 - } - $name = $name.Trim() - Set-EnvironmentVariableWrapper -name $name -value $value -target ([System.EnvironmentVariableTarget]::Machine) - return 0 - } catch { - $logged = Log-Data -data @{ - header = "Set-EnvVar: Failed to set environment variable '$name'" - exception = $_ - } - return -1 - } - } - - } - - BeforeEach { - # Reset mock registry before each test - $global:MockRegistry = @{ - Machine = @{ - "Path" = "C:\Windows\System32" - "PHP" = $null - "pvm" = $null - } - Process = @{} - User = @{} - } - - Mock Get-EnvVar-ByName -MockWith { return $null } - Mock Set-EnvVar -MockWith { return 0 } - Mock Is-Directory-Exists -MockWith { return $false } - Mock Make-Directory -MockWith { return $true } - Mock Log-Data -MockWith { return $true } - Mock Optimize-SystemPath -MockWith {} - } - - Context "When Path environment variable is empty" { - It "Should add both PVM and PHP paths when neither exists" { - Mock Get-EnvVar-ByName -ParameterFilter { $name -eq "Path" } -MockWith { return "" } - - $result = Setup-PVM - - $result.code | Should -Be 0 - $result.message | Should -Be "PVM environment has been set up." - Should -Invoke Set-EnvVar -ParameterFilter { $name -eq "Path" -and $value -like "*C:\php\8.2;C:\PVM" } -Exactly 1 - } - } - - Context "When Path environment variable has existing entries" { - It "Should only add missing paths" { - Mock Get-EnvVar-ByName -ParameterFilter { $name -eq "Path" } -MockWith { - return "C:\Windows\System32;C:\Program Files\PowerShell" - } - - $result = Setup-PVM - - $result.code | Should -Be 0 - $result.message | Should -Be "PVM environment has been set up." - Should -Invoke Set-EnvVar -ParameterFilter { $name -eq "Path" -and $value -like "*C:\php\8.2;C:\PVM" } -Exactly 1 - } - - It "Should not add paths that already exist" { - Mock Get-EnvVar-ByName -ParameterFilter { $name -eq "Path" } -MockWith { - return "C:\Windows\System32;C:\php\8.2;C:\PVM" - } - - $result = Setup-PVM - - $result.code | Should -Be 0 - $result.message | Should -Be "PVM environment has been set up." - Should -Invoke Set-EnvVar -ParameterFilter { $name -eq "Path" } -Exactly 0 - } - - It "Should recognize existing paths in different cases" { - Mock Get-EnvVar-ByName -ParameterFilter { $name -eq "Path" } -MockWith { - return "C:\Windows\System32;C:\PHP\8.2;C:\pvm" - } - - $result = Setup-PVM - - $result.code | Should -Be 0 - $result.message | Should -Be "PVM environment has been set up." - Should -Invoke Set-EnvVar -ParameterFilter { $name -eq "Path" } -Exactly 0 - } - } - - Context "When directory creation is needed" { - It "Should create parent directory if it doesn't exist" { - Mock Get-EnvVar-ByName -MockWith { return "" } - Mock Is-Directory-Exists -ParameterFilter { $path -eq (Split-Path $PHP_CURRENT_VERSION_PATH) } -MockWith { return $false } - - $result = Setup-PVM - - $result.code | Should -Be 0 - Should -Invoke Make-Directory -Exactly 1 - } - - It "Should not create directory if it already exists" { - Mock Get-EnvVar-ByName -MockWith { return "" } - Mock Is-Directory-Exists -ParameterFilter { $path -eq (Split-Path $PHP_CURRENT_VERSION_PATH) } -MockWith { return $true } - - $result = Setup-PVM - - $result.code | Should -Be 0 - Should -Invoke Make-Directory -Exactly 0 - } - } - - Context "When errors occur" { - It "Should handle exceptions and log them" { - Mock Get-EnvVar-ByName -MockWith { throw "Test exception" } - - $result = Setup-PVM - - $result.code | Should -Be -1 - $result.message | Should -Be "Failed to set up PVM environment." - Should -Invoke Log-Data -Exactly 1 - } - } - - AfterAll { - Remove-Item function:Get-EnvVar-ByName - Remove-Item function:Set-EnvVar - Remove-Item function:Is-Directory-Exists - Remove-Item function:Make-Directory - Remove-Item function:Log-Data - Remove-Item function:Optimize-SystemPath - Remove-Item function:Setup-PVM - - Remove-Variable PHP_CURRENT_VERSION_PATH -Scope Global -ErrorAction SilentlyContinue - Remove-Variable PVMRoot -Scope Global -ErrorAction SilentlyContinue - Remove-Variable LOG_ERROR_PATH -Scope Global -ErrorAction SilentlyContinue - } +# Load required modules and functions +. "$PSScriptRoot\..\src\actions\setup.ps1" + +Describe "Setup-PVM" { + BeforeAll { + Mock Write-Host {} + # Mock global variables that the function depends on + $global:PHP_CURRENT_VERSION_PATH = "C:\php\8.2" + $global:PVMRoot = "C:\PVM" + $global:LOG_ERROR_PATH = "TestDrive:\logs\error.log" + + # Initialize mock registry + $global:MockRegistry = @{ + Machine = @{ + "Path" = "C:\Windows\System32" + "PHP" = $null + "pvm" = $null + } + Process = @{} + User = @{} + } + + # Mock Log-Data function + Mock Log-Data { return $true } + + # Mock the System.Environment methods + function Get-EnvironmentVariableWrapper { + param($name, $target) + + if ($global:MockRegistryThrowException) { + throw $global:MockRegistryException + } + + switch ($target) { + ([System.EnvironmentVariableTarget]::Machine) { return $global:MockRegistry.Machine[$name] } + ([System.EnvironmentVariableTarget]::Process) { return $global:MockRegistry.Process[$name] } + ([System.EnvironmentVariableTarget]::User) { return $global:MockRegistry.User[$name] } + default { return $null } + } + } + function Get-EnvVar-ByName { + param ($name) + try { + if ([string]::IsNullOrWhiteSpace($name)) { + return $null + } + $name = $name.Trim() + return Get-EnvironmentVariableWrapper -name $name -target ([System.EnvironmentVariableTarget]::Machine) + } catch { + $logged = Log-Data -data @{ + header = "Get-EnvVar-ByName: Failed to get environment variable '$name'" + exception = $_ + } + return $null + } + } + function Set-EnvironmentVariableWrapper { + param($name, $value, $target) + + if ($global:MockRegistryThrowException) { + throw $global:MockRegistryException + } + + switch ($target) { + ([System.EnvironmentVariableTarget]::Machine) { + if ($value -eq $null) { + $global:MockRegistry.Machine.Remove($name) + } else { + $global:MockRegistry.Machine[$name] = $value + } + } + ([System.EnvironmentVariableTarget]::Process) { + if ($value -eq $null) { + $global:MockRegistry.Process.Remove($name) + } else { + $global:MockRegistry.Process[$name] = $value + } + } + ([System.EnvironmentVariableTarget]::User) { + if ($value -eq $null) { + $global:MockRegistry.User.Remove($name) + } else { + $global:MockRegistry.User[$name] = $value + } + } + } + } + function Set-EnvVar { + param ($name, $value) + try { + if ([string]::IsNullOrWhiteSpace($name)) { + return -1 + } + $name = $name.Trim() + Set-EnvironmentVariableWrapper -name $name -value $value -target ([System.EnvironmentVariableTarget]::Machine) + return 0 + } catch { + $logged = Log-Data -data @{ + header = "Set-EnvVar: Failed to set environment variable '$name'" + exception = $_ + } + return -1 + } + } + + } + + BeforeEach { + # Reset mock registry before each test + $global:MockRegistry = @{ + Machine = @{ + "Path" = "C:\Windows\System32" + "PHP" = $null + "pvm" = $null + } + Process = @{} + User = @{} + } + + Mock Get-EnvVar-ByName -MockWith { return $null } + Mock Set-EnvVar -MockWith { return 0 } + Mock Is-Directory-Exists -MockWith { return $false } + Mock Make-Directory -MockWith { return $true } + Mock Log-Data -MockWith { return $true } + Mock Optimize-SystemPath -MockWith {} + } + + Context "When Path environment variable is empty" { + It "Should add both PVM and PHP paths when neither exists" { + Mock Get-EnvVar-ByName -ParameterFilter { $name -eq "Path" } -MockWith { return "" } + + $result = Setup-PVM + + $result.code | Should -Be 0 + $result.message | Should -Be "PVM environment has been set up." + Should -Invoke Set-EnvVar -ParameterFilter { $name -eq "Path" -and $value -like "*C:\php\8.2;C:\PVM" } -Exactly 1 + } + } + + Context "When Path environment variable has existing entries" { + It "Should only add missing paths" { + Mock Get-EnvVar-ByName -ParameterFilter { $name -eq "Path" } -MockWith { + return "C:\Windows\System32;C:\Program Files\PowerShell" + } + + $result = Setup-PVM + + $result.code | Should -Be 0 + $result.message | Should -Be "PVM environment has been set up." + Should -Invoke Set-EnvVar -ParameterFilter { $name -eq "Path" -and $value -like "*C:\php\8.2;C:\PVM" } -Exactly 1 + } + + It "Should not add paths that already exist" { + Mock Get-EnvVar-ByName -ParameterFilter { $name -eq "Path" } -MockWith { + return "C:\Windows\System32;C:\php\8.2;C:\PVM" + } + + $result = Setup-PVM + + $result.code | Should -Be 0 + $result.message | Should -Be "PVM environment has been set up." + Should -Invoke Set-EnvVar -ParameterFilter { $name -eq "Path" } -Exactly 0 + } + + It "Should recognize existing paths in different cases" { + Mock Get-EnvVar-ByName -ParameterFilter { $name -eq "Path" } -MockWith { + return "C:\Windows\System32;C:\PHP\8.2;C:\pvm" + } + + $result = Setup-PVM + + $result.code | Should -Be 0 + $result.message | Should -Be "PVM environment has been set up." + Should -Invoke Set-EnvVar -ParameterFilter { $name -eq "Path" } -Exactly 0 + } + } + + Context "When directory creation is needed" { + It "Should create parent directory if it doesn't exist" { + Mock Get-EnvVar-ByName -MockWith { return "" } + Mock Is-Directory-Exists -ParameterFilter { $path -eq (Split-Path $PHP_CURRENT_VERSION_PATH) } -MockWith { return $false } + + $result = Setup-PVM + + $result.code | Should -Be 0 + Should -Invoke Make-Directory -Exactly 1 + } + + It "Should not create directory if it already exists" { + Mock Get-EnvVar-ByName -MockWith { return "" } + Mock Is-Directory-Exists -ParameterFilter { $path -eq (Split-Path $PHP_CURRENT_VERSION_PATH) } -MockWith { return $true } + + $result = Setup-PVM + + $result.code | Should -Be 0 + Should -Invoke Make-Directory -Exactly 0 + } + } + + Context "When errors occur" { + It "Should handle exceptions and log them" { + Mock Get-EnvVar-ByName -MockWith { throw "Test exception" } + + $result = Setup-PVM + + $result.code | Should -Be -1 + $result.message | Should -Be "Failed to set up PVM environment." + Should -Invoke Log-Data -Exactly 1 + } + } + + AfterAll { + Remove-Item function:Get-EnvVar-ByName + Remove-Item function:Set-EnvVar + Remove-Item function:Is-Directory-Exists + Remove-Item function:Make-Directory + Remove-Item function:Log-Data + Remove-Item function:Optimize-SystemPath + Remove-Item function:Setup-PVM + + Remove-Variable PHP_CURRENT_VERSION_PATH -Scope Global -ErrorAction SilentlyContinue + Remove-Variable PVMRoot -Scope Global -ErrorAction SilentlyContinue + Remove-Variable LOG_ERROR_PATH -Scope Global -ErrorAction SilentlyContinue + } } \ No newline at end of file From c995458ce8a7f1403ef9a14443f7489267276268 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 13:52:30 +0100 Subject: [PATCH 38/69] Add tests for Get-PHPInstallInfo --- src/functions/helpers.ps1 | 18 +++++++- tests/helpers.tests.ps1 | 88 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index a0837ce..c3d7745 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -401,7 +401,21 @@ function Resolve-Arch { function Get-PHPInstallInfo { param ($path) - $dll = Get-ChildItem "$path\php*nts.dll","$path\php*ts.dll" -ErrorAction SilentlyContinue | Select-Object -First 1 + $tsDll = Get-ChildItem "$path\php*ts.dll" -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notmatch 'nts\.dll$' } | + Select-Object -First 1 + + if ($tsDll) { + $buildType = 'TS' + $dll = $tsDll + } + else { + $dll = Get-ChildItem "$path\php*.dll" | + Where-Object { $_.Name -notmatch 'phpdbg' } | + Select-Object -First 1 + $buildType = 'NTS' + } + if (-not $dll) { return $null @@ -410,7 +424,7 @@ function Get-PHPInstallInfo { return @{ Version = $dll.VersionInfo.ProductVersion Arch = Get-BinaryArchitecture-From-DLL -path $dll.FullName - BuildType = if ($dll.Name -match 'nts') { 'NTS' } else { 'TS' } + BuildType = $buildType Dll = $dll.Name InstallPath = $path } diff --git a/tests/helpers.tests.ps1 b/tests/helpers.tests.ps1 index 254949e..c27800a 100644 --- a/tests/helpers.tests.ps1 +++ b/tests/helpers.tests.ps1 @@ -872,3 +872,91 @@ Describe "Resolve-Arch" { } } } + +Describe "Get-PHPInstallInfo" { + Context "When PHP DLL exists" { + It "Returns PHP install info with NTS build type" { + $testPath = "TestDrive:\php\8.3" + New-Item -Path $testPath -ItemType Directory -Force | Out-Null + + # Create a mock NTS DLL file + New-Item -Path "$testPath\php8nts.dll" -ItemType File -Force | Out-Null + + Mock Get-ChildItem { + return @{ + VersionInfo = @{ ProductVersion = "8.3.0" } + Name = "php8nts.dll" + FullName = "$testPath\php8nts.dll" + } + } + + Mock Get-BinaryArchitecture-From-DLL { return "x64" } + + $result = Get-PHPInstallInfo -path $testPath + + $result | Should -Not -BeNullOrEmpty + $result.Version | Should -Be "8.3.0" + $result.Arch | Should -Be "x64" + $result.BuildType | Should -Be "NTS" + $result.Dll | Should -Be "php8nts.dll" + $result.InstallPath | Should -Be $testPath + } + + It "Returns PHP install info with TS build type" { + $testPath = "TestDrive:\php\8.2" + New-Item -Path $testPath -ItemType Directory -Force | Out-Null + + Mock Get-ChildItem { + return @{ + VersionInfo = @{ ProductVersion = "8.2.5" } + Name = "php8ts.dll" + FullName = "$testPath\php8ts.dll" + } + } + + Mock Get-BinaryArchitecture-From-DLL { return "x86" } + + $result = Get-PHPInstallInfo -path $testPath + + $result.BuildType | Should -Be "TS" + $result.Arch | Should -Be "x86" + $result.Version | Should -Be "8.2.5" + } + + It "Returns first DLL when multiple match" { + $testPath = "TestDrive:\php\8.1" + + Mock Get-ChildItem { + return @( + @{ + VersionInfo = @{ ProductVersion = "8.1.0" } + Name = "php81nts.dll" + FullName = "$testPath\php81nts.dll" + }, + @{ + VersionInfo = @{ ProductVersion = "8.1.0" } + Name = "php81ts.dll" + FullName = "$testPath\php81ts.dll" + } + ) | Select-Object -First 1 + } + + Mock Get-BinaryArchitecture-From-DLL { return "x64" } + + $result = Get-PHPInstallInfo -path $testPath + $result.Dll | Should -Be "php81nts.dll" + } + } + + Context "When PHP DLL does not exist" { + It "Returns null when no DLL found" { + $testPath = "TestDrive:\php\empty" + New-Item -Path $testPath -ItemType Directory -Force | Out-Null + + Mock Get-ChildItem { return $null } + + $result = Get-PHPInstallInfo -path $testPath + $result | Should -BeNullOrEmpty + } + } +} From d2f2bab3b4d047ef5a6fa635045440c8bfa03a71 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 13:59:10 +0100 Subject: [PATCH 39/69] Add tests for Is-Two-PHP-Versions-Equal --- tests/helpers.tests.ps1 | 168 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/tests/helpers.tests.ps1 b/tests/helpers.tests.ps1 index c27800a..5454544 100644 --- a/tests/helpers.tests.ps1 +++ b/tests/helpers.tests.ps1 @@ -960,3 +960,171 @@ Describe "Get-PHPInstallInfo" { } } } + +Describe "Is-Two-PHP-Versions-Equal" { + Context "When both versions are equal" { + It "Returns true when all properties match" { + $version1 = @{ + version = "8.3.0" + arch = "x64" + buildType = "NTS" + } + $version2 = @{ + version = "8.3.0" + arch = "x64" + buildType = "NTS" + } + + $result = Is-Two-PHP-Versions-Equal -version1 $version1 -version2 $version2 + $result | Should -Be $true + } + + It "Returns true for x86 TS build versions" { + $version1 = @{ + version = "8.1.5" + arch = "x86" + buildType = "TS" + } + $version2 = @{ + version = "8.1.5" + arch = "x86" + buildType = "TS" + } + + $result = Is-Two-PHP-Versions-Equal -version1 $version1 -version2 $version2 + $result | Should -Be $true + } + } + + Context "When versions differ" { + It "Returns false when version numbers differ" { + $version1 = @{ + version = "8.3.0" + arch = "x64" + buildType = "NTS" + } + $version2 = @{ + version = "8.2.0" + arch = "x64" + buildType = "NTS" + } + + $result = Is-Two-PHP-Versions-Equal -version1 $version1 -version2 $version2 + $result | Should -Be $false + } + + It "Returns false when architecture differs" { + $version1 = @{ + version = "8.3.0" + arch = "x64" + buildType = "NTS" + } + $version2 = @{ + version = "8.3.0" + arch = "x86" + buildType = "NTS" + } + + $result = Is-Two-PHP-Versions-Equal -version1 $version1 -version2 $version2 + $result | Should -Be $false + } + + It "Returns false when build type differs" { + $version1 = @{ + version = "8.3.0" + arch = "x64" + buildType = "NTS" + } + $version2 = @{ + version = "8.3.0" + arch = "x64" + buildType = "TS" + } + + $result = Is-Two-PHP-Versions-Equal -version1 $version1 -version2 $version2 + $result | Should -Be $false + } + } + + Context "With null or incomplete versions" { + It "Returns false when first version is null" { + $version2 = @{ + version = "8.3.0" + arch = "x64" + buildType = "NTS" + } + + $result = Is-Two-PHP-Versions-Equal -version1 $null -version2 $version2 + $result | Should -Be $false + } + + It "Returns false when second version is null" { + $version1 = @{ + version = "8.3.0" + arch = "x64" + buildType = "NTS" + } + + $result = Is-Two-PHP-Versions-Equal -version1 $version1 -version2 $null + $result | Should -Be $false + } + + It "Returns false when both versions are null" { + $result = Is-Two-PHP-Versions-Equal -version1 $null -version2 $null + $result | Should -Be $false + } + + It "Returns false when a property value is missing (null)" { + $version1 = @{ + version = "8.3.0" + arch = $null + buildType = "NTS" + } + $version2 = @{ + version = "8.3.0" + arch = "x64" + buildType = "NTS" + } + + $result = Is-Two-PHP-Versions-Equal -version1 $version1 -version2 $version2 + $result | Should -Be $false + } + } + + Context "With edge cases" { + It "Returns true for versions with additional properties" { + $version1 = @{ + version = "8.3.0" + arch = "x64" + buildType = "NTS" + Dll = "php8_nts.dll" + InstallPath = "C:\php\8.3" + } + $version2 = @{ + version = "8.3.0" + arch = "x64" + buildType = "NTS" + } + + $result = Is-Two-PHP-Versions-Equal -version1 $version1 -version2 $version2 + $result | Should -Be $true + } + + It "Returns false when version is empty string vs null" { + $version1 = @{ + version = "" + arch = "x64" + buildType = "NTS" + } + $version2 = @{ + version = "8.3.0" + arch = "x64" + buildType = "NTS" + } + + $result = Is-Two-PHP-Versions-Equal -version1 $version1 -version2 $version2 + $result | Should -Be $false + } + } +} + From 5090c3fd22410384cb0ee01b288437621410616d Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 14:24:24 +0100 Subject: [PATCH 40/69] Add tests for Get-BinaryArchitecture-From-DLL --- tests/helpers.tests.ps1 | 104 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/tests/helpers.tests.ps1 b/tests/helpers.tests.ps1 index 5454544..8ad71c9 100644 --- a/tests/helpers.tests.ps1 +++ b/tests/helpers.tests.ps1 @@ -1128,3 +1128,107 @@ Describe "Is-Two-PHP-Versions-Equal" { } } + +Describe "Get-BinaryArchitecture-From-DLL" { + Context "Reading PE format from binary files" { + It "Returns x64 architecture when machine type is 0x8664" { + $dllPath = "TestDrive:\php\php8_x64.dll" + New-Item -Path $dllPath -ItemType File -Force | Out-Null + + # Convert TestDrive path to actual filesystem path + $actualPath = (Resolve-Path -Path $dllPath).ProviderPath + + # Create a minimal PE file structure for x64 + # PE Header starts at offset 0x3C + $bytes = [byte[]]::new(1024) + + # Write MZ header + $bytes[0] = 0x4D # 'M' + $bytes[1] = 0x5A # 'Z' + + # PE offset is at 0x3C (60 decimal) + $peOffset = 0x80 + [BitConverter]::GetBytes($peOffset) | ` + ForEach-Object -Begin { $i = 0 } -Process { $bytes[0x3C + $i] = $_; $i++ } + + # At PE offset, write "PE\0\0" + $bytes[$peOffset] = 0x50 # 'P' + $bytes[$peOffset + 1] = 0x45 # 'E' + + # Machine type at PE offset + 4 (0x8664 for x64) + [BitConverter]::GetBytes([uint16]0x8664) | ` + ForEach-Object -Begin { $i = 0 } -Process { $bytes[$peOffset + 4 + $i] = $_; $i++ } + + [System.IO.File]::WriteAllBytes($actualPath, $bytes) + + $result = Get-BinaryArchitecture-From-DLL -path $actualPath + $result | Should -Be "x64" + } + + It "Returns x86 architecture when machine type is 0x014c" { + $dllPath = "TestDrive:\php\php8_x86.dll" + New-Item -Path $dllPath -ItemType File -Force | Out-Null + + # Convert TestDrive path to actual filesystem path + $actualPath = (Resolve-Path -Path $dllPath).ProviderPath + + # Create a minimal PE file structure for x86 + $bytes = [byte[]]::new(1024) + + # Write MZ header + $bytes[0] = 0x4D # 'M' + $bytes[1] = 0x5A # 'Z' + + # PE offset is at 0x3C (60 decimal) + $peOffset = 0x80 + [BitConverter]::GetBytes($peOffset) | ` + ForEach-Object -Begin { $i = 0 } -Process { $bytes[0x3C + $i] = $_; $i++ } + + # At PE offset, write "PE\0\0" + $bytes[$peOffset] = 0x50 # 'P' + $bytes[$peOffset + 1] = 0x45 # 'E' + + # Machine type at PE offset + 4 (0x014c for x86) + [BitConverter]::GetBytes([uint16]0x014c) | ` + ForEach-Object -Begin { $i = 0 } -Process { $bytes[$peOffset + 4 + $i] = $_; $i++ } + + [System.IO.File]::WriteAllBytes($actualPath, $bytes) + + $result = Get-BinaryArchitecture-From-DLL -path $actualPath + $result | Should -Be "x86" + } + + It "Returns Unknown for unknown machine type" { + $dllPath = "TestDrive:\php\php8_unknown.dll" + New-Item -Path $dllPath -ItemType File -Force | Out-Null + + # Convert TestDrive path to actual filesystem path + $actualPath = (Resolve-Path -Path $dllPath).ProviderPath + + # Create a minimal PE file structure with unknown type + $bytes = [byte[]]::new(1024) + + # Write MZ header + $bytes[0] = 0x4D # 'M' + $bytes[1] = 0x5A # 'Z' + + # PE offset is at 0x3C (60 decimal) + $peOffset = 0x80 + [BitConverter]::GetBytes($peOffset) | ` + ForEach-Object -Begin { $i = 0 } -Process { $bytes[0x3C + $i] = $_; $i++ } + + # At PE offset, write "PE\0\0" + $bytes[$peOffset] = 0x50 # 'P' + $bytes[$peOffset + 1] = 0x45 # 'E' + + # Machine type at PE offset + 4 (0x0000 for unknown) + [BitConverter]::GetBytes([uint16]0x0000) | ` + ForEach-Object -Begin { $i = 0 } -Process { $bytes[$peOffset + 4 + $i] = $_; $i++ } + + [System.IO.File]::WriteAllBytes($actualPath, $bytes) + + $result = Get-BinaryArchitecture-From-DLL -path $actualPath + $result | Should -Be "Unknown" + } + } +} From a07291f176b8bfd4f70a4d6b8336b22163aeb1eb Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 15:10:31 +0100 Subject: [PATCH 41/69] Fix ini.tests.ps1 --- src/actions/ini.ps1 | 1 + tests/ini.tests.ps1 | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/actions/ini.ps1 b/src/actions/ini.ps1 index f108435..b6731e4 100644 --- a/src/actions/ini.ps1 +++ b/src/actions/ini.ps1 @@ -1150,6 +1150,7 @@ function List-PHP-Extensions { $availableExtensions = Get-Data-From-Cache -cacheFileName "available_extensions" if ($availableExtensions.Count -eq 0) { $availableExtensions = Get-PHPExtensions-From-Source + $availableExtensions = [pscustomobject] $availableExtensions } } else { $availableExtensions = Get-PHPExtensions-From-Source diff --git a/tests/ini.tests.ps1 b/tests/ini.tests.ps1 index f3c95e1..85f9221 100644 --- a/tests/ini.tests.ps1 +++ b/tests/ini.tests.ps1 @@ -1213,8 +1213,7 @@ Describe "List-PHP-Extensions" { } It "Handles thrown exception" { - Mock Test-Path { return $true } - Mock New-TimeSpan { throw "Access denied" } + Mock Can-Use-Cache { throw 'Error' } $code = List-PHP-Extensions -iniPath $testIniPath -available $true $code | Should -Be -1 } From ac0aa818435a0b70559498d3d8e2e5e3015ecc1d Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 15:10:58 +0100 Subject: [PATCH 42/69] Fix common.tests.ps1 --- tests/common.tests.ps1 | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/common.tests.ps1 b/tests/common.tests.ps1 index 3f5c4bc..c330d64 100644 --- a/tests/common.tests.ps1 +++ b/tests/common.tests.ps1 @@ -113,13 +113,8 @@ Describe "Get-Installed-PHP-Versions" { } It "Should return empty array when no PHP versions are found" { - Mock Get-All-EnvVars { - return @{ - "PATH" = "C:\Windows" - "OTHER_VAR" = "some value" - } - } - Mock Log-Data { return $true } + Mock Can-Use-Cache { return $false } + Mock Get-Installed-PHP-Versions-From-Directory { return @() } $result = Get-Installed-PHP-Versions $result.Count | Should -Be 0 @@ -143,8 +138,8 @@ Describe "Get-Installed-PHP-Versions" { } Context "When exceptions occur" { - It "Should return empty array and log error when Get-All-EnvVars throws exception" { - Mock Get-All-Subdirectories { throw "Test exception" } + It "Should return empty array and log error when Get-Installed-PHP-Versions-From-Directory throws exception" { + Mock Get-Installed-PHP-Versions-From-Directory { throw "Test exception" } Mock Log-Data { return $true } $result = Get-Installed-PHP-Versions From d33259b053548bae8c131452b99259d46a0d962d Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 15:38:16 +0100 Subject: [PATCH 43/69] Convert fetched versions to pscustomobject --- src/actions/list.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/actions/list.ps1 b/src/actions/list.ps1 index 23afa7d..da8eca2 100644 --- a/src/actions/list.ps1 +++ b/src/actions/list.ps1 @@ -68,9 +68,11 @@ function Get-PHP-List-To-Install { $fetchedVersionsGrouped = Get-Data-From-Cache -cacheFileName "available_php_versions" if (-not $fetchedVersionsGrouped -or $fetchedVersionsGrouped.Count -eq 0) { $fetchedVersionsGrouped = Get-From-Source -arch $arch + $fetchedVersionsGrouped = [pscustomobject] $fetchedVersionsGrouped } } else { $fetchedVersionsGrouped = Get-From-Source -arch $arch + $fetchedVersionsGrouped = [pscustomobject] $fetchedVersionsGrouped } return $fetchedVersionsGrouped @@ -97,7 +99,7 @@ function Get-Available-PHP-Versions { } $fetchedVersionsGroupedPartialList = @{} - $fetchedVersionsGrouped.GetEnumerator() | ForEach-Object { + $fetchedVersionsGrouped.PSObject.Properties | ForEach-Object { $searchResult = $_.Value if ($term) { $searchResult = $searchResult | Where-Object { From 923a8884bce10b14bdc9add38ac8f72aa8cb0468 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 15:38:50 +0100 Subject: [PATCH 44/69] Fix list.tests.ps1 --- tests/list.tests.ps1 | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tests/list.tests.ps1 b/tests/list.tests.ps1 index d3a69d4..be02237 100644 --- a/tests/list.tests.ps1 +++ b/tests/list.tests.ps1 @@ -143,9 +143,7 @@ Describe "Get-PHP-List-To-Install" { } It "Should fetch from source when cache is empty" { - Mock Test-Path { $true } - $timeWithinLastWeek = (Get-Date).AddHours(-160).ToString("yyyy-MM-ddTHH:mm:ss.fffffffK") - Mock Get-Item { @{ LastWriteTime = $timeWithinLastWeek } } + Mock Can-Use-Cache { return $true } Mock Get-Data-From-Cache { return @{} } Mock Get-From-Source { return @{ @@ -157,14 +155,14 @@ Describe "Get-PHP-List-To-Install" { $result = Get-PHP-List-To-Install $result | Should -Not -BeNullOrEmpty - $result.Keys | Should -Contain 'Archives' - $result.Keys | Should -Contain 'Releases' + $result.Archives | Should -Not -BeNullOrEmpty + $result.Releases | Should -Not -BeNullOrEmpty Assert-MockCalled Get-Data-From-Cache -Exactly 1 Assert-MockCalled Get-From-Source -Exactly 1 } It "Should fetch from source" { - Mock Test-Path { return $false } + Mock Can-Use-Cache { return $false } Mock Get-From-Source { return @{ 'Archives' = @('php-8.1.0-Win32-x64.zip') @@ -175,13 +173,13 @@ Describe "Get-PHP-List-To-Install" { $result = Get-PHP-List-To-Install $result | Should -Not -BeNullOrEmpty - $result.Keys | Should -Contain 'Archives' - $result.Keys | Should -Contain 'Releases' + $result.Archives | Should -Not -BeNullOrEmpty + $result.Releases | Should -Not -BeNullOrEmpty Assert-MockCalled Get-From-Source -Exactly 1 } It "Handles exceptions gracefully" { - Mock Test-Path { throw "Cache error" } + Mock Can-Use-Cache { throw "Cache error" } $result = Get-PHP-List-To-Install $result | Should -BeNullOrEmpty } @@ -210,6 +208,20 @@ Describe "Get-Available-PHP-Versions" { } It "Display available versions matching filter" { + Mock Get-PHP-List-To-Install { return [pscustomobject]@{ + 'Archives' = @(@{ + Link = "php-7.1.0-Win32-x64.zip" + BuildType = "TS" + Arch = "x64" + Version = "7.1.0" + }) + 'Releases' = @(@{ + Link = "php-7.2.0-Win32-x64.zip" + BuildType = "TS" + Arch = "x64" + Version = "7.2.0" + }) + }} $code = Get-Available-PHP-Versions -term "7.1" $code | Should -Be 0 } @@ -226,8 +238,7 @@ Describe "Get-Available-PHP-Versions" { } It "Should fetch from source when cache is empty" { - Mock Test-Path { $true } - Mock Get-Item { @{ LastWriteTime = (Get-Date) } } + Mock Can-Use-Cache { return $true } Mock Get-Data-From-Cache { return @{} } Mock Get-From-Source { return @{ From 6828a2b4c311a61e278e11da3c7e4a083df28958 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 15:53:22 +0100 Subject: [PATCH 45/69] Add tests for Refresh-Installed-PHP-Versions-Cache --- tests/common.tests.ps1 | 92 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/common.tests.ps1 b/tests/common.tests.ps1 index c330d64..1ba96da 100644 --- a/tests/common.tests.ps1 +++ b/tests/common.tests.ps1 @@ -320,3 +320,95 @@ Describe "Is-PHP-Version-Installed" { } } } + +Describe "Refresh-Installed-PHP-Versions-Cache" { + Context "When cache is successfully refreshed" { + It "Should return 0 on success" { + Mock Get-Installed-PHP-Versions-From-Directory { + return @( + @{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.2"; Arch = "x64"; BuildType = 'NTS'} + ) + } + Mock Cache-Data { return 0 } + + $result = Refresh-Installed-PHP-Versions-Cache + $result | Should -Be 0 + } + + It "Should call Get-Installed-PHP-Versions-From-Directory" { + Mock Get-Installed-PHP-Versions-From-Directory { + return @( + @{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'} + ) + } + Mock Cache-Data { return 0 } + + $result = Refresh-Installed-PHP-Versions-Cache + + Assert-MockCalled Get-Installed-PHP-Versions-From-Directory -Exactly 1 + } + + It "Should call Cache-Data with installed_php_versions file and depth 1" { + Mock Get-Installed-PHP-Versions-From-Directory { + return @( + @{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'} + ) + } + Mock Cache-Data { return 0 } + + $result = Refresh-Installed-PHP-Versions-Cache + + Assert-MockCalled Cache-Data -Exactly 1 -ParameterFilter { + $cacheFileName -eq "installed_php_versions" -and $depth -eq 1 + } + } + + It "Should cache the results from Get-Installed-PHP-Versions-From-Directory" { + $mockVersions = @( + @{Version = "7.4"; Arch = "x64"; BuildType = 'NTS'} + @{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'} + ) + Mock Get-Installed-PHP-Versions-From-Directory { return $mockVersions } + Mock Cache-Data { return 0 } + + $result = Refresh-Installed-PHP-Versions-Cache + + Assert-MockCalled Cache-Data -Exactly 1 -ParameterFilter { + $data.Count -eq 2 -and $data[0].Version -eq "7.4" + } + } + } + + Context "When exceptions occur" { + It "Should return -1 on exception" { + Mock Get-Installed-PHP-Versions-From-Directory { throw "Test exception" } + Mock Log-Data { return $true } + + $result = Refresh-Installed-PHP-Versions-Cache + $result | Should -Be -1 + } + + It "Should log error when exception occurs" { + Mock Get-Installed-PHP-Versions-From-Directory { throw "Test exception" } + Mock Log-Data { return $true } + + $result = Refresh-Installed-PHP-Versions-Cache + + Assert-MockCalled Log-Data -Exactly 1 -ParameterFilter { + $data.header -eq "Refresh-Installed-PHP-Versions-Cache - Failed to refresh installed PHP versions cache" + } + } + + It "Should return -1 when Cache-Data throws exception" { + Mock Get-Installed-PHP-Versions-From-Directory { + return @(@{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'}) + } + Mock Cache-Data { throw "Cache exception" } + Mock Log-Data { return $true } + + $result = Refresh-Installed-PHP-Versions-Cache + $result | Should -Be -1 + } + } +} From 7bf098415f1f5258e22ea163d2c157156796b637 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 16:07:22 +0100 Subject: [PATCH 46/69] Add tests for Get-Installed-PHP-Versions-From-Directory --- tests/common.tests.ps1 | 115 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/common.tests.ps1 b/tests/common.tests.ps1 index 1ba96da..8b974c3 100644 --- a/tests/common.tests.ps1 +++ b/tests/common.tests.ps1 @@ -412,3 +412,118 @@ Describe "Refresh-Installed-PHP-Versions-Cache" { } } } + +Describe "Get-Installed-PHP-Versions-From-Directory" { + BeforeAll { + $script:STORAGE_PATH = "C:\test\storage" + } + + Context "When PHP versions exist" { + It "Should return installed PHP versions with php.exe present" { + Mock Get-All-Subdirectories { + return @( + @{FullName = "C:\test\storage\php\8.1"} + @{FullName = "C:\test\storage\php\8.2"} + ) + } + Mock Test-Path { return $true } + Mock Get-PHPInstallInfo { + param($path) + if ($path -eq "C:\test\storage\php\8.1") { + return @{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'; InstallPath = "C:\test\storage\php\8.1"} + } else { + return @{Version = "8.2"; Arch = "x64"; BuildType = 'NTS'; InstallPath = "C:\test\storage\php\8.2"} + } + } + + $result = Get-Installed-PHP-Versions-From-Directory + $result.Count | Should -Be 2 + } + + It "Should skip directories without php.exe" { + Mock Get-All-Subdirectories { + return @( + @{FullName = "C:\test\storage\php\8.1"} + @{FullName = "C:\test\storage\php\invalid"} + @{FullName = "C:\test\storage\php\8.2"} + ) + } + Mock Test-Path { + param($path) + return $path -notmatch "invalid" + } + Mock Get-PHPInstallInfo { + param($path) + if ($path -eq "C:\test\storage\php\8.1") { + return @{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'} + } elseif ($path -eq "C:\test\storage\php\8.2") { + return @{Version = "8.2"; Arch = "x64"; BuildType = 'NTS'} + } + } + + $result = Get-Installed-PHP-Versions-From-Directory + $result.Count | Should -Be 2 + } + + It "Should return versions sorted by version number" { + Mock Get-All-Subdirectories { + return @( + @{FullName = "C:\test\storage\php\8.2"} + @{FullName = "C:\test\storage\php\7.4"} + @{FullName = "C:\test\storage\php\8.1"} + ) + } + Mock Test-Path { return $true } + Mock Get-PHPInstallInfo { + param($path) + if ($path -eq "C:\test\storage\php\8.2") { + return @{Version = "8.2"; Arch = "x64"; BuildType = 'NTS'} + } elseif ($path -eq "C:\test\storage\php\7.4") { + return @{Version = "7.4"; Arch = "x86"; BuildType = 'TS'} + } else { + return @{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'} + } + } + + $result = Get-Installed-PHP-Versions-From-Directory + $result.Count | Should -Be 3 + $result[0].Version | Should -Be "7.4" + $result[1].Version | Should -Be "8.1" + $result[2].Version | Should -Be "8.2" + } + } + + Context "When no PHP versions exist" { + It "Should return empty array when no directories exist" { + Mock Get-All-Subdirectories { return @() } + + $result = Get-Installed-PHP-Versions-From-Directory + $result.Count | Should -Be 0 + } + + It "Should return empty array when no php.exe files are present" { + Mock Get-All-Subdirectories { + return @( + @{FullName = "C:\test\storage\php\invalid1"} + @{FullName = "C:\test\storage\php\invalid2"} + ) + } + Mock Test-Path { return $false } + + $result = Get-Installed-PHP-Versions-From-Directory + $result.Count | Should -Be 0 + } + } + + Context "When calling Get-All-Subdirectories" { + It "Should call Get-All-Subdirectories with php storage path" { + Mock Get-All-Subdirectories { return @() } + + Get-Installed-PHP-Versions-From-Directory + + Assert-MockCalled Get-All-Subdirectories -Exactly 1 -ParameterFilter { + $path -eq "$STORAGE_PATH\php" + } + } + } +} From 5cd3cc214a7667771534aa73aeace64ac8cc33f1 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 16:39:55 +0100 Subject: [PATCH 47/69] Fix router.tests.ps1 --- tests/router.tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/router.tests.ps1 b/tests/router.tests.ps1 index ade8888..4559ce9 100644 --- a/tests/router.tests.ps1 +++ b/tests/router.tests.ps1 @@ -165,6 +165,7 @@ Describe "Invoke-PVMInstall Tests" { It "Should install detected PHP version from the project" { $arguments = @("auto") + Mock Get-Matching-PHP-Versions { return @() } Mock Detect-PHP-VersionFromProject { return "8.1" } $result = Invoke-PVMInstall -arguments $arguments $result | Should -Be 0 From d673f91897dfef05961da0397683751af46803cb Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 16:53:06 +0100 Subject: [PATCH 48/69] Add tests for Invoke-PVMProfile --- tests/router.tests.ps1 | 267 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) diff --git a/tests/router.tests.ps1 b/tests/router.tests.ps1 index 4559ce9..e131bc6 100644 --- a/tests/router.tests.ps1 +++ b/tests/router.tests.ps1 @@ -400,6 +400,273 @@ Describe "Invoke-PVMTest Tests" { } } +Describe "Invoke-PVMProfile Tests" { + BeforeEach { + Mock Write-Host { } + Mock Save-PHP-Profile { 0 } + Mock Load-PHP-Profile { 0 } + Mock List-PHP-Profiles { 0 } + Mock Show-PHP-Profile { 0 } + Mock Delete-PHP-Profile { 0 } + Mock Export-PHP-Profile { 0 } + Mock Import-PHP-Profile { 0 } + } + + Context "No action provided" { + It "Should return -1 when no action is provided" { + $arguments = @() + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be -1 + + Assert-MockCalled Write-Host -ParameterFilter { + $Object -like "*Please specify an action for 'pvm profile'*" -and + $ForegroundColor -eq "Yellow" + } + } + } + + Context "Save action" { + It "Should return -1 when save action has no profile name" { + $arguments = @("save") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be -1 + + Assert-MockCalled Write-Host -ParameterFilter { + $Object -like "*Please provide a profile name: pvm profile save*" + } + } + + It "Should save profile with name only" { + $arguments = @("save", "myprofile") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Save-PHP-Profile -Times 1 -ParameterFilter { + $profileName -eq "myprofile" -and $description -eq $null + } + } + + It "Should save profile with name and description" { + $arguments = @("save", "myprofile", "This", "is", "my", "description") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Save-PHP-Profile -Times 1 -ParameterFilter { + $profileName -eq "myprofile" -and + $description -eq "This is my description" + } + } + } + + Context "Load action" { + It "Should return -1 when load action has no profile name" { + $arguments = @("load") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be -1 + + Assert-MockCalled Write-Host -ParameterFilter { + $Object -like "*Please provide a profile name: pvm profile load*" + } + } + + It "Should load profile with provided name" { + $arguments = @("load", "myprofile") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Load-PHP-Profile -Times 1 -ParameterFilter { + $profileName -eq "myprofile" + } + } + } + + Context "List action" { + It "Should list profiles without additional arguments" { + $arguments = @("list") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled List-PHP-Profiles -Times 1 + } + } + + Context "Show action" { + It "Should return -1 when show action has no profile name" { + $arguments = @("show") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be -1 + + Assert-MockCalled Write-Host -ParameterFilter { + $Object -like "*Please provide a profile name: pvm profile show*" + } + } + + It "Should show profile with provided name" { + $arguments = @("show", "myprofile") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Show-PHP-Profile -Times 1 -ParameterFilter { + $profileName -eq "myprofile" + } + } + } + + Context "Delete action" { + It "Should return -1 when delete action has no profile name" { + $arguments = @("delete") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be -1 + + Assert-MockCalled Write-Host -ParameterFilter { + $Object -like "*Please provide a profile name: pvm profile delete*" + } + } + + It "Should delete profile with provided name" { + $arguments = @("delete", "myprofile") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Delete-PHP-Profile -Times 1 -ParameterFilter { + $profileName -eq "myprofile" + } + } + } + + Context "Export action" { + It "Should return -1 when export action has no profile name" { + $arguments = @("export") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be -1 + + Assert-MockCalled Write-Host -ParameterFilter { + $Object -like "*Please provide a profile name: pvm profile export*" + } + } + + It "Should export profile with name only" { + $arguments = @("export", "myprofile") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Export-PHP-Profile -Times 1 -ParameterFilter { + $profileName -eq "myprofile" -and $exportPath -eq $null + } + } + + It "Should export profile with name and path" { + $arguments = @("export", "myprofile", "C:\exports\profile.json") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Export-PHP-Profile -Times 1 -ParameterFilter { + $profileName -eq "myprofile" -and + $exportPath -eq "C:\exports\profile.json" + } + } + } + + Context "Import action" { + It "Should return -1 when import action has no file path" { + $arguments = @("import") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be -1 + + Assert-MockCalled Write-Host -ParameterFilter { + $Object -like "*Please provide a file path: pvm profile import*" + } + } + + It "Should import profile from file path only" { + $arguments = @("import", "C:\profiles\export.json") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Import-PHP-Profile -Times 1 -ParameterFilter { + $importPath -eq "C:\profiles\export.json" -and $profileName -eq $null + } + } + + It "Should import profile from file path with custom name" { + $arguments = @("import", "C:\profiles\export.json", "myimported") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Import-PHP-Profile -Times 1 -ParameterFilter { + $importPath -eq "C:\profiles\export.json" -and + $profileName -eq "myimported" + } + } + } + + Context "Unknown action" { + It "Should return -1 for unknown action" { + $arguments = @("unknown") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be -1 + + Assert-MockCalled Write-Host -ParameterFilter { + $Object -like "*Unknown action 'unknown'*" -and + $ForegroundColor -eq "Yellow" + } + } + + It "Should handle case-insensitive action names" { + $arguments = @("SAVE", "testprofile") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Save-PHP-Profile -Times 1 + } + } + + Context "Action success and failure returns" { + It "Should return 0 when Save-PHP-Profile succeeds" { + Mock Save-PHP-Profile { return 0 } + $arguments = @("save", "test") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + } + + It "Should return -1 when Load-PHP-Profile fails" { + Mock Load-PHP-Profile { return -1 } + $arguments = @("load", "test") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be -1 + } + + It "Should return action result code from any profile action" { + Mock Delete-PHP-Profile { return 5 } + $arguments = @("delete", "test") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 5 + } + } +} + Describe "Get-Actions Tests" { BeforeEach { From 6299ebac67d8537b5bc0c00bc0f58f0c3c304bbd Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 18:18:23 +0100 Subject: [PATCH 49/69] Filter by arch after fetching from source/cache --- src/actions/list.ps1 | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/actions/list.ps1 b/src/actions/list.ps1 index da8eca2..d03d41f 100644 --- a/src/actions/list.ps1 +++ b/src/actions/list.ps1 @@ -1,6 +1,5 @@ function Get-From-Source { - param ($arch = $null) try { $urls = Get-Source-Urls @@ -31,10 +30,6 @@ function Get-From-Source { $fetchedVersions = $fetchedVersions + $filteredLinks # ($filteredLinks | ForEach-Object { $_.href }) } - if ($null -ne $arch) { - $fetchedVersions = $fetchedVersions | Where-Object { $_.Arch -match $arch } - } - $fetchedVersionsGrouped = [ordered]@{ 'Archives' = $fetchedVersions | Where-Object { $_.Link -match "archives" } 'Releases' = $fetchedVersions | Where-Object { $_.Link -notmatch "archives" } @@ -59,7 +54,6 @@ function Get-From-Source { } function Get-PHP-List-To-Install { - param ($arch = $null) try { $fetchedVersionsGrouped = @{} $useCache = Can-Use-Cache -cacheFileName 'available_php_versions' @@ -67,11 +61,11 @@ function Get-PHP-List-To-Install { if ($useCache) { $fetchedVersionsGrouped = Get-Data-From-Cache -cacheFileName "available_php_versions" if (-not $fetchedVersionsGrouped -or $fetchedVersionsGrouped.Count -eq 0) { - $fetchedVersionsGrouped = Get-From-Source -arch $arch + $fetchedVersionsGrouped = Get-From-Source $fetchedVersionsGrouped = [pscustomobject] $fetchedVersionsGrouped } } else { - $fetchedVersionsGrouped = Get-From-Source -arch $arch + $fetchedVersionsGrouped = Get-From-Source $fetchedVersionsGrouped = [pscustomobject] $fetchedVersionsGrouped } @@ -91,7 +85,7 @@ function Get-Available-PHP-Versions { try { Write-Host "`nLoading available PHP versions..." - $fetchedVersionsGrouped = Get-PHP-List-To-Install -arch $arch + $fetchedVersionsGrouped = Get-PHP-List-To-Install if ($fetchedVersionsGrouped.Count -eq 0) { Write-Host "`nNo PHP versions found in the source. Please check your internet connection or the source URLs." @@ -101,10 +95,11 @@ function Get-Available-PHP-Versions { $fetchedVersionsGroupedPartialList = @{} $fetchedVersionsGrouped.PSObject.Properties | ForEach-Object { $searchResult = $_.Value + if ($null -ne $arch) { + $searchResult = $searchResult | Where-Object { $_.Arch -match $arch } + } if ($term) { - $searchResult = $searchResult | Where-Object { - $_.Version -like "$term*" - } + $searchResult = $searchResult | Where-Object { $_.Version -like "$term*" } } if ($searchResult.Count -ne 0) { $fetchedVersionsGroupedPartialList[$_.Name] = $searchResult | Select-Object -Last $LATEST_VERSION_COUNT @@ -127,12 +122,8 @@ function Get-Available-PHP-Versions { } Write-Host "`n$key`n" $fetchedVersionsGroupe | ForEach-Object { - $version = $_.Version - $arch = $_.Arch - $buildType = $_.BuildType - $versionNumber = "$version ".PadRight(15, '.') - - Write-Host " $versionNumber $arch $buildType" + $versionNumber = "$($_.Version) ".PadRight(15, '.') + Write-Host " $versionNumber $($_.Arch) $($_.BuildType)" } } From b3d41aeb8945eeb6b0f3a1d3bdb397feb3bedbf0 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 18:20:24 +0100 Subject: [PATCH 50/69] Add tests for handeling both arch --- tests/list.tests.ps1 | 55 +++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/tests/list.tests.ps1 b/tests/list.tests.ps1 index be02237..ae3f14d 100644 --- a/tests/list.tests.ps1 +++ b/tests/list.tests.ps1 @@ -77,27 +77,6 @@ Describe "Get-From-Source" { $allVersions | Should -Not -Contain 'php-8.2.0-Win32-x86.zip' } - It "Should handle x86 architecture" { - Mock Is-OS-64Bit { return $false } - - $mockLinks = @( - @{ href = 'php-8.2.0-Win32-x86.zip' }, - @{ href = 'php-8.2.0-Win32-x64.zip' } - ) - - Mock Invoke-WebRequest { - return @{ Links = $mockLinks } - } - - Mock Cache-Data { } - - $result = Get-From-Source -arch x86 - - $allVersions = $result['Archives'] + $result['Releases'] - $allVersions | Where-Object { $_.link -eq 'php-8.2.0-Win32-x86.zip' } | Should -Not -BeNullOrEmpty - $allVersions | Where-Object { $_.link -eq 'php-8.2.0-Win32-x64.zip' } | Should -BeNullOrEmpty - } - It "Should return empty list" { Mock Invoke-WebRequest { return @{ Links = @() } @@ -190,6 +169,40 @@ Describe "Get-Available-PHP-Versions" { Mock Write-Host { } } + It "Should handle x86 architecture" { + Mock Get-PHP-List-To-Install { return [pscustomobject]@{ + 'Archives' = @(@{ + Link = "php-7.1.0-Win32-x64.zip" + BuildType = "TS"; Arch = "x64"; Version = "7.1.0"; + }) + 'Releases' = @(@{ + Link = "php-7.1.0-Win32-x86.zip" + BuildType = "TS"; Arch = "x86"; Version = "7.1.0" + }) + }} + + $code = Get-Available-PHP-Versions -arch "x86" + + $code | Should -Be 0 + } + + It "Should handle x64 architecture" -tag i { + Mock Get-PHP-List-To-Install { return [pscustomobject]@{ + 'Archives' = @(@{ + Link = "php-7.1.0-Win32-x64.zip" + BuildType = "TS"; Arch = "x64"; Version = "7.1.0"; + }) + 'Releases' = @(@{ + Link = "php-7.1.0-Win32-x86.zip" + BuildType = "TS"; Arch = "x86"; Version = "7.1.0" + }) + }} + + $code = Get-Available-PHP-Versions -arch "x64" + + $code | Should -Be 0 + } + It "Should read from cache by default" { Mock Get-Data-From-Cache { return @{ From 796b28738a430143e5b7a996174a16be39095e45 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 19:10:46 +0100 Subject: [PATCH 51/69] Fix dynamic property assignment --- src/actions/common.ps1 | 2 +- src/actions/install.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/actions/common.ps1 b/src/actions/common.ps1 index ca1fac1..467059e 100644 --- a/src/actions/common.ps1 +++ b/src/actions/common.ps1 @@ -113,7 +113,7 @@ function Get-UserSelected-PHP-Version { $index = 0 Write-Host "`nInstalled versions :" $installedVersions | ForEach-Object { - $_.index = $index + $_ | Add-Member -NotePropertyName "index" -NotePropertyValue $index -Force $isCurrent = "" if (Is-Two-PHP-Versions-Equal -version1 $currentVersion -version2 $_) { $isCurrent = "(Current)" diff --git a/src/actions/install.ps1 b/src/actions/install.ps1 index f55815e..5d12348 100644 --- a/src/actions/install.ps1 +++ b/src/actions/install.ps1 @@ -260,7 +260,7 @@ function Select-Version { } Write-Host "`n$key versions:`n" $versionsList | ForEach-Object { - $_.index = $index + $_ | Add-Member -NotePropertyName "index" -NotePropertyValue $index -Force Write-Host " [$index] $($_.version) $($_.arch) $($_.BuildType)" $index++ } From 7d7d3954a06f3c7a4791c9c2b1a64737f23d945f Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 20:18:42 +0100 Subject: [PATCH 52/69] Handle existence of previous xdebug line --- src/actions/ini.ps1 | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/actions/ini.ps1 b/src/actions/ini.ps1 index b6731e4..3f92a37 100644 --- a/src/actions/ini.ps1 +++ b/src/actions/ini.ps1 @@ -861,8 +861,21 @@ function Install-XDebug-Extension { if ($chosenItem.xDebugVersion -like "3.*") { $xDebugConfig = getXdebugConfigV3 -XDebugPath $($chosenItem.fileName) } - $xDebugConfig = $xDebugConfig -replace "\ +" - Add-Content -Path $iniPath -Value $xDebugConfig + # check existence of previous xdebug + $iniContent = Get-Content $iniPath + $dllXDebugExists = $false + for ($i = 0; $i -lt $iniContent.Count; $i++) { + if ($iniContent[$i] -match '^(?;)?\s*zend_extension\s*=.*xdebug.*$') { + $iniContent[$i] = $iniContent[$i] -replace '^(?;)?(\s*zend_extension\s*=).*$', "zend_extension='$($chosenItem.fileName)'" + $dllXDebugExists = $true + } + } + if ($dllXDebugExists) { + Set-Content -Path $iniPath -Value $iniContent -Encoding UTF8 + } else { + $xDebugConfig = $xDebugConfig -replace "\ +" + Add-Content -Path $iniPath -Value $xDebugConfig + } return 0 } catch { From 27aedce065827e37441b185ddefefc1663b2e462 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sat, 7 Feb 2026 22:00:22 +0100 Subject: [PATCH 53/69] Use TestDrive for current php path --- tests/current.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/current.tests.ps1 b/tests/current.tests.ps1 index 099ec48..65d75be 100644 --- a/tests/current.tests.ps1 +++ b/tests/current.tests.ps1 @@ -3,7 +3,7 @@ BeforeAll { # Mock dependencies $global:LOG_ERROR_PATH = "TestDrive:\logs\error.log" - $global:PHP_CURRENT_VERSION_PATH = "C:\php\current" + $global:PHP_CURRENT_VERSION_PATH = "TestDrive:\php\current" # Mock Log-Data function Mock Write-Host {} From a775c5f9c6ac294aa557a0a4e99d2f84ae631aea Mon Sep 17 00:00:00 2001 From: Driss B Date: Sun, 8 Feb 2026 19:38:15 +0100 Subject: [PATCH 54/69] Remove test tag --- tests/list.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/list.tests.ps1 b/tests/list.tests.ps1 index ae3f14d..f0871f8 100644 --- a/tests/list.tests.ps1 +++ b/tests/list.tests.ps1 @@ -186,7 +186,7 @@ Describe "Get-Available-PHP-Versions" { $code | Should -Be 0 } - It "Should handle x64 architecture" -tag i { + It "Should handle x64 architecture" { Mock Get-PHP-List-To-Install { return [pscustomobject]@{ 'Archives' = @(@{ Link = "php-7.1.0-Win32-x64.zip" From b1a41a7e854251072e52febcceabd5f7029fd115 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sun, 8 Feb 2026 19:38:48 +0100 Subject: [PATCH 55/69] Format date before printing --- src/actions/profile.ps1 | 1264 ++++++++++++++++++++------------------- 1 file changed, 634 insertions(+), 630 deletions(-) diff --git a/src/actions/profile.ps1 b/src/actions/profile.ps1 index e681827..4344161 100644 --- a/src/actions/profile.ps1 +++ b/src/actions/profile.ps1 @@ -1,630 +1,634 @@ - -function Set-IniSetting-Direct { - param ($iniPath, $settingName, $value, $enabled = $true) - - try { - $lines = [string[]](Get-Content $iniPath) - $modified = $false - $escapedName = [regex]::Escape($settingName) - $exactPattern = "^[#;]?\s*$escapedName\s*=\s*(.*)$" - - for ($i = 0; $i -lt $lines.Count; $i++) { - if ($lines[$i] -match $exactPattern) { - $newLine = if ($enabled) { - "$settingName = $value" - } else { - ";$settingName = $value" - } - $lines[$i] = $newLine - $modified = $true - break - } - } - - if (-not $modified) { - # Setting doesn't exist, add it at the end - $newLine = if ($enabled) { - "$settingName = $value" - } else { - ";$settingName = $value" - } - $lines += $newLine - } - - Set-Content $iniPath $lines -Encoding UTF8 - return 0 - } catch { - return -1 - } -} - -function Enable-IniExtension-Direct { - param ($iniPath, $extName, $extType = "extension") - - try { - # Normalize extension name - remove php_ prefix and .dll suffix if present - $extName = $extName -replace '^php_', '' -replace '\.dll$', '' - $extFileName = "php_$extName.dll" - - $lines = [string[]](Get-Content $iniPath) - $modified = $false - - # Check for extension in multiple formats: - # 1. extension=php_openssl.dll (full filename, may have path) - # 2. extension=openssl (just the name without php_ prefix and .dll suffix) - for ($i = 0; $i -lt $lines.Count; $i++) { - $line = $lines[$i] - $isMatch = $false - - # Match extension or zend_extension lines (commented or not) - $pattern = if ($extType -eq "zend_extension") { - "^[#;]?\s*zend_extension\s*=\s*([`"']?)([^\s`"';]*)\1\s*(;.*)?$" - } else { - "^[#;]?\s*extension\s*=\s*([`"']?)([^\s`"';]*)\1\s*(;.*)?$" - } - - if ($line -match $pattern) { - $foundExt = $matches[2].Trim() - # Extract just the filename if there's a path - $foundExtFileName = [System.IO.Path]::GetFileName($foundExt) - # Normalize: remove php_ prefix and .dll suffix to get base name - $foundExtBaseName = $foundExtFileName -replace '^php_', '' -replace '\.dll$', '' - - # Also check the original value (for cases like extension=openssl) - $foundExtBaseNameOriginal = $foundExt -replace '^php_', '' -replace '\.dll$', '' - - # Match if the normalized base name matches (handles both formats) - if ($foundExtBaseName -eq $extName -or $foundExtBaseNameOriginal -eq $extName) { - $isMatch = $true - } - } - - if ($isMatch) { - # Uncomment the line (remove leading ; or #) - $lines[$i] = $line -replace '^[#;]\s*', '' - $modified = $true - break - } - } - - if (-not $modified) { - # Extension doesn't exist, add it at the end - $newLine = if ($extType -eq "zend_extension") { - "zend_extension=$extFileName" - } else { - "extension=$extFileName" - } - $lines += $newLine - } - - Set-Content $iniPath $lines -Encoding UTF8 - return 0 - } catch { - return -1 - } -} - -function Disable-IniExtension-Direct { - param ($iniPath, $extName, $extType = "extension") - - try { - # Normalize extension name - remove php_ prefix and .dll suffix if present - $extName = $extName -replace '^php_', '' -replace '\.dll$', '' - $extFileName = "php_$extName.dll" - - $lines = [string[]](Get-Content $iniPath) - $modified = $false - - # Check for extension in multiple formats (only enabled/not commented lines): - # 1. extension=php_openssl.dll (full filename, may have path) - # 2. extension=openssl (just the name without php_ prefix and .dll suffix) - for ($i = 0; $i -lt $lines.Count; $i++) { - $line = $lines[$i] - # Skip commented lines - if ($line -match '^\s*[#;]') { - continue - } - - $isMatch = $false - - # Match extension or zend_extension lines (must be enabled/not commented) - $pattern = if ($extType -eq "zend_extension") { - "^\s*zend_extension\s*=\s*([`"']?)([^\s`"';]*)\1\s*(;.*)?$" - } else { - "^\s*extension\s*=\s*([`"']?)([^\s`"';]*)\1\s*(;.*)?$" - } - - if ($line -match $pattern) { - $foundExt = $matches[2].Trim() - # Extract just the filename if there's a path - $foundExtFileName = [System.IO.Path]::GetFileName($foundExt) - # Normalize: remove php_ prefix and .dll suffix to get base name - $foundExtBaseName = $foundExtFileName -replace '^php_', '' -replace '\.dll$', '' - - # Also check the original value (for cases like extension=openssl) - $foundExtBaseNameOriginal = $foundExt -replace '^php_', '' -replace '\.dll$', '' - - # Match if the normalized base name matches (handles both formats) - if ($foundExtBaseName -eq $extName -or $foundExtBaseNameOriginal -eq $extName) { - $isMatch = $true - } - } - - if ($isMatch) { - # Comment out the line - $lines[$i] = ";$line" - $modified = $true - break - } - } - - Set-Content $iniPath $lines -Encoding UTF8 - return 0 - } catch { - return -1 - } -} - -function Get-Popular-PHP-Settings { - # Return list of popular/common PHP settings that should be included in profiles - return @( - "memory_limit", "max_execution_time", "max_input_time", - "post_max_size", "upload_max_filesize", "max_file_uploads", - "display_errors", "error_reporting", "log_errors", - "opcache.enable", "opcache.enable_cli", "opcache.memory_consumption", "opcache.max_accelerated_files" - ) -} - -function Get-Popular-PHP-Extensions { - # Return list of popular/common PHP extensions that should be included in profiles - return @( - "curl", "fileinfo", "gd", "gettext", "intl", "mbstring", "exif", "openssl", - "mysqli", "pdo_mysql", "pdo_pgsql", "pdo_sqlite", "pgsql", - "sodium", "sqlite3", "zip", "opcache", "xdebug" - ) -} - -function Save-PHP-Profile { - param($profileName, $description = $null) - - try { - $currentPhpVersion = Get-Current-PHP-Version - - if (-not $currentPhpVersion -or -not $currentPhpVersion.version -or -not $currentPhpVersion.path) { - Write-Host "`nFailed to get current PHP version." -ForegroundColor DarkYellow - return -1 - } - - $iniPath = "$($currentPhpVersion.path)\php.ini" - if (-not (Test-Path $iniPath)) { - Write-Host "`nphp.ini not found at: $($currentPhpVersion.path)" -ForegroundColor DarkYellow - return -1 - } - - # Get current PHP configuration - $phpIniData = Get-PHP-Data -PhpIniPath $iniPath - - # Build profile structure - $userProfile = [ordered]@{ - name = $profileName - description = if ($description) { $description } else { "Profile saved on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" } - created = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ') - phpVersion = $currentPhpVersion.version - settings = [ordered]@{} - extensions = [ordered]@{} - } - - # Get popular settings and extensions lists - $popularSettings = Get-Popular-PHP-Settings - $popularExtensions = Get-Popular-PHP-Extensions - - # Extract only popular settings - foreach ($setting in $phpIniData.settings) { - if ($popularSettings -contains $setting.Name) { - $userProfile.settings[$setting.Name] = @{ - value = $setting.Value - enabled = $setting.Enabled - } - } - } - - # Extract only popular extensions - foreach ($ext in $phpIniData.extensions) { - $extName = $ext.Extension -replace '^php_', '' -replace '\.dll$', '' - if ($popularExtensions -contains $extName) { - $userProfile.extensions[$extName] = @{ - enabled = $ext.Enabled - type = $ext.Type # "extension" or "zend_extension" - } - } - } - - # Save to JSON file - $created = Make-Directory -path $PROFILES_PATH - if ($created -ne 0) { - Write-Host "`nFailed to create profiles directory." -ForegroundColor DarkYellow - return -1 - } - - $profilePath = "$PROFILES_PATH\$profileName.json" - $jsonContent = $userProfile | ConvertTo-Json -Depth 10 - Set-Content -Path $profilePath -Value $jsonContent -Encoding UTF8 - - Write-Host "`nProfile '$profileName' saved successfully." -ForegroundColor DarkGreen - Write-Host " Settings: $($userProfile.settings.Count) (popular/common only)" -ForegroundColor Gray - Write-Host " Extensions: $($userProfile.extensions.Count) (popular/common only)" -ForegroundColor Gray - Write-Host " Location: $profilePath" -ForegroundColor Gray - Write-Host "`nNote: Only popular/common settings and extensions are saved." -ForegroundColor DarkCyan - Write-Host " You can manually add other settings/extensions using 'pvm ini' commands." -ForegroundColor DarkCyan - - return 0 - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to save profile '$profileName'" - exception = $_ - } - Write-Host "`nFailed to save profile: $($_.Exception.Message)" -ForegroundColor DarkYellow - return -1 - } -} - -function Load-PHP-Profile { - param($profileName) - - try { - $currentPhpVersion = Get-Current-PHP-Version - - if (-not $currentPhpVersion -or -not $currentPhpVersion.version -or -not $currentPhpVersion.path) { - Write-Host "`nFailed to get current PHP version." -ForegroundColor DarkYellow - return -1 - } - - $iniPath = "$($currentPhpVersion.path)\php.ini" - if (-not (Test-Path $iniPath)) { - Write-Host "`nphp.ini not found at: $($currentPhpVersion.path)" -ForegroundColor DarkYellow - return -1 - } - - # Load profile JSON - $profilePath = "$PROFILES_PATH\$profileName.json" - if (-not (Test-Path $profilePath)) { - Write-Host "`nProfile '$profileName' not found." -ForegroundColor DarkYellow - Write-Host " Use 'pvm profile list' to see available profiles." -ForegroundColor Gray - return -1 - } - - $jsonContent = Get-Content $profilePath -Raw | ConvertFrom-Json - - Write-Host "`nLoading profile '$($jsonContent.name)'..." -ForegroundColor Cyan - if ($jsonContent.description) { - Write-Host " Description: $($jsonContent.description)" -ForegroundColor Gray - } - Write-Host " Created: $($jsonContent.created)" -ForegroundColor Gray - - # Backup ini file before applying changes - Backup-IniFile $iniPath - - # Get popular lists to validate profile contents - $popularSettings = Get-Popular-PHP-Settings - $popularExtensions = Get-Popular-PHP-Extensions - - # Apply only popular settings (filter out any non-popular ones that might be in old profiles) - # Use direct functions for exact name matching (no fuzzy matching or user interaction) - $settingsApplied = 0 - $settingsSkipped = 0 - $settingsIgnored = 0 - foreach ($settingName in $jsonContent.settings.PSObject.Properties.Name) { - if ($popularSettings -contains $settingName) { - $setting = $jsonContent.settings.$settingName - $result = Set-IniSetting-Direct -iniPath $iniPath -settingName $settingName -value $setting.value -enabled $setting.enabled - if ($result -eq 0) { - $settingsApplied++ - } else { - $settingsSkipped++ - } - } else { - $settingsIgnored++ - } - } - - # Apply only popular extensions (filter out any non-popular ones that might be in old profiles) - # Use direct functions for exact name matching (no fuzzy matching or user interaction) - $extensionsEnabled = 0 - $extensionsDisabled = 0 - $extensionsSkipped = 0 - $extensionsIgnored = 0 - foreach ($extName in $jsonContent.extensions.PSObject.Properties.Name) { - if ($popularExtensions -contains $extName) { - $ext = $jsonContent.extensions.$extName - $extType = if ($ext.type) { $ext.type } else { "extension" } - if ($ext.enabled) { - $result = Enable-IniExtension-Direct -iniPath $iniPath -extName $extName -extType $extType - if ($result -eq 0) { - $extensionsEnabled++ - } else { - $extensionsSkipped++ - } - } else { - $result = Disable-IniExtension-Direct -iniPath $iniPath -extName $extName -extType $extType - if ($result -eq 0) { - $extensionsDisabled++ - } else { - $extensionsSkipped++ - } - } - } else { - $extensionsIgnored++ - } - } - - Write-Host "`nProfile applied successfully:" -ForegroundColor DarkGreen - Write-Host " Settings applied: $settingsApplied" -ForegroundColor Gray - if ($settingsSkipped -gt 0) { - Write-Host " Settings skipped: $settingsSkipped" -ForegroundColor DarkYellow - } - if ($settingsIgnored -gt 0) { - Write-Host " Settings ignored (not popular): $settingsIgnored" -ForegroundColor DarkCyan - } - Write-Host " Extensions enabled: $extensionsEnabled" -ForegroundColor Gray - Write-Host " Extensions disabled: $extensionsDisabled" -ForegroundColor Gray - if ($extensionsSkipped -gt 0) { - Write-Host " Extensions skipped: $extensionsSkipped" -ForegroundColor DarkYellow - } - if ($extensionsIgnored -gt 0) { - Write-Host " Extensions ignored (not popular): $extensionsIgnored" -ForegroundColor DarkCyan - } - - return 0 - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to load profile '$profileName'" - exception = $_ - } - Write-Host "`nFailed to load profile: $($_.Exception.Message)" -ForegroundColor DarkYellow - return -1 - } -} - -function List-PHP-Profiles { - try { - if (-not (Test-Path $PROFILES_PATH)) { - Write-Host "`nNo profiles directory found. Create a profile with 'pvm profile save '." -ForegroundColor DarkYellow - return -1 - } - - $profileFiles = Get-ChildItem -Path $PROFILES_PATH -Filter "*.json" -ErrorAction SilentlyContinue - - if ($profileFiles.Count -eq 0) { - Write-Host "`nNo profiles found. Create a profile with 'pvm profile save '." -ForegroundColor DarkYellow - return -1 - } - - Write-Host "`nAvailable Profiles:" -ForegroundColor Cyan - Write-Host "-------------------" - - $profiles = @() - foreach ($file in $profileFiles) { - try { - $userProfile = Get-Content $file.FullName -Raw | ConvertFrom-Json - $settingsCount = if ($userProfile.settings) { ($userProfile.settings.PSObject.Properties | Measure-Object).Count } else { 0 } - $extensionsCount = if ($userProfile.extensions) { ($userProfile.extensions.PSObject.Properties | Measure-Object).Count } else { 0 } - $profiles += [PSCustomObject]@{ - Name = $userProfile.name - Description = if ($userProfile.description) { $userProfile.description } else { "(no description)" } - Created = $userProfile.created - PHPVersion = $userProfile.phpVersion - Settings = $settingsCount - Extensions = $extensionsCount - File = $file.Name - } - } catch { - Write-Host " Warning: Failed to parse $($file.Name)" -ForegroundColor DarkYellow - } - } - - $maxNameLength = ($profiles.Name | Measure-Object -Maximum Length).Maximum + 10 - - foreach ($userProfile in $profiles) { - Write-Host " Name ".PadRight($maxNameLength, '.') $($userProfile.Name) - Write-Host " Description ".PadRight($maxNameLength, '.') $($userProfile.Description) - Write-Host " Created ".PadRight($maxNameLength, '.') $($userProfile.Created) - Write-Host " PHP ".PadRight($maxNameLength, '.') $($userProfile.PHPVersion) - Write-Host " Settings ".PadRight($maxNameLength, '.') $($userProfile.Settings) - Write-Host " Extensions ".PadRight($maxNameLength, '.') $($userProfile.Extensions) - Write-Host " Path ".PadRight($maxNameLength, '.') "$PROFILES_PATH\$($userProfile.File)`n" - } - - return 0 - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to list profiles" - exception = $_ - } - Write-Host "`nFailed to list profiles: $($_.Exception.Message)" -ForegroundColor DarkYellow - return -1 - } -} - -function Show-PHP-Profile { - param($profileName) - - try { - $profilePath = "$PROFILES_PATH\$profileName.json" - if (-not (Test-Path $profilePath)) { - Write-Host "`nProfile '$profileName' not found." -ForegroundColor DarkYellow - Write-Host " Use 'pvm profile list' to see available profiles." -ForegroundColor Gray - return -1 - } - - $userProfile = Get-Content $profilePath -Raw | ConvertFrom-Json - - Write-Host "`nProfile: $($userProfile.name)" -ForegroundColor Cyan - Write-Host "=========================" - Write-Host "Description: $($userProfile.description)" -ForegroundColor White - Write-Host "Created: $($userProfile.created)" -ForegroundColor White - Write-Host "PHP Version: $($userProfile.phpVersion)" -ForegroundColor White - Write-Host "PATH: $profilePath" -ForegroundColor White - - $settingsCount = if ($userProfile.settings) { ($userProfile.settings.PSObject.Properties | Measure-Object).Count } else { 0 } - Write-Host "`nSettings ($settingsCount):" -ForegroundColor Cyan - if ($settingsCount -eq 0) { - Write-Host " (none)" -ForegroundColor Gray - } else { - $maxNameLength = ($userProfile.settings.PSObject.Properties.Name | Measure-Object -Maximum Length).Maximum + 10 - foreach ($settingName in ($userProfile.settings.PSObject.Properties.Name | Sort-Object)) { - $setting = $userProfile.settings.$settingName - $name = "$settingName ".PadRight($maxNameLength, '.') - $status = if ($setting.enabled) { "Enabled" } else { "Disabled" } - $color = if ($setting.enabled) { "DarkGreen" } else { "DarkYellow" } - Write-Host " $name $($setting.value) " -NoNewline - Write-Host $status -ForegroundColor $color - } - } - - $extensionsCount = if ($userProfile.extensions) { ($userProfile.extensions.PSObject.Properties | Measure-Object).Count } else { 0 } - Write-Host "`nExtensions ($extensionsCount):" -ForegroundColor Cyan - if ($extensionsCount -eq 0) { - Write-Host " (none)" -ForegroundColor Gray - } else { - $maxNameLength = ($userProfile.extensions.PSObject.Properties.Name | Measure-Object -Maximum Length).Maximum + 21 - foreach ($extName in ($userProfile.extensions.PSObject.Properties.Name | Sort-Object)) { - $ext = $userProfile.extensions.$extName - $name = "$extName ".PadRight($maxNameLength, '.') - $status = if ($ext.enabled) { "Enabled" } else { "Disabled" } - $color = if ($ext.enabled) { "DarkGreen" } else { "DarkYellow" } - $type = $ext.type - Write-Host " $name $type " -NoNewline - Write-Host $status -ForegroundColor $color - } - } - - return 0 - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to show profile '$profileName'" - exception = $_ - } - Write-Host "`nFailed to show profile: $($_.Exception.Message)" -ForegroundColor DarkYellow - return -1 - } -} - -function Delete-PHP-Profile { - param($profileName) - - try { - $profilePath = "$PROFILES_PATH\$profileName.json" - - if (-not (Test-Path $profilePath)) { - Write-Host "`nProfile '$profileName' not found." -ForegroundColor DarkYellow - return -1 - } - - $response = Read-Host "`nAre you sure you want to delete profile '$profileName'? (y/n)" - $response = $response.Trim() - - if ($response -ne "y" -and $response -ne "Y") { - Write-Host "`nDeletion cancelled." -ForegroundColor Gray - return -1 - } - - Remove-Item -Path $profilePath -Force - Write-Host "`nProfile '$profileName' deleted successfully." -ForegroundColor DarkGreen - - return 0 - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to delete profile '$profileName'" - exception = $_ - } - Write-Host "`nFailed to delete profile: $($_.Exception.Message)" -ForegroundColor DarkYellow - return -1 - } -} - -function Export-PHP-Profile { - param($profileName, $exportPath = $null) - - try { - $profilePath = "$PROFILES_PATH\$profileName.json" - - if (-not (Test-Path $profilePath)) { - Write-Host "`nProfile '$profileName' not found." -ForegroundColor DarkYellow - return -1 - } - - if (-not $exportPath) { - $exportPath = "$(Get-Location)\$profileName.json" - } - - Copy-Item -Path $profilePath -Destination $exportPath -Force - Write-Host "`nProfile '$profileName' exported to: $exportPath" -ForegroundColor DarkGreen - - return 0 - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to export profile '$profileName'" - exception = $_ - } - Write-Host "`nFailed to export profile: $($_.Exception.Message)" -ForegroundColor DarkYellow - return -1 - } -} - -function Import-PHP-Profile { - param($importPath, $profileName = $null) - - try { - if (-not (Test-Path $importPath)) { - Write-Host "`nFile not found: $importPath" -ForegroundColor DarkYellow - return -1 - } - - # Validate JSON structure - try { - $userProfile = Get-Content $importPath -Raw | ConvertFrom-Json - if (-not $userProfile.name -or -not $userProfile.settings -or -not $userProfile.extensions) { - Write-Host "`nInvalid profile format. Profile must contain 'name', 'settings', and 'extensions'." -ForegroundColor DarkYellow - return -1 - } - } catch { - Write-Host "`nInvalid JSON file: $($_.Exception.Message)" -ForegroundColor DarkYellow - return -1 - } - - # Use provided name or name from profile - $finalName = if ($profileName) { $profileName } else { $userProfile.name } - - $created = Make-Directory -path $PROFILES_PATH - if ($created -ne 0) { - Write-Host "`nFailed to create profiles directory." -ForegroundColor DarkYellow - return -1 - } - - $targetPath = "$PROFILES_PATH\$finalName.json" - - # Update profile name if different - if ($finalName -ne $userProfile.name) { - $userProfile.name = $finalName - $jsonContent = $userProfile | ConvertTo-Json -Depth 10 - Set-Content -Path $targetPath -Value $jsonContent -Encoding UTF8 - } else { - Copy-Item -Path $importPath -Destination $targetPath -Force - } - - Write-Host "`nProfile imported successfully as '$finalName'." -ForegroundColor DarkGreen - Write-Host " Use 'pvm profile load $finalName' to apply it." -ForegroundColor Gray - - return 0 - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to import profile from '$importPath'" - exception = $_ - } - Write-Host "`nFailed to import profile: $($_.Exception.Message)" -ForegroundColor DarkYellow - return -1 - } -} - - - + +function Set-IniSetting-Direct { + param ($iniPath, $settingName, $value, $enabled = $true) + + try { + $lines = [string[]](Get-Content $iniPath) + $modified = $false + $escapedName = [regex]::Escape($settingName) + $exactPattern = "^[#;]?\s*$escapedName\s*=\s*(.*)$" + + for ($i = 0; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match $exactPattern) { + $newLine = if ($enabled) { + "$settingName = $value" + } else { + ";$settingName = $value" + } + $lines[$i] = $newLine + $modified = $true + break + } + } + + if (-not $modified) { + # Setting doesn't exist, add it at the end + $newLine = if ($enabled) { + "$settingName = $value" + } else { + ";$settingName = $value" + } + $lines += $newLine + } + + Set-Content $iniPath $lines -Encoding UTF8 + return 0 + } catch { + return -1 + } +} + +function Enable-IniExtension-Direct { + param ($iniPath, $extName, $extType = "extension") + + try { + # Normalize extension name - remove php_ prefix and .dll suffix if present + $extName = $extName -replace '^php_', '' -replace '\.dll$', '' + $extFileName = "php_$extName.dll" + + $lines = [string[]](Get-Content $iniPath) + $modified = $false + + # Check for extension in multiple formats: + # 1. extension=php_openssl.dll (full filename, may have path) + # 2. extension=openssl (just the name without php_ prefix and .dll suffix) + for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i] + $isMatch = $false + + # Match extension or zend_extension lines (commented or not) + $pattern = if ($extType -eq "zend_extension") { + "^[#;]?\s*zend_extension\s*=\s*([`"']?)([^\s`"';]*)\1\s*(;.*)?$" + } else { + "^[#;]?\s*extension\s*=\s*([`"']?)([^\s`"';]*)\1\s*(;.*)?$" + } + + if ($line -match $pattern) { + $foundExt = $matches[2].Trim() + # Extract just the filename if there's a path + $foundExtFileName = [System.IO.Path]::GetFileName($foundExt) + # Normalize: remove php_ prefix and .dll suffix to get base name + $foundExtBaseName = $foundExtFileName -replace '^php_', '' -replace '\.dll$', '' + + # Also check the original value (for cases like extension=openssl) + $foundExtBaseNameOriginal = $foundExt -replace '^php_', '' -replace '\.dll$', '' + + # Match if the normalized base name matches (handles both formats) + if ($foundExtBaseName -eq $extName -or $foundExtBaseNameOriginal -eq $extName) { + $isMatch = $true + } + } + + if ($isMatch) { + # Uncomment the line (remove leading ; or #) + $lines[$i] = $line -replace '^[#;]\s*', '' + $modified = $true + break + } + } + + if (-not $modified) { + # Extension doesn't exist, add it at the end + $newLine = if ($extType -eq "zend_extension") { + "zend_extension=$extFileName" + } else { + "extension=$extFileName" + } + $lines += $newLine + } + + Set-Content $iniPath $lines -Encoding UTF8 + return 0 + } catch { + return -1 + } +} + +function Disable-IniExtension-Direct { + param ($iniPath, $extName, $extType = "extension") + + try { + # Normalize extension name - remove php_ prefix and .dll suffix if present + $extName = $extName -replace '^php_', '' -replace '\.dll$', '' + $extFileName = "php_$extName.dll" + + $lines = [string[]](Get-Content $iniPath) + $modified = $false + + # Check for extension in multiple formats (only enabled/not commented lines): + # 1. extension=php_openssl.dll (full filename, may have path) + # 2. extension=openssl (just the name without php_ prefix and .dll suffix) + for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i] + # Skip commented lines + if ($line -match '^\s*[#;]') { + continue + } + + $isMatch = $false + + # Match extension or zend_extension lines (must be enabled/not commented) + $pattern = if ($extType -eq "zend_extension") { + "^\s*zend_extension\s*=\s*([`"']?)([^\s`"';]*)\1\s*(;.*)?$" + } else { + "^\s*extension\s*=\s*([`"']?)([^\s`"';]*)\1\s*(;.*)?$" + } + + if ($line -match $pattern) { + $foundExt = $matches[2].Trim() + # Extract just the filename if there's a path + $foundExtFileName = [System.IO.Path]::GetFileName($foundExt) + # Normalize: remove php_ prefix and .dll suffix to get base name + $foundExtBaseName = $foundExtFileName -replace '^php_', '' -replace '\.dll$', '' + + # Also check the original value (for cases like extension=openssl) + $foundExtBaseNameOriginal = $foundExt -replace '^php_', '' -replace '\.dll$', '' + + # Match if the normalized base name matches (handles both formats) + if ($foundExtBaseName -eq $extName -or $foundExtBaseNameOriginal -eq $extName) { + $isMatch = $true + } + } + + if ($isMatch) { + # Comment out the line + $lines[$i] = ";$line" + $modified = $true + break + } + } + + Set-Content $iniPath $lines -Encoding UTF8 + return 0 + } catch { + return -1 + } +} + +function Get-Popular-PHP-Settings { + # Return list of popular/common PHP settings that should be included in profiles + return @( + "memory_limit", "max_execution_time", "max_input_time", + "post_max_size", "upload_max_filesize", "max_file_uploads", + "display_errors", "error_reporting", "log_errors", + "opcache.enable", "opcache.enable_cli", "opcache.memory_consumption", "opcache.max_accelerated_files" + ) +} + +function Get-Popular-PHP-Extensions { + # Return list of popular/common PHP extensions that should be included in profiles + return @( + "curl", "fileinfo", "gd", "gettext", "intl", "mbstring", "exif", "openssl", + "mysqli", "pdo_mysql", "pdo_pgsql", "pdo_sqlite", "pgsql", + "sodium", "sqlite3", "zip", "opcache", "xdebug" + ) +} + +function Save-PHP-Profile { + param($profileName, $description = $null) + + try { + $currentPhpVersion = Get-Current-PHP-Version + + if (-not $currentPhpVersion -or -not $currentPhpVersion.version -or -not $currentPhpVersion.path) { + Write-Host "`nFailed to get current PHP version." -ForegroundColor DarkYellow + return -1 + } + + $iniPath = "$($currentPhpVersion.path)\php.ini" + if (-not (Test-Path $iniPath)) { + Write-Host "`nphp.ini not found at: $($currentPhpVersion.path)" -ForegroundColor DarkYellow + return -1 + } + + # Get current PHP configuration + $phpIniData = Get-PHP-Data -PhpIniPath $iniPath + + # Build profile structure + $userProfile = [ordered]@{ + name = $profileName + description = if ($description) { $description } else { "Profile saved on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" } + created = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ') + phpVersion = $currentPhpVersion.version + settings = [ordered]@{} + extensions = [ordered]@{} + } + + # Get popular settings and extensions lists + $popularSettings = Get-Popular-PHP-Settings + $popularExtensions = Get-Popular-PHP-Extensions + + # Extract only popular settings + foreach ($setting in $phpIniData.settings) { + if ($popularSettings -contains $setting.Name) { + $userProfile.settings[$setting.Name] = @{ + value = $setting.Value + enabled = $setting.Enabled + } + } + } + + # Extract only popular extensions + foreach ($ext in $phpIniData.extensions) { + $extName = $ext.Extension -replace '^php_', '' -replace '\.dll$', '' + if ($popularExtensions -contains $extName) { + $userProfile.extensions[$extName] = @{ + enabled = $ext.Enabled + type = $ext.Type # "extension" or "zend_extension" + } + } + } + + # Save to JSON file + $created = Make-Directory -path $PROFILES_PATH + if ($created -ne 0) { + Write-Host "`nFailed to create profiles directory." -ForegroundColor DarkYellow + return -1 + } + + $profilePath = "$PROFILES_PATH\$profileName.json" + $jsonContent = $userProfile | ConvertTo-Json -Depth 10 + Set-Content -Path $profilePath -Value $jsonContent -Encoding UTF8 + + Write-Host "`nProfile '$profileName' saved successfully." -ForegroundColor DarkGreen + Write-Host " Settings: $($userProfile.settings.Count) (popular/common only)" -ForegroundColor Gray + Write-Host " Extensions: $($userProfile.extensions.Count) (popular/common only)" -ForegroundColor Gray + Write-Host " Location: $profilePath" -ForegroundColor Gray + Write-Host "`nNote: Only popular/common settings and extensions are saved." -ForegroundColor DarkCyan + Write-Host " You can manually add other settings/extensions using 'pvm ini' commands." -ForegroundColor DarkCyan + + return 0 + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to save profile '$profileName'" + exception = $_ + } + Write-Host "`nFailed to save profile: $($_.Exception.Message)" -ForegroundColor DarkYellow + return -1 + } +} + +function Load-PHP-Profile { + param($profileName) + + try { + $currentPhpVersion = Get-Current-PHP-Version + + if (-not $currentPhpVersion -or -not $currentPhpVersion.version -or -not $currentPhpVersion.path) { + Write-Host "`nFailed to get current PHP version." -ForegroundColor DarkYellow + return -1 + } + + $iniPath = "$($currentPhpVersion.path)\php.ini" + if (-not (Test-Path $iniPath)) { + Write-Host "`nphp.ini not found at: $($currentPhpVersion.path)" -ForegroundColor DarkYellow + return -1 + } + + # Load profile JSON + $profilePath = "$PROFILES_PATH\$profileName.json" + if (-not (Test-Path $profilePath)) { + Write-Host "`nProfile '$profileName' not found." -ForegroundColor DarkYellow + Write-Host " Use 'pvm profile list' to see available profiles." -ForegroundColor Gray + return -1 + } + + $jsonContent = Get-Content $profilePath -Raw | ConvertFrom-Json + + Write-Host "`nLoading profile '$($jsonContent.name)'..." -ForegroundColor Cyan + if ($jsonContent.description) { + Write-Host " Description: $($jsonContent.description)" -ForegroundColor Gray + } + Write-Host " Created: $($jsonContent.created)" -ForegroundColor Gray + + # Backup ini file before applying changes + Backup-IniFile $iniPath + + # Get popular lists to validate profile contents + $popularSettings = Get-Popular-PHP-Settings + $popularExtensions = Get-Popular-PHP-Extensions + + # Apply only popular settings (filter out any non-popular ones that might be in old profiles) + # Use direct functions for exact name matching (no fuzzy matching or user interaction) + $settingsApplied = 0 + $settingsSkipped = 0 + $settingsIgnored = 0 + foreach ($settingName in $jsonContent.settings.PSObject.Properties.Name) { + if ($popularSettings -contains $settingName) { + $setting = $jsonContent.settings.$settingName + $result = Set-IniSetting-Direct -iniPath $iniPath -settingName $settingName -value $setting.value -enabled $setting.enabled + if ($result -eq 0) { + $settingsApplied++ + } else { + $settingsSkipped++ + } + } else { + $settingsIgnored++ + } + } + + # Apply only popular extensions (filter out any non-popular ones that might be in old profiles) + # Use direct functions for exact name matching (no fuzzy matching or user interaction) + $extensionsEnabled = 0 + $extensionsDisabled = 0 + $extensionsSkipped = 0 + $extensionsIgnored = 0 + foreach ($extName in $jsonContent.extensions.PSObject.Properties.Name) { + if ($popularExtensions -contains $extName) { + $ext = $jsonContent.extensions.$extName + $extType = if ($ext.type) { $ext.type } else { "extension" } + if ($ext.enabled) { + $result = Enable-IniExtension-Direct -iniPath $iniPath -extName $extName -extType $extType + if ($result -eq 0) { + $extensionsEnabled++ + } else { + $extensionsSkipped++ + } + } else { + $result = Disable-IniExtension-Direct -iniPath $iniPath -extName $extName -extType $extType + if ($result -eq 0) { + $extensionsDisabled++ + } else { + $extensionsSkipped++ + } + } + } else { + $extensionsIgnored++ + } + } + + Write-Host "`nProfile applied successfully:" -ForegroundColor DarkGreen + Write-Host " Settings applied: $settingsApplied" -ForegroundColor Gray + if ($settingsSkipped -gt 0) { + Write-Host " Settings skipped: $settingsSkipped" -ForegroundColor DarkYellow + } + if ($settingsIgnored -gt 0) { + Write-Host " Settings ignored (not popular): $settingsIgnored" -ForegroundColor DarkCyan + } + Write-Host " Extensions enabled: $extensionsEnabled" -ForegroundColor Gray + Write-Host " Extensions disabled: $extensionsDisabled" -ForegroundColor Gray + if ($extensionsSkipped -gt 0) { + Write-Host " Extensions skipped: $extensionsSkipped" -ForegroundColor DarkYellow + } + if ($extensionsIgnored -gt 0) { + Write-Host " Extensions ignored (not popular): $extensionsIgnored" -ForegroundColor DarkCyan + } + + return 0 + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to load profile '$profileName'" + exception = $_ + } + Write-Host "`nFailed to load profile: $($_.Exception.Message)" -ForegroundColor DarkYellow + return -1 + } +} + +function List-PHP-Profiles { + try { + if (-not (Test-Path $PROFILES_PATH)) { + Write-Host "`nNo profiles directory found. Create a profile with 'pvm profile save '." -ForegroundColor DarkYellow + return -1 + } + + $profileFiles = Get-ChildItem -Path $PROFILES_PATH -Filter "*.json" -ErrorAction SilentlyContinue + + if ($profileFiles.Count -eq 0) { + Write-Host "`nNo profiles found. Create a profile with 'pvm profile save '." -ForegroundColor DarkYellow + return -1 + } + + Write-Host "`nAvailable Profiles:" -ForegroundColor Cyan + Write-Host "-------------------" + + $profiles = @() + foreach ($file in $profileFiles) { + try { + $userProfile = Get-Content $file.FullName -Raw | ConvertFrom-Json + $settingsCount = if ($userProfile.settings) { ($userProfile.settings.PSObject.Properties | Measure-Object).Count } else { 0 } + $extensionsCount = if ($userProfile.extensions) { ($userProfile.extensions.PSObject.Properties | Measure-Object).Count } else { 0 } + $profiles += [PSCustomObject]@{ + Name = $userProfile.name + Description = if ($userProfile.description) { $userProfile.description } else { "(no description)" } + Created = $userProfile.created + PHPVersion = $userProfile.phpVersion + Settings = $settingsCount + Extensions = $extensionsCount + File = $file.Name + } + } catch { + Write-Host " Warning: Failed to parse $($file.Name)" -ForegroundColor DarkYellow + } + } + + $maxNameLength = ($profiles.Name | Measure-Object -Maximum Length).Maximum + 10 + + foreach ($userProfile in $profiles) { + Write-Host " Name ".PadRight($maxNameLength, '.') $($userProfile.Name) + Write-Host " Description ".PadRight($maxNameLength, '.') $($userProfile.Description) + Write-Host " Created ".PadRight($maxNameLength, '.') $($userProfile.Created) + Write-Host " PHP ".PadRight($maxNameLength, '.') $($userProfile.PHPVersion) + Write-Host " Settings ".PadRight($maxNameLength, '.') $($userProfile.Settings) + Write-Host " Extensions ".PadRight($maxNameLength, '.') $($userProfile.Extensions) + Write-Host " Path ".PadRight($maxNameLength, '.') "$PROFILES_PATH\$($userProfile.File)`n" + } + + return 0 + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to list profiles" + exception = $_ + } + Write-Host "`nFailed to list profiles: $($_.Exception.Message)" -ForegroundColor DarkYellow + return -1 + } +} + +function Show-PHP-Profile { + param($profileName) + + try { + $profilePath = "$PROFILES_PATH\$profileName.json" + if (-not (Test-Path $profilePath)) { + Write-Host "`nProfile '$profileName' not found." -ForegroundColor DarkYellow + Write-Host " Use 'pvm profile list' to see available profiles." -ForegroundColor Gray + return -1 + } + + $userProfile = Get-Content $profilePath -Raw | ConvertFrom-Json + + $dt = [datetime]$userProfile.Created + $utc = $dt.ToUniversalTime() + $createdAtFormatted = $utc.ToString("dd/MM/yyyy HH:mm:ss") + + Write-Host "`nProfile: $($userProfile.name)" -ForegroundColor Cyan + Write-Host "=========================" + Write-Host "Description: $($userProfile.description)" -ForegroundColor White + Write-Host "Created: $createdAtFormatted" -ForegroundColor White + Write-Host "PHP Version: $($userProfile.phpVersion)" -ForegroundColor White + Write-Host "PATH: $profilePath" -ForegroundColor White + + $settingsCount = if ($userProfile.settings) { ($userProfile.settings.PSObject.Properties | Measure-Object).Count } else { 0 } + Write-Host "`nSettings ($settingsCount):" -ForegroundColor Cyan + if ($settingsCount -eq 0) { + Write-Host " (none)" -ForegroundColor Gray + } else { + $maxNameLength = ($userProfile.settings.PSObject.Properties.Name | Measure-Object -Maximum Length).Maximum + 10 + foreach ($settingName in ($userProfile.settings.PSObject.Properties.Name | Sort-Object)) { + $setting = $userProfile.settings.$settingName + $name = "$settingName ".PadRight($maxNameLength, '.') + $status = if ($setting.enabled) { "Enabled" } else { "Disabled" } + $color = if ($setting.enabled) { "DarkGreen" } else { "DarkYellow" } + Write-Host " $name $($setting.value) " -NoNewline + Write-Host $status -ForegroundColor $color + } + } + + $extensionsCount = if ($userProfile.extensions) { ($userProfile.extensions.PSObject.Properties | Measure-Object).Count } else { 0 } + Write-Host "`nExtensions ($extensionsCount):" -ForegroundColor Cyan + if ($extensionsCount -eq 0) { + Write-Host " (none)" -ForegroundColor Gray + } else { + $maxNameLength = ($userProfile.extensions.PSObject.Properties.Name | Measure-Object -Maximum Length).Maximum + 21 + foreach ($extName in ($userProfile.extensions.PSObject.Properties.Name | Sort-Object)) { + $ext = $userProfile.extensions.$extName + $name = "$extName ".PadRight($maxNameLength, '.') + $status = if ($ext.enabled) { "Enabled" } else { "Disabled" } + $color = if ($ext.enabled) { "DarkGreen" } else { "DarkYellow" } + $type = $ext.type + Write-Host " $name $type " -NoNewline + Write-Host $status -ForegroundColor $color + } + } + + return 0 + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to show profile '$profileName'" + exception = $_ + } + Write-Host "`nFailed to show profile: $($_.Exception.Message)" -ForegroundColor DarkYellow + return -1 + } +} + +function Delete-PHP-Profile { + param($profileName) + + try { + $profilePath = "$PROFILES_PATH\$profileName.json" + + if (-not (Test-Path $profilePath)) { + Write-Host "`nProfile '$profileName' not found." -ForegroundColor DarkYellow + return -1 + } + + $response = Read-Host "`nAre you sure you want to delete profile '$profileName'? (y/n)" + $response = $response.Trim() + + if ($response -ne "y" -and $response -ne "Y") { + Write-Host "`nDeletion cancelled." -ForegroundColor Gray + return -1 + } + + Remove-Item -Path $profilePath -Force + Write-Host "`nProfile '$profileName' deleted successfully." -ForegroundColor DarkGreen + + return 0 + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to delete profile '$profileName'" + exception = $_ + } + Write-Host "`nFailed to delete profile: $($_.Exception.Message)" -ForegroundColor DarkYellow + return -1 + } +} + +function Export-PHP-Profile { + param($profileName, $exportPath = $null) + + try { + $profilePath = "$PROFILES_PATH\$profileName.json" + + if (-not (Test-Path $profilePath)) { + Write-Host "`nProfile '$profileName' not found." -ForegroundColor DarkYellow + return -1 + } + + if (-not $exportPath) { + $exportPath = "$(Get-Location)\$profileName.json" + } + + Copy-Item -Path $profilePath -Destination $exportPath -Force + Write-Host "`nProfile '$profileName' exported to: $exportPath" -ForegroundColor DarkGreen + + return 0 + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to export profile '$profileName'" + exception = $_ + } + Write-Host "`nFailed to export profile: $($_.Exception.Message)" -ForegroundColor DarkYellow + return -1 + } +} + +function Import-PHP-Profile { + param($importPath, $profileName = $null) + + try { + if (-not (Test-Path $importPath)) { + Write-Host "`nFile not found: $importPath" -ForegroundColor DarkYellow + return -1 + } + + # Validate JSON structure + try { + $userProfile = Get-Content $importPath -Raw | ConvertFrom-Json + if (-not $userProfile.name -or -not $userProfile.settings -or -not $userProfile.extensions) { + Write-Host "`nInvalid profile format. Profile must contain 'name', 'settings', and 'extensions'." -ForegroundColor DarkYellow + return -1 + } + } catch { + Write-Host "`nInvalid JSON file: $($_.Exception.Message)" -ForegroundColor DarkYellow + return -1 + } + + # Use provided name or name from profile + $finalName = if ($profileName) { $profileName } else { $userProfile.name } + + $created = Make-Directory -path $PROFILES_PATH + if ($created -ne 0) { + Write-Host "`nFailed to create profiles directory." -ForegroundColor DarkYellow + return -1 + } + + $targetPath = "$PROFILES_PATH\$finalName.json" + + # Update profile name if different + if ($finalName -ne $userProfile.name) { + $userProfile.name = $finalName + $jsonContent = $userProfile | ConvertTo-Json -Depth 10 + Set-Content -Path $targetPath -Value $jsonContent -Encoding UTF8 + } else { + Copy-Item -Path $importPath -Destination $targetPath -Force + } + + Write-Host "`nProfile imported successfully as '$finalName'." -ForegroundColor DarkGreen + Write-Host " Use 'pvm profile load $finalName' to apply it." -ForegroundColor Gray + + return 0 + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to import profile from '$importPath'" + exception = $_ + } + Write-Host "`nFailed to import profile: $($_.Exception.Message)" -ForegroundColor DarkYellow + return -1 + } +} + + + From 3bb057af56359abf27a0a594327993e7589b35d8 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sun, 8 Feb 2026 19:39:16 +0100 Subject: [PATCH 56/69] Convert seconds to single --- src/functions/helpers.ps1 | 4 ++++ tests/helpers.tests.ps1 | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index c3d7745..d06bc73 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -351,6 +351,10 @@ function Format-Seconds { param ($totalSeconds) try { + if ($totalSeconds -ne $null) { + $totalSeconds = [Single] $totalSeconds + } + if ($null -eq $totalSeconds -or $totalSeconds -lt 0) { $totalSeconds = 0 } diff --git a/tests/helpers.tests.ps1 b/tests/helpers.tests.ps1 index 8ad71c9..80b7f68 100644 --- a/tests/helpers.tests.ps1 +++ b/tests/helpers.tests.ps1 @@ -667,7 +667,7 @@ Describe "Format-Seconds" { It "Handles null input" { $result = Format-Seconds -totalSeconds $null - $result | Should -Be 0 + $result | Should -Be "0s" } It "Handles string input that can be converted" { From 26d7b6c10b580aaabbab977cfd62be1fdd8eca85 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sun, 8 Feb 2026 19:39:55 +0100 Subject: [PATCH 57/69] Add arch and buildtype to xdebug fetched list items --- src/actions/ini.ps1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/actions/ini.ps1 b/src/actions/ini.ps1 index 3f92a37..3f692c7 100644 --- a/src/actions/ini.ps1 +++ b/src/actions/ini.ps1 @@ -31,7 +31,16 @@ function Get-XDebug-FROM-URL { if ($_.href -match "php_xdebug-([^-]+)") { $xDebugVersion = $matches[1] } - $formattedList += @{ href = $_.href; version = $version; xDebugVersion = $xDebugVersion; fileName = $fileName; outerHTML = $_.outerHTML } + + $formattedList += @{ + href = $_.href + version = $version + xDebugVersion = $xDebugVersion; + arch = if ($fileName -match '(x86_64|x64)(?=\.dll$)') { 'x64' } else { 'x86' } + buildType = if ($fileName -match '-nts-') { 'NTS' } else { 'TS' } + fileName = $fileName; + outerHTML = $_.outerHTML + } } return $formattedList From ae1f8eaf4d3d1298d9ba6beb0cec7011feb4d8cc Mon Sep 17 00:00:00 2001 From: Driss B Date: Sun, 8 Feb 2026 19:41:46 +0100 Subject: [PATCH 58/69] Sort xdebug list by type and arch --- src/actions/ini.ps1 | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/actions/ini.ps1 b/src/actions/ini.ps1 index 3f692c7..3132d5b 100644 --- a/src/actions/ini.ps1 +++ b/src/actions/ini.ps1 @@ -796,7 +796,6 @@ function Install-XDebug-Extension { try { $currentVersion = (Get-Current-PHP-Version).version -replace '^(\d+\.\d+)\..*$', '$1' $xDebugList = Get-XDebug-FROM-URL -url $XDEBUG_HISTORICAL_URL -version $currentVersion -arch $arch - $xDebugList = $xDebugList | Sort-Object { [version]$_.xDebugVersion } -Descending if ($null -eq $xDebugList -or $xDebugList.Count -eq 0) { Write-Host "`nNo match was found, check the '$LOG_ERROR_PATH' for any potentiel errors" @@ -827,14 +826,25 @@ function Install-XDebug-Extension { } }; Descending = $true } | ForEach-Object { - $xDebugListGrouped[$_.Name] = $_.Group + $sortedGroup = $_.Group | Sort-Object ` + @{ Expression = { $_.buildType -eq 'NTS' }; Descending = $true }, + @{ Expression = { + switch ($_.arch) { + 'x86_64' { 2 } + 'x64' { 2 } + 'x86' { 1 } + default { 0 } + } + }; Descending = $true } + + $xDebugListGrouped[$_.Name] = $sortedGroup } $index = 0 $xDebugListGrouped.GetEnumerator() | ForEach-Object { - Write-Host "`n$($_.Key)" + Write-Host "`nXDebug $($_.Key)" $_.Value | ForEach-Object { - $text = ($_.outerHTML -replace '<.*?>|.zip','').Trim() + $text = "PHP XDebug $($_.version) $($_.buildType) $($_.arch)" Write-Host " [$index] $text" $index++ } From 90101db29ed394a58bf50af210be1d180f0d0b68 Mon Sep 17 00:00:00 2001 From: Driss B Date: Sun, 8 Feb 2026 20:14:52 +0100 Subject: [PATCH 59/69] Fix current.tests.ps1 --- tests/current.tests.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/current.tests.ps1 b/tests/current.tests.ps1 index 65d75be..f1c38ec 100644 --- a/tests/current.tests.ps1 +++ b/tests/current.tests.ps1 @@ -262,8 +262,14 @@ Describe "Get-Current-PHP-Version Function Tests" { } It "Should call Get-PHP-Status with correct path" { - Mock Is-Directory-Exists { return $true } # Act + Mock Get-PHPInstallInfo {@{ + Version = '8.2.0' + Arch = 'x64' + BuildType = 'ts' + InstallPath = 'C:\php\8.2.0' + }} + Mock Is-Directory-Exists { return $true } $result = Get-Current-PHP-Version # Assert From 31a5aac3803974385ccb1295a1fcfd5ec172d7fc Mon Sep 17 00:00:00 2001 From: Driss B Date: Mon, 9 Feb 2026 13:58:43 +0100 Subject: [PATCH 60/69] Add tests covering missing paths for Format-NiceTimestamp --- tests/log.tests.ps1 | 277 +++++++++++++++++++++++++------------------- 1 file changed, 156 insertions(+), 121 deletions(-) diff --git a/tests/log.tests.ps1 b/tests/log.tests.ps1 index 881e0c8..e8a82b2 100644 --- a/tests/log.tests.ps1 +++ b/tests/log.tests.ps1 @@ -1,121 +1,156 @@ - -. "$PSScriptRoot\..\src\actions\log.ps1" - -Describe "Format-NiceTimestamp" { - It "returns 'just now' for current timestamp" { - $now = Get-Date - $result = Format-NiceTimestamp $now.ToString("yyyy-MM-dd HH:mm:ss") - - $result.Relative | Should -Be "just now" - } - - It "returns '1 minute ago' for 1 minute old timestamp" { - $ts = (Get-Date).AddMinutes(-1) - $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") - - $result.Relative | Should -Be "1 minute ago" - } - - It "returns 'yesterday' for 1 day old timestamp" { - $ts = (Get-Date).AddDays(-1) - $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") - - $result.Relative | Should -Be "yesterday" - } - - It "returns '2 weeks ago' for 15 days old timestamp" { - $ts = (Get-Date).AddDays(-15) - $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") - - $result.Relative | Should -Be "2 weeks ago" - } - - It "returns '1 month ago' for ~35 days old timestamp" { - $ts = (Get-Date).AddDays(-35) - $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") - - $result.Relative | Should -Be "1 month ago" - } - - It "handles invalid timestamp input gracefully" { - $result = Format-NiceTimestamp "not-a-date" - - $result.Date | Should -Be "not-a-date" - $result.Time | Should -Be "" - $result.Relative | Should -Be "" - } -} - -Describe "Show-Log" { - BeforeAll { - $global:DEFAULT_LOG_PAGE_SIZE = 3 - $global:LOG_ERROR_PATH = "TestDrive:\logs\error.log" - New-Item -ItemType Directory -Path (Split-Path $LOG_ERROR_PATH) -Force | Out-Null - Mock Write-Host {} - - @' --------------------------- -[2025-08-23 14:38:48] Test log entry 1 : -Message: Issue 1 -Position: At D:\Code\Tools\pvm\file.ps1:10 char:9 -+ throw "Issue $limit" -+ ~~~~~~~~~~~~~~~~~~~~ - --------------------------- -[2025-08-23 14:38:48] Test log entry 0 : -Message: Issue 0 -Position: At D:\Code\Tools\pvm\file.ps1:10 char:9 -+ throw "Issue $limit" -+ ~~~~~~~~~~~~~~~~~~~~ -'@ | Set-Content $LOG_ERROR_PATH - } - - It "returns -1 for invalid page size (non-numeric)" { - $result = Show-Log -pageSize "abc" - - $result | Should -Be -1 - Assert-MockCalled Write-Host -Times 1 -ParameterFilter { - $Object -eq "`nInvalid page size: abc" - } - } - - It "returns -1 for invalid page size (zero)" { - $result = Show-Log -pageSize 0 - - $result | Should -Be -1 - Assert-MockCalled Write-Host -Times 1 -ParameterFilter { - $Object -eq "`nPage size must be a positive integer." - } - } - - It "returns -1 for invalid page size (negative number)" { - $result = Show-Log -pageSize -5 - - $result | Should -Be -1 - Assert-MockCalled Write-Host -Times 1 -ParameterFilter { - $Object -eq "`nPage size must be a positive integer." - } - } - - It "parses log file and returns 0 for valid page size" { - # Suppress screen clearing and key reading - Mock Clear-Host {} - Mock Get-ConsoleKey { [PSCustomObject]@{ Key = "Q" } } - - $result = Show-Log -pageSize 1 - - $result | Should -Be 0 - } - - It "returns -1 if log file is missing" { - Mock Test-Path { $false } - - $result = Show-Log -pageSize 1 - - $result | Should -Be -1 - Assert-MockCalled Write-Host -Times 1 -ParameterFilter { - $Object -eq "`nLog file not found: $LOG_ERROR_PATH" - } - } -} - + +. "$PSScriptRoot\..\src\actions\log.ps1" + +Describe "Format-NiceTimestamp" { + It "returns 'just now' for current timestamp" { + $now = Get-Date + $result = Format-NiceTimestamp $now.ToString("yyyy-MM-dd HH:mm:ss") + + $result.Relative | Should -Be "just now" + } + + It "returns '1 minute ago' for 1 minute old timestamp" { + $ts = (Get-Date).AddMinutes(-1) + $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") + + $result.Relative | Should -Be "1 minute ago" + } + + It "returns 'X minutes ago for more than 1 minute old timestamp" { + $ts = (Get-Date).AddMinutes(-30) + $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") + + $result.Relative | Should -Be "30 minutes ago" + } + + It "returns '1 hour ago for 1 hour old timestamp" { + $ts = (Get-Date).AddHours(-1) + $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") + + $result.Relative | Should -Be "1 hour ago" + } + + It "returns 'X hours ago for more than 1 hour old timestamp" { + $ts = (Get-Date).AddHours(-5) + $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") + + $result.Relative | Should -Be "5 hours ago" + } + + It "returns 'yesterday' for 1 day old timestamp" { + $ts = (Get-Date).AddDays(-1) + $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") + + $result.Relative | Should -Be "yesterday" + } + + It "returns 'X days ago for more than 1 day old timestamp" { + $ts = (Get-Date).AddDays(-5) + $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") + + $result.Relative | Should -Be "5 days ago" + } + + It "returns '1 week ago' for 7 days old timestamp" { + $ts = (Get-Date).AddDays(-7) + $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") + + $result.Relative | Should -Be "1 week ago" + } + + It "returns '2 weeks ago' for 15 days old timestamp" { + $ts = (Get-Date).AddDays(-15) + $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") + + $result.Relative | Should -Be "2 weeks ago" + } + + It "returns '1 month ago' for ~35 days old timestamp" { + $ts = (Get-Date).AddDays(-35) + $result = Format-NiceTimestamp $ts.ToString("yyyy-MM-dd HH:mm:ss") + + $result.Relative | Should -Be "1 month ago" + } + + It "handles invalid timestamp input gracefully" { + $result = Format-NiceTimestamp "not-a-date" + + $result.Date | Should -Be "not-a-date" + $result.Time | Should -Be "" + $result.Relative | Should -Be "" + } +} + +Describe "Show-Log" { + BeforeAll { + $global:DEFAULT_LOG_PAGE_SIZE = 3 + $global:LOG_ERROR_PATH = "TestDrive:\logs\error.log" + New-Item -ItemType Directory -Path (Split-Path $LOG_ERROR_PATH) -Force | Out-Null + Mock Write-Host {} + + @' +-------------------------- +[2025-08-23 14:38:48] Test log entry 1 : +Message: Issue 1 +Position: At D:\Code\Tools\pvm\file.ps1:10 char:9 ++ throw "Issue $limit" ++ ~~~~~~~~~~~~~~~~~~~~ + +-------------------------- +[2025-08-23 14:38:48] Test log entry 0 : +Message: Issue 0 +Position: At D:\Code\Tools\pvm\file.ps1:10 char:9 ++ throw "Issue $limit" ++ ~~~~~~~~~~~~~~~~~~~~ +'@ | Set-Content $LOG_ERROR_PATH + } + + It "returns -1 for invalid page size (non-numeric)" { + $result = Show-Log -pageSize "abc" + + $result | Should -Be -1 + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { + $Object -eq "`nInvalid page size: abc" + } + } + + It "returns -1 for invalid page size (zero)" { + $result = Show-Log -pageSize 0 + + $result | Should -Be -1 + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { + $Object -eq "`nPage size must be a positive integer." + } + } + + It "returns -1 for invalid page size (negative number)" { + $result = Show-Log -pageSize -5 + + $result | Should -Be -1 + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { + $Object -eq "`nPage size must be a positive integer." + } + } + + It "parses log file and returns 0 for valid page size" { + # Suppress screen clearing and key reading + Mock Clear-Host {} + Mock Get-ConsoleKey { [PSCustomObject]@{ Key = "Q" } } + + $result = Show-Log -pageSize 1 + + $result | Should -Be 0 + } + + It "returns -1 if log file is missing" { + Mock Test-Path { $false } + + $result = Show-Log -pageSize 1 + + $result | Should -Be -1 + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { + $Object -eq "`nLog file not found: $LOG_ERROR_PATH" + } + } +} + From 8b780400e29a15c0a3308e4f06553c44295b40cf Mon Sep 17 00:00:00 2001 From: Driss B Date: Mon, 9 Feb 2026 13:59:05 +0100 Subject: [PATCH 61/69] Add tests covering missing paths for Show-Log --- src/actions/log.ps1 | 462 ++++++++++++++++++++++---------------------- tests/log.tests.ps1 | 8 + 2 files changed, 239 insertions(+), 231 deletions(-) diff --git a/src/actions/log.ps1 b/src/actions/log.ps1 index ca11c11..170ce8e 100644 --- a/src/actions/log.ps1 +++ b/src/actions/log.ps1 @@ -1,232 +1,232 @@ - -function Get-ConsoleKey { - param([bool]$intercept = $true) - return [System.Console]::ReadKey($intercept) -} - -function Format-NiceTimestamp { - param($timestamp) - - try { - $dateTime = [DateTime]::Parse($timestamp) - $now = Get-Date - $timeSpan = $now - $dateTime - - # Format the date part - $dateStr = $dateTime.ToString("dd MMMM") - $timeStr = $dateTime.ToString("HH:mm:ss") - - # Calculate relative time - $relativeTime = "" - if ($timeSpan.Days -eq 0) { - if ($timeSpan.Hours -eq 0) { - if ($timeSpan.Minutes -eq 0) { - $relativeTime = "just now" - } elseif ($timeSpan.Minutes -eq 1) { - $relativeTime = "1 minute ago" - } else { - $relativeTime = "$($timeSpan.Minutes) minutes ago" - } - } elseif ($timeSpan.Hours -eq 1) { - $relativeTime = "1 hour ago" - } else { - $relativeTime = "$($timeSpan.Hours) hours ago" - } - } elseif ($timeSpan.Days -eq 1) { - $relativeTime = "yesterday" - } elseif ($timeSpan.Days -lt 7) { - $relativeTime = "$($timeSpan.Days) days ago" - } elseif ($timeSpan.Days -lt 30) { - $weeks = [Math]::Floor($timeSpan.Days / 7) - if ($weeks -eq 1) { - $relativeTime = "1 week ago" - } else { - $relativeTime = "$weeks weeks ago" - } - } else { - $months = [Math]::Floor($timeSpan.Days / 30) - if ($months -eq 1) { - $relativeTime = "1 month ago" - } else { - $relativeTime = "$months months ago" - } - } - - return @{ - Date = $dateStr - Time = $timeStr - Relative = $relativeTime - DateTime = $dateTime - } - } catch { - return @{ - Date = $timestamp - Time = "" - Relative = "" - DateTime = Get-Date - } - } -} - -function Show-Log { - param($pageSize = $DEFAULT_LOG_PAGE_SIZE, $term = $null) - - try { - if ($pageSize -notmatch '^-?\d+$') { - Write-Host "`nInvalid page size: $pageSize" -ForegroundColor Red - return -1 - } - - $pageSize = [int]$pageSize - if ($pageSize -le 0) { - Write-Host "`nPage size must be a positive integer." -ForegroundColor Red - return -1 - } - - # Check if log file exists - if (-not (Test-Path $LOG_ERROR_PATH)) { - Write-Host "`nLog file not found: $LOG_ERROR_PATH" -ForegroundColor Red - return -1 - } - - # Read the entire log file - $logContent = Get-Content $LOG_ERROR_PATH -Raw - - # Split by the separator and filter out empty entries - $logEntries = $logContent -split '-{26}' | Where-Object { $_.Trim() -ne '' } - - # Parse each entry into objects - $parsedEntries = @() - foreach ($entry in $logEntries) { - if ($term -and ($entry -notmatch [regex]::Escape($term))) { - continue - } - $lines = $entry.Trim() -split "`n" - if ($lines.Count -ge 1) { # Changed from 2 to 1 to catch single-line entries - # Extract timestamp from first line - $firstLine = $lines[0].Trim() - if ($firstLine -match '^\[(.+?)\]\s*(.+?)$') { - $timestamp = $matches[1] - $firstMessage = $matches[2] - - # Get remaining content - $remainingContent = @() - if ($lines.Count -gt 1) { - $remainingContent = $lines[1..($lines.Count-1)] | Where-Object { $_.Trim() -ne '' } - } - - # Combine first message with remaining content - $fullMessage = @($firstMessage) + $remainingContent | Where-Object { $_.Trim() -ne '' } - $fullMessageText = ($fullMessage -join "`n").Trim() - - # Parse structured error information if present - $errorMessage = $null - $positionDetail = $null - $header = $null - - if ($fullMessageText -match '(?s)Message:\s*(.+?)\s*\nPosition:\s*(.*)') { - $errorMessage = $matches[1].Trim() - $positionDetail = $matches[2].Trim() - $header = $firstMessage.Trim() - } - - # Format the timestamp nicely - $niceTime = Format-NiceTimestamp $timestamp - - $parsedEntries += [PSCustomObject]@{ - Timestamp = $timestamp - Message = $fullMessageText - ErrorMessage = $errorMessage - PositionDetail = $positionDetail - Header = $header - RawEntry = $entry.Trim() - NiceTime = $niceTime - } - } - } - } - - # Reverse the order to show most recent first - $reversedEntries = $parsedEntries[-1..-($parsedEntries.Length)] - - if ($reversedEntries.Count -eq 0) { - Write-Host "`nNo log entries found." -ForegroundColor Yellow - return - } - - # Display entries with pagination - $currentIndex = 0 - $totalEntries = $reversedEntries.Count - - while ($currentIndex -lt $totalEntries) { - # Clear screen for cleaner display - Clear-Host - - # Show header - Write-Host "`n=== PVM Log Viewer ===" -ForegroundColor Cyan - Write-Host "`nShowing entries $($currentIndex + 1)-$([Math]::Min($currentIndex + $PageSize, $totalEntries)) of $totalEntries (most recent first)`n" -ForegroundColor Green - - # Display current page of entries - $endIndex = [Math]::Min($currentIndex + $PageSize - 1, $totalEntries - 1) - - Write-Host ("-" * 80) -ForegroundColor DarkGray - for ($i = $currentIndex; $i -le $endIndex; $i++) { - $entry = $reversedEntries[$i] - - # Display structured error format - Write-Host "Header : " -NoNewline -ForegroundColor Gray - Write-Host "$($entry.Header)" -ForegroundColor White - - Write-Host "Message : " -NoNewline -ForegroundColor Gray - # Handle multi-line error messages with proper indentation (23 spaces to align with "Message :") - $errorLines = $entry.ErrorMessage -split "`n" - foreach ($errorLine in $errorLines) { - if ($errorLine.Trim() -ne '') { - Write-Host "$($errorLine)" -ForegroundColor White - } - } - - # Display entry with nice formatting - Write-Host "When : " -NoNewline -ForegroundColor Gray - Write-Host "$($entry.NiceTime.Date) @ $($entry.NiceTime.Time) " -NoNewline -ForegroundColor White - Write-Host "($($entry.NiceTime.Relative))" -ForegroundColor DarkGray - - Write-Host "Where : " -NoNewline -ForegroundColor Gray - Write-Host "$($entry.PositionDetail)" -ForegroundColor White - - Write-Host ("-" * 80) -ForegroundColor DarkGray - } - - $currentIndex += $PageSize - # Show navigation prompt if there are more entries - if ($currentIndex -lt $totalEntries) { - Write-Host "`nPress Left/Up arrow for previous page, Right/Down arrow, [Enter] or [Space] for next page, [Q] to quit: " -NoNewline -ForegroundColor Yellow - - $key = Get-ConsoleKey - - switch ($key.Key) { - { $_ -in @("LeftArrow", "UpArrow") } { $currentIndex = [Math]::Max(0, $currentIndex - (2 * $PageSize)) } - { $_ -in @("RightArrow", "DownArrow", "Enter", "Spacebar") } { continue } - { $_ -in @('q', 'Q') } { return 0 } - default { $currentIndex -= $PageSize } - } - } else { - Write-Host "End of log reached. Press Left/Up arrow to go back or any other key to exit..." -ForegroundColor Green - $key = Get-ConsoleKey - if ($key.Key -in @("LeftArrow", "UpArrow")) { - # Go back one page from the end - $currentIndex = [Math]::Max(0, $currentIndex - (2 * $PageSize)) - } - } - } - - Clear-Host - return 0 - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to show log" - exception = $_ - } - return -1 - } + +function Get-ConsoleKey { + param([bool]$intercept = $true) + return [System.Console]::ReadKey($intercept) +} + +function Format-NiceTimestamp { + param($timestamp) + + try { + $dateTime = [DateTime]::Parse($timestamp) + $now = Get-Date + $timeSpan = $now - $dateTime + + # Format the date part + $dateStr = $dateTime.ToString("dd MMMM") + $timeStr = $dateTime.ToString("HH:mm:ss") + + # Calculate relative time + $relativeTime = "" + if ($timeSpan.Days -eq 0) { + if ($timeSpan.Hours -eq 0) { + if ($timeSpan.Minutes -eq 0) { + $relativeTime = "just now" + } elseif ($timeSpan.Minutes -eq 1) { + $relativeTime = "1 minute ago" + } else { + $relativeTime = "$($timeSpan.Minutes) minutes ago" + } + } elseif ($timeSpan.Hours -eq 1) { + $relativeTime = "1 hour ago" + } else { + $relativeTime = "$($timeSpan.Hours) hours ago" + } + } elseif ($timeSpan.Days -eq 1) { + $relativeTime = "yesterday" + } elseif ($timeSpan.Days -lt 7) { + $relativeTime = "$($timeSpan.Days) days ago" + } elseif ($timeSpan.Days -lt 30) { + $weeks = [Math]::Floor($timeSpan.Days / 7) + if ($weeks -eq 1) { + $relativeTime = "1 week ago" + } else { + $relativeTime = "$weeks weeks ago" + } + } else { + $months = [Math]::Floor($timeSpan.Days / 30) + if ($months -eq 1) { + $relativeTime = "1 month ago" + } else { + $relativeTime = "$months months ago" + } + } + + return @{ + Date = $dateStr + Time = $timeStr + Relative = $relativeTime + DateTime = $dateTime + } + } catch { + return @{ + Date = $timestamp + Time = "" + Relative = "" + DateTime = Get-Date + } + } +} + +function Show-Log { + param($pageSize = $DEFAULT_LOG_PAGE_SIZE, $term = $null) + + try { + if ($pageSize -notmatch '^-?\d+$') { + Write-Host "`nInvalid page size: $pageSize" -ForegroundColor Red + return -1 + } + + $pageSize = [int]$pageSize + if ($pageSize -le 0) { + Write-Host "`nPage size must be a positive integer." -ForegroundColor Red + return -1 + } + + # Check if log file exists + if (-not (Test-Path $LOG_ERROR_PATH)) { + Write-Host "`nLog file not found: $LOG_ERROR_PATH" -ForegroundColor Red + return -1 + } + + # Read the entire log file + $logContent = Get-Content $LOG_ERROR_PATH -Raw + + # Split by the separator and filter out empty entries + $logEntries = $logContent -split '-{26}' | Where-Object { $_.Trim() -ne '' } + + # Parse each entry into objects + $parsedEntries = @() + foreach ($entry in $logEntries) { + if ($term -and ($entry -notmatch [regex]::Escape($term))) { + continue + } + $lines = $entry.Trim() -split "`n" + if ($lines.Count -ge 1) { # Changed from 2 to 1 to catch single-line entries + # Extract timestamp from first line + $firstLine = $lines[0].Trim() + if ($firstLine -match '^\[(.+?)\]\s*(.+?)$') { + $timestamp = $matches[1] + $firstMessage = $matches[2] + + # Get remaining content + $remainingContent = @() + if ($lines.Count -gt 1) { + $remainingContent = $lines[1..($lines.Count-1)] | Where-Object { $_.Trim() -ne '' } + } + + # Combine first message with remaining content + $fullMessage = @($firstMessage) + $remainingContent | Where-Object { $_.Trim() -ne '' } + $fullMessageText = ($fullMessage -join "`n").Trim() + + # Parse structured error information if present + $errorMessage = $null + $positionDetail = $null + $header = $null + + if ($fullMessageText -match '(?s)Message:\s*(.+?)\s*\nPosition:\s*(.*)') { + $errorMessage = $matches[1].Trim() + $positionDetail = $matches[2].Trim() + $header = $firstMessage.Trim() + } + + # Format the timestamp nicely + $niceTime = Format-NiceTimestamp $timestamp + + $parsedEntries += [PSCustomObject]@{ + Timestamp = $timestamp + Message = $fullMessageText + ErrorMessage = $errorMessage + PositionDetail = $positionDetail + Header = $header + RawEntry = $entry.Trim() + NiceTime = $niceTime + } + } + } + } + + # Reverse the order to show most recent first + $reversedEntries = $parsedEntries[-1..-($parsedEntries.Length)] + + if ($reversedEntries.Count -eq 0) { + Write-Host "`nNo log entries found." -ForegroundColor Yellow + return -1 + } + + # Display entries with pagination + $currentIndex = 0 + $totalEntries = $reversedEntries.Count + + while ($currentIndex -lt $totalEntries) { + # Clear screen for cleaner display + Clear-Host + + # Show header + Write-Host "`n=== PVM Log Viewer ===" -ForegroundColor Cyan + Write-Host "`nShowing entries $($currentIndex + 1)-$([Math]::Min($currentIndex + $PageSize, $totalEntries)) of $totalEntries (most recent first)`n" -ForegroundColor Green + + # Display current page of entries + $endIndex = [Math]::Min($currentIndex + $PageSize - 1, $totalEntries - 1) + + Write-Host ("-" * 80) -ForegroundColor DarkGray + for ($i = $currentIndex; $i -le $endIndex; $i++) { + $entry = $reversedEntries[$i] + + # Display structured error format + Write-Host "Header : " -NoNewline -ForegroundColor Gray + Write-Host "$($entry.Header)" -ForegroundColor White + + Write-Host "Message : " -NoNewline -ForegroundColor Gray + # Handle multi-line error messages with proper indentation (23 spaces to align with "Message :") + $errorLines = $entry.ErrorMessage -split "`n" + foreach ($errorLine in $errorLines) { + if ($errorLine.Trim() -ne '') { + Write-Host "$($errorLine)" -ForegroundColor White + } + } + + # Display entry with nice formatting + Write-Host "When : " -NoNewline -ForegroundColor Gray + Write-Host "$($entry.NiceTime.Date) @ $($entry.NiceTime.Time) " -NoNewline -ForegroundColor White + Write-Host "($($entry.NiceTime.Relative))" -ForegroundColor DarkGray + + Write-Host "Where : " -NoNewline -ForegroundColor Gray + Write-Host "$($entry.PositionDetail)" -ForegroundColor White + + Write-Host ("-" * 80) -ForegroundColor DarkGray + } + + $currentIndex += $PageSize + # Show navigation prompt if there are more entries + if ($currentIndex -lt $totalEntries) { + Write-Host "`nPress Left/Up arrow for previous page, Right/Down arrow, [Enter] or [Space] for next page, [Q] to quit: " -NoNewline -ForegroundColor Yellow + + $key = Get-ConsoleKey + + switch ($key.Key) { + { $_ -in @("LeftArrow", "UpArrow") } { $currentIndex = [Math]::Max(0, $currentIndex - (2 * $PageSize)) } + { $_ -in @("RightArrow", "DownArrow", "Enter", "Spacebar") } { continue } + { $_ -in @('q', 'Q') } { return 0 } + default { $currentIndex -= $PageSize } + } + } else { + Write-Host "End of log reached. Press Left/Up arrow to go back or any other key to exit..." -ForegroundColor Green + $key = Get-ConsoleKey + if ($key.Key -in @("LeftArrow", "UpArrow")) { + # Go back one page from the end + $currentIndex = [Math]::Max(0, $currentIndex - (2 * $PageSize)) + } + } + } + + Clear-Host + return 0 + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to show log" + exception = $_ + } + return -1 + } } \ No newline at end of file diff --git a/tests/log.tests.ps1 b/tests/log.tests.ps1 index e8a82b2..821718e 100644 --- a/tests/log.tests.ps1 +++ b/tests/log.tests.ps1 @@ -142,6 +142,14 @@ Position: At D:\Code\Tools\pvm\file.ps1:10 char:9 $result | Should -Be 0 } + It "returns -1 if no entries found" { + "" | Set-Content $LOG_ERROR_PATH + + $result = Show-Log -pageSize 1 + + $result | Should -Be -1 + } + It "returns -1 if log file is missing" { Mock Test-Path { $false } From 8a748da207896ae63e9e4c628c26d0083beb317c Mon Sep 17 00:00:00 2001 From: Driss B Date: Mon, 9 Feb 2026 14:29:14 +0100 Subject: [PATCH 62/69] Print xdebug compiler version --- src/actions/ini.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/actions/ini.ps1 b/src/actions/ini.ps1 index 3132d5b..ab7aacc 100644 --- a/src/actions/ini.ps1 +++ b/src/actions/ini.ps1 @@ -37,7 +37,8 @@ function Get-XDebug-FROM-URL { version = $version xDebugVersion = $xDebugVersion; arch = if ($fileName -match '(x86_64|x64)(?=\.dll$)') { 'x64' } else { 'x86' } - buildType = if ($fileName -match '-nts-') { 'NTS' } else { 'TS' } + buildType = if ($fileName -match '(?i)(?:^|-)nts(?:-|\.dll$)') { 'NTS' } else { 'TS' } + compiler = if ($fileName -match '(?i)\b(vs|vc)\d+\b') { $matches[0].ToUpper() } else { 'unknown' } fileName = $fileName; outerHTML = $_.outerHTML } @@ -844,7 +845,7 @@ function Install-XDebug-Extension { $xDebugListGrouped.GetEnumerator() | ForEach-Object { Write-Host "`nXDebug $($_.Key)" $_.Value | ForEach-Object { - $text = "PHP XDebug $($_.version) $($_.buildType) $($_.arch)" + $text = "PHP XDebug $($_.version) $($_.compiler) $($_.buildType) $($_.arch)" Write-Host " [$index] $text" $index++ } From 32534a0c72918ba1052e516814c56cc41164a206 Mon Sep 17 00:00:00 2001 From: Driss B Date: Mon, 9 Feb 2026 22:07:23 +0100 Subject: [PATCH 63/69] Added helper Get-OrUpdateCache to refactor cache fetching process w/ tests --- src/functions/helpers.ps1 | 21 ++++++++++++++++++++ tests/helpers.tests.ps1 | 42 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index d06bc73..047ed64 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -63,6 +63,27 @@ function Cache-Data { } } +function Get-OrUpdateCache { + param ($cacheFileName, $compute, $depth = 3) + + $useCache = Can-Use-Cache -cacheFileName $cacheFileName + + if ($useCache) { + $data = Get-Data-From-Cache -cacheFileName $cacheFileName + if ($null -ne $data -and $data.Count -gt 0) { + return $data + } + } + + $data = & $compute + + if ($null -ne $data) { + $cached = Cache-Data -cacheFileName $cacheFileName -data $data -depth $depth + } + + return $data +} + function Get-All-Subdirectories { param ($path) try { diff --git a/tests/helpers.tests.ps1 b/tests/helpers.tests.ps1 index 80b7f68..643cdaa 100644 --- a/tests/helpers.tests.ps1 +++ b/tests/helpers.tests.ps1 @@ -784,6 +784,48 @@ Describe "Can-Use-Cache" { } } +Describe "Get-OrUpdateCache" { + It "Reads from cache first" { + function Example { return @{} } + Mock Example { return @{} } + Mock Can-Use-Cache { return $true } + Mock Cache-Data { return 0 } + Mock Get-Data-From-Cache { + return @{ + 'Archives' = @('php-8.1.0-Win32-x64.zip') + 'Releases' = @('php-8.2.0-Win32-x64.zip') + } + } + + $result = Get-OrUpdateCache -cacheFileName "file.json" -compute { + Example + } + + Assert-MockCalled Get-Data-From-Cache -Exactly 1 + Assert-MockCalled Example -Exactly 0 + Assert-MockCalled Cache-Data -Exactly 0 + } + + It "Runs the passed command when can't read from cache" { + function Example { return @{} } + Mock Example { + return @{ + 'Archives' = @('php-8.1.0-Win32-x64.zip') + 'Releases' = @('php-8.2.0-Win32-x64.zip') + } + } + Mock Cache-Data { return 0 } + Mock Can-Use-Cache { return $false } + + $result = Get-OrUpdateCache -cacheFileName "file.json" -compute { + Example + } + + Assert-MockCalled Example -Exactly 1 + Assert-MockCalled Cache-Data -Exactly 1 + } +} + Describe "Resolve-Arch" { Context "When searching in arguments" { It "Returns x86 when x86 is in arguments" { From 9159a057d9cd6f4b8301cf883cd4b35185b085a0 Mon Sep 17 00:00:00 2001 From: Driss B Date: Mon, 9 Feb 2026 22:08:44 +0100 Subject: [PATCH 64/69] Refactor cache fetching with Get-OrUpdateCache --- src/actions/common.ps1 | 19 ++++++++----------- src/actions/list.ps1 | 20 ++++++++------------ tests/common.tests.ps1 | 18 ++++++++++++++++++ tests/list.tests.ps1 | 12 ++++++++++-- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/actions/common.ps1 b/src/actions/common.ps1 index 467059e..6d0708b 100644 --- a/src/actions/common.ps1 +++ b/src/actions/common.ps1 @@ -66,27 +66,24 @@ function Get-Installed-PHP-Versions-From-Directory { $installedVersions = ($installedVersions | Sort-Object { [version]$_.Version }) + $cached = Cache-Data -cacheFileName "installed_php_versions" -data $installedVersions -depth 1 + return $installedVersions } function Get-Installed-PHP-Versions { param ($arch = $null) try { - $useCache = Can-Use-Cache -cacheFileName 'installed_php_versions' + $installedVersions = Get-OrUpdateCache -cacheFileName "installed_php_versions" -depth 1 -compute { + Get-Installed-PHP-Versions-From-Directory + } - if ($useCache) { - $installedVersions = Get-Data-From-Cache -cacheFileName "installed_php_versions" - if (-not $installedVersions -or $installedVersions.Count -eq 0) { - $installedVersions = Get-Installed-PHP-Versions-From-Directory - $cached = Cache-Data -cacheFileName "installed_php_versions" -data $installedVersions -depth 1 - } - } else { - $installedVersions = Get-Installed-PHP-Versions-From-Directory - $cached = Cache-Data -cacheFileName "installed_php_versions" -data $installedVersions -depth 1 + if ($null -eq $installedVersions) { + return @() } if ($arch) { - $installedVersions = $installedVersions | Where-Object { $_.Arch -eq $arch } + $installedVersions = $installedVersions | Where-Object { $_.Arch -eq $arch } | Sort-Object { [version]$_.Version } } return $installedVersions diff --git a/src/actions/list.ps1 b/src/actions/list.ps1 index d03d41f..d3ea415 100644 --- a/src/actions/list.ps1 +++ b/src/actions/list.ps1 @@ -55,20 +55,16 @@ function Get-From-Source { function Get-PHP-List-To-Install { try { - $fetchedVersionsGrouped = @{} - $useCache = Can-Use-Cache -cacheFileName 'available_php_versions' - - if ($useCache) { - $fetchedVersionsGrouped = Get-Data-From-Cache -cacheFileName "available_php_versions" - if (-not $fetchedVersionsGrouped -or $fetchedVersionsGrouped.Count -eq 0) { - $fetchedVersionsGrouped = Get-From-Source - $fetchedVersionsGrouped = [pscustomobject] $fetchedVersionsGrouped - } - } else { - $fetchedVersionsGrouped = Get-From-Source - $fetchedVersionsGrouped = [pscustomobject] $fetchedVersionsGrouped + $fetchedVersionsGrouped = Get-OrUpdateCache -cacheFileName "available_php_versions" -compute { + Get-From-Source + } + + if (-not $fetchedVersionsGrouped) { + return @{} } + $fetchedVersionsGrouped = [pscustomobject] $fetchedVersionsGrouped + return $fetchedVersionsGrouped } catch { $logged = Log-Data -data @{ diff --git a/tests/common.tests.ps1 b/tests/common.tests.ps1 index 8b974c3..c1c4504 100644 --- a/tests/common.tests.ps1 +++ b/tests/common.tests.ps1 @@ -135,6 +135,24 @@ Describe "Get-Installed-PHP-Versions" { $result[0].version | Should -Be "7.4" $result[1].version | Should -Be "8.1" } + + It "Should filter the right arch input" { + Mock Get-OrUpdateCache { + return @( + @{version = "5.6"; arch = "x64"; buildType = "nts"} + @{version = "5.6"; arch = "x86"; buildType = "nts"} + @{version = "7.4"; arch = "x64"; buildType = "nts"} + @{version = "8.0"; arch = "x64"; buildType = "nts"} + @{version = "8.0"; arch = "x86"; buildType = "nts"} + ) + } + + $result = Get-Installed-PHP-Versions -arch "x86" + + $result.Count | Should -Be 2 + $result[0].version | Should -Be "5.6" + $result[1].version | Should -Be "8.0" + } } Context "When exceptions occur" { diff --git a/tests/list.tests.ps1 b/tests/list.tests.ps1 index f0871f8..0c9726a 100644 --- a/tests/list.tests.ps1 +++ b/tests/list.tests.ps1 @@ -102,6 +102,14 @@ Describe "Get-From-Source" { Describe "Get-PHP-List-To-Install" { + It "Returns empty object when cache and/or source not working" { + Mock Get-OrUpdateCache { return $null } + + $result = Get-PHP-List-To-Install + + $result.Count | Should -Be 0 + } + It "Should read from cache" { Mock Test-Path { return $true } $timeWithinLastWeek = (Get-Date).AddHours(-160).ToString("yyyy-MM-ddTHH:mm:ss.fffffffK") @@ -116,8 +124,8 @@ Describe "Get-PHP-List-To-Install" { $result = Get-PHP-List-To-Install $result | Should -Not -BeNullOrEmpty - $result.Keys | Should -Contain 'Archives' - $result.Keys | Should -Contain 'Releases' + $result.Archives | Should -Not -BeNullOrEmpty + $result.Releases | Should -Not -BeNullOrEmpty Assert-MockCalled Get-Data-From-Cache -Exactly 1 } From 2a781c0d069d260d0db4b6fb08197b4111440b9b Mon Sep 17 00:00:00 2001 From: Driss B Date: Mon, 9 Feb 2026 22:09:17 +0100 Subject: [PATCH 65/69] Cover missing paths in current.tests.ps1 --- tests/current.tests.ps1 | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/tests/current.tests.ps1 b/tests/current.tests.ps1 index f1c38ec..052a791 100644 --- a/tests/current.tests.ps1 +++ b/tests/current.tests.ps1 @@ -224,6 +224,21 @@ Describe "Get-PHP-Status Function Tests" { $result.opcache | Should -Be $false $result.xdebug | Should -Be $false } + + It "Should handle Test-Path exceptions gracefully" { + # Arrange + Mock Test-Path { throw "Access Denied" } + Mock Log-Data { return 0 } + $testPath = "TestDrive:\php" + + # Act + $result = Get-PHP-Status -phpPath $testPath + + # Assert + Assert-MockCalled Log-Data -Times 1 + $result.opcache | Should -Be $false + $result.xdebug | Should -Be $false + } } } @@ -278,14 +293,26 @@ Describe "Get-Current-PHP-Version Function Tests" { } Context "When PHP current version path does not exist" { - BeforeEach { - # Mock Get-Item to throw an exception - Mock Get-Item { - throw "Path does not exist" - } -ParameterFilter { $Path -eq $PHP_CURRENT_VERSION_PATH } + + It "returns empty result when path does not exist" { + # Arrange + Mock Get-Item { return @{ Target = "C:\php\8.2.0" } } + Mock Is-Directory-Exists { return $false } + + # Act + $result = Get-Current-PHP-Version + + # Assert + $result.version | Should -Be $null + $result.path | Should -Be $null + $result.status.opcache | Should -Be $false + $result.status.xdebug | Should -Be $false } It "Should return null values when path does not exist" { + # Arrange + Mock Get-Item { throw "Path does not exist" } + # Act $result = Get-Current-PHP-Version @@ -298,6 +325,7 @@ Describe "Get-Current-PHP-Version Function Tests" { It "Should call Log-Data when exception occurs" { # Arrange + Mock Get-Item { throw "Path does not exist" } Mock Log-Data { return $true } # Act From 9bef158ac2eae94f57efb471fa5077eb7c43a58a Mon Sep 17 00:00:00 2001 From: Driss B Date: Tue, 10 Feb 2026 14:56:16 +0100 Subject: [PATCH 66/69] Fix log-data mock --- tests/common.tests.ps1 | 16 ++++++++-------- tests/current.tests.ps1 | 2 +- tests/list.tests.ps1 | 8 ++++---- tests/main.tests.ps1 | 6 +++--- tests/profile.tests.ps1 | 12 ++++++------ tests/setup.tests.ps1 | 4 ++-- tests/uninstall.tests.ps1 | 12 ++++++------ 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/common.tests.ps1 b/tests/common.tests.ps1 index c1c4504..afc0e75 100644 --- a/tests/common.tests.ps1 +++ b/tests/common.tests.ps1 @@ -74,7 +74,7 @@ Describe "Is-PVM-Setup" { Context "When exceptions occur" { It "Should return false and log error when Get-EnvVar-ByName throws exception" { Mock Get-EnvVar-ByName { throw "Test exception" } - Mock Log-Data { return $true } + Mock Log-Data { return 0 } $result = Is-PVM-Setup $result | Should -Be $false @@ -158,7 +158,7 @@ Describe "Get-Installed-PHP-Versions" { Context "When exceptions occur" { It "Should return empty array and log error when Get-Installed-PHP-Versions-From-Directory throws exception" { Mock Get-Installed-PHP-Versions-From-Directory { throw "Test exception" } - Mock Log-Data { return $true } + Mock Log-Data { return 0 } $result = Get-Installed-PHP-Versions $result.Count | Should -Be 0 @@ -263,7 +263,7 @@ Describe "Get-Matching-PHP-Versions" { Mock Get-Installed-PHP-Versions { return @("php7.4", "php8.0", "php8.1") } - Mock Log-Data { return $true } + Mock Log-Data { return 0 } $result = Get-Matching-PHP-Versions -version "9" $result.Count | Should -Be 0 @@ -273,7 +273,7 @@ Describe "Get-Matching-PHP-Versions" { Context "When exceptions occur" { It "Should return null and log error when Get-Installed-PHP-Versions throws exception" { Mock Get-Installed-PHP-Versions { throw "Test exception" } - Mock Log-Data { return $true } + Mock Log-Data { return 0 } $result = Get-Matching-PHP-Versions -version "8.1" $result | Should -Be $null @@ -327,7 +327,7 @@ Describe "Is-PHP-Version-Installed" { Context "When exceptions occur" { It "Should return false and log error when Get-Matching-PHP-Versions throws exception" { Mock Get-Matching-PHP-Versions { throw "Test exception" } - Mock Log-Data { return $true } + Mock Log-Data { return 0 } $result = Is-PHP-Version-Installed -version "8.1" $result | Should -Be $false @@ -401,7 +401,7 @@ Describe "Refresh-Installed-PHP-Versions-Cache" { Context "When exceptions occur" { It "Should return -1 on exception" { Mock Get-Installed-PHP-Versions-From-Directory { throw "Test exception" } - Mock Log-Data { return $true } + Mock Log-Data { return 0 } $result = Refresh-Installed-PHP-Versions-Cache $result | Should -Be -1 @@ -409,7 +409,7 @@ Describe "Refresh-Installed-PHP-Versions-Cache" { It "Should log error when exception occurs" { Mock Get-Installed-PHP-Versions-From-Directory { throw "Test exception" } - Mock Log-Data { return $true } + Mock Log-Data { return 0 } $result = Refresh-Installed-PHP-Versions-Cache @@ -423,7 +423,7 @@ Describe "Refresh-Installed-PHP-Versions-Cache" { return @(@{Version = "8.1"; Arch = "x64"; BuildType = 'NTS'}) } Mock Cache-Data { throw "Cache exception" } - Mock Log-Data { return $true } + Mock Log-Data { return 0 } $result = Refresh-Installed-PHP-Versions-Cache $result | Should -Be -1 diff --git a/tests/current.tests.ps1 b/tests/current.tests.ps1 index 052a791..b88f49d 100644 --- a/tests/current.tests.ps1 +++ b/tests/current.tests.ps1 @@ -326,7 +326,7 @@ Describe "Get-Current-PHP-Version Function Tests" { It "Should call Log-Data when exception occurs" { # Arrange Mock Get-Item { throw "Path does not exist" } - Mock Log-Data { return $true } + Mock Log-Data { return 0 } # Act $result = Get-Current-PHP-Version diff --git a/tests/list.tests.ps1 b/tests/list.tests.ps1 index 0c9726a..eadc4ea 100644 --- a/tests/list.tests.ps1 +++ b/tests/list.tests.ps1 @@ -19,7 +19,7 @@ BeforeAll { } return 0 } - Mock Log-Data { param($logPath, $message, $data) return "Logged: $message - $data" } + Mock Log-Data { param($logPath, $message, $data) return 0 } Mock Get-Source-Urls { return @{ 'releases' = $PHP_WIN_RELEASES_URL @@ -91,7 +91,7 @@ Describe "Get-From-Source" { It "Should handle web request failure" { Mock Invoke-WebRequest { throw "Network error" } - Mock Log-Data { return "Logged error" } + Mock Log-Data { return 0 } $result = Get-From-Source @@ -338,7 +338,7 @@ Describe "Get-Available-PHP-Versions" { 'Releases' = @('php-8.2.0-Win32-x64.zip') }} Mock ForEach-Object { throw "Cache error" } - Mock Log-Data { return "Logged error" } + Mock Log-Data { return 0 } $result = Get-Available-PHP-Versions @@ -423,7 +423,7 @@ Describe "Display-Installed-PHP-Versions" { It "Should handle exceptions gracefully" { Mock Get-Current-PHP-Version { throw "Error getting current version" } - Mock Log-Data { return "Logged error" } + Mock Log-Data { return 0 } { Display-Installed-PHP-Versions } | Should -Not -Throw } diff --git a/tests/main.tests.ps1 b/tests/main.tests.ps1 index e177c48..7d375ad 100644 --- a/tests/main.tests.ps1 +++ b/tests/main.tests.ps1 @@ -144,7 +144,7 @@ Describe "Start-PVM Function Tests" { } } Mock Is-PVM-Setup { $true } - Mock Log-Data { $true } + Mock Log-Data { 0 } Mock Alias-Handler { param($alias) @@ -522,7 +522,7 @@ Describe "Start-PVM Function Tests" { } It "Should handle exception when Log-Data fails" { - Mock Log-Data { $false } + Mock Log-Data { -1 } Mock Get-Actions { [ordered]@{ "install" = [PSCustomObject]@{ @@ -688,7 +688,7 @@ Describe "Start-PVM Function Tests" { } Mock Alias-Handler { param($alias) return $alias } Mock Is-PVM-Setup { $true } - Mock Log-Data { $true } + Mock Log-Data { 0 } $result = Start-PVM -operation "install" -arguments @("8.2.0") diff --git a/tests/profile.tests.ps1 b/tests/profile.tests.ps1 index 2ae6d93..8fb9090 100644 --- a/tests/profile.tests.ps1 +++ b/tests/profile.tests.ps1 @@ -115,7 +115,7 @@ Describe "Save-PHP-Profile Tests" { } Mock Write-Host {} - Mock Log-Data { return $true } + Mock Log-Data { return 0 } } It "Returns -1 when php.ini file is missing" { @@ -217,7 +217,7 @@ Describe "Load-PHP-Profile Tests" { Mock Enable-IniExtension-Direct { return 0 } Mock Disable-IniExtension-Direct { return 0 } Mock Write-Host {} - Mock Log-Data { return $true } + Mock Log-Data { return 0 } } It "Should return -1 when current PHP version cannot be determined" { @@ -360,7 +360,7 @@ Describe "Get-Popular-PHP-Settings and Get-Popular-PHP-Extensions Tests" { Describe "Show-PHP-Profile Tests" { BeforeEach { Mock Write-Host {} - Mock Log-Data { return $true } + Mock Log-Data { return 0 } # Ensure profiles directory exists New-Item -ItemType Directory -Force -Path $global:PROFILES_PATH | Out-Null @@ -821,7 +821,7 @@ Describe "Show-PHP-Profile Tests" { Describe "Delete-PHP-Profile Tests" { BeforeEach { Mock Write-Host {} - Mock Log-Data { return $true } + Mock Log-Data { return 0 } # Ensure profiles directory exists New-Item -ItemType Directory -Force -Path $global:PROFILES_PATH | Out-Null @@ -1112,7 +1112,7 @@ Describe "Delete-PHP-Profile Tests" { Describe "Export-PHP-Profile Tests" { BeforeEach { Mock Write-Host {} - Mock Log-Data { return $true } + Mock Log-Data { return 0 } # Ensure profiles directory exists New-Item -ItemType Directory -Force -Path $global:PROFILES_PATH | Out-Null @@ -1321,7 +1321,7 @@ Describe "Export-PHP-Profile Tests" { Describe "Import-PHP-Profile Tests" { BeforeEach { Mock Write-Host {} - Mock Log-Data { return $true } + Mock Log-Data { return 0 } Mock Make-Directory { return 0 } # Ensure profiles directory exists diff --git a/tests/setup.tests.ps1 b/tests/setup.tests.ps1 index 40b92f8..35aca7a 100644 --- a/tests/setup.tests.ps1 +++ b/tests/setup.tests.ps1 @@ -21,7 +21,7 @@ Describe "Setup-PVM" { } # Mock Log-Data function - Mock Log-Data { return $true } + Mock Log-Data { return 0 } # Mock the System.Environment methods function Get-EnvironmentVariableWrapper { @@ -121,7 +121,7 @@ Describe "Setup-PVM" { Mock Set-EnvVar -MockWith { return 0 } Mock Is-Directory-Exists -MockWith { return $false } Mock Make-Directory -MockWith { return $true } - Mock Log-Data -MockWith { return $true } + Mock Log-Data -MockWith { return 0 } Mock Optimize-SystemPath -MockWith {} } diff --git a/tests/uninstall.tests.ps1 b/tests/uninstall.tests.ps1 index 5c07cb3..4812009 100644 --- a/tests/uninstall.tests.ps1 +++ b/tests/uninstall.tests.ps1 @@ -13,7 +13,7 @@ BeforeAll { # Mock Log-Data globally - this will be available for all tests Mock Log-Data -MockWith { param($logPath, $message, $data) - return $true + return 0 } } @@ -23,7 +23,7 @@ Describe "Uninstall-PHP" { Mock Get-Matching-PHP-Versions -MockWith { } Mock Get-UserSelected-PHP-Version -MockWith { } Mock Remove-Item -MockWith { } - Mock Log-Data -MockWith { $true } + Mock Log-Data -MockWith { 0 } } It "Should successfully uninstall when version is found directly" { @@ -88,7 +88,7 @@ Describe "Uninstall-PHP" { @{ code = 0; version = "8.0"; path = "$testPhpPath\8.0" } } Mock Remove-Item -MockWith { } - Mock Log-Data -MockWith { $true } + Mock Log-Data -MockWith { 0 } } It "Should successfully uninstall after user selection" { @@ -113,7 +113,7 @@ Describe "Uninstall-PHP" { } Mock Get-UserSelected-PHP-Version -MockWith { } Mock Remove-Item -MockWith { } - Mock Log-Data -MockWith { $true } + Mock Log-Data -MockWith { 0 } } It "Should return version not found message" { @@ -137,7 +137,7 @@ Describe "Uninstall-PHP" { @{ code = -1; message = "User cancelled the selection"; color = "DarkYellow" } } Mock Remove-Item -MockWith { } - Mock Log-Data -MockWith { $true } + Mock Log-Data -MockWith { 0 } } It "Should return the user selection error" { @@ -162,7 +162,7 @@ Describe "Uninstall-PHP" { @{ code = 0; version = "8.2"; path = $null } } Mock Remove-Item -MockWith { } - Mock Log-Data -MockWith { $true } + Mock Log-Data -MockWith { 0 } } It "Should return version not found message" { From 477e7b072b54bc5604a9508da46c28cbc6b039bb Mon Sep 17 00:00:00 2001 From: Driss B Date: Tue, 10 Feb 2026 14:56:31 +0100 Subject: [PATCH 67/69] Fix common.tests.ps1 --- tests/common.tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/common.tests.ps1 b/tests/common.tests.ps1 index afc0e75..15d9795 100644 --- a/tests/common.tests.ps1 +++ b/tests/common.tests.ps1 @@ -157,7 +157,7 @@ Describe "Get-Installed-PHP-Versions" { Context "When exceptions occur" { It "Should return empty array and log error when Get-Installed-PHP-Versions-From-Directory throws exception" { - Mock Get-Installed-PHP-Versions-From-Directory { throw "Test exception" } + Mock Get-OrUpdateCache { throw "Test exception" } Mock Log-Data { return 0 } $result = Get-Installed-PHP-Versions From 527019c3ce258f7bc7bd00a71e81c55c0e33d295 Mon Sep 17 00:00:00 2001 From: Driss B Date: Tue, 10 Feb 2026 19:13:52 +0100 Subject: [PATCH 68/69] Cover missing paths in common.tests.ps1 --- tests/common.tests.ps1 | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/common.tests.ps1 b/tests/common.tests.ps1 index 15d9795..e820121 100644 --- a/tests/common.tests.ps1 +++ b/tests/common.tests.ps1 @@ -49,6 +49,14 @@ Describe "Is-PVM-Setup" { $result = Is-PVM-Setup $result | Should -Be $true } + + It "Should return false when the path var is null" { + Mock Get-EnvVar-ByName { return $null } + Mock Test-Path { return $true} + + $result = Is-PVM-Setup + $result | Should -Be $false + } } Context "When PVM is not properly set up" { @@ -206,6 +214,21 @@ Describe "Get-UserSelected-PHP-Version" { $result.code | Should -Be 0 $result.path | Should -Be "C:\php\8.1" } + + It "Should print current next to active php version" { + Mock Read-Host { return "2" } + Mock Write-Host { } + Mock Get-Current-PHP-Version { return @{ version = "8.0"; arch = "x64"; buildType = "ts"}} + + $result = Get-UserSelected-PHP-Version -installedVersions @( + @{ version = '7.4'; Arch = 'x64'; BuildType = 'ts'; InstallPath = "C:\php\7.4"} + @{ version = '8.0'; Arch = 'x64'; BuildType = 'ts'; InstallPath = "C:\php\8.0"} + @{ version = '8.1'; Arch = 'x64'; BuildType = 'ts'; InstallPath = "C:\php\8.1"} + ) + + $version = "8.0 ".PadRight(15, '.') + Assert-MockCalled Write-Host -ParameterFilter { $Object -eq " [1] $version x64 ts (Current)" } + } } Describe "Get-Matching-PHP-Versions" { From 2c6881e7de8a97df422edf022b9ad5473f20b618 Mon Sep 17 00:00:00 2001 From: Driss B Date: Wed, 11 Feb 2026 15:19:58 +0100 Subject: [PATCH 69/69] Add tests covering missing paths in router.tests.ps1 --- tests/router.tests.ps1 | 58 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/tests/router.tests.ps1 b/tests/router.tests.ps1 index e131bc6..2b860a3 100644 --- a/tests/router.tests.ps1 +++ b/tests/router.tests.ps1 @@ -75,7 +75,13 @@ Describe "Invoke-PVMSetup Tests" { Describe "Invoke-PVMCurrent Tests" { BeforeEach { - Mock Get-Current-PHP-Version { @{ version = "8.2.0"; status = @{ "xdebug" = $true; "opcache" = $false }; path = "C:\PHP\8.2.0" } } + Mock Get-Current-PHP-Version { @{ + version = "8.2.0" + arch = "x64" + buildType = "TS" + path = "C:\PHP\8.2.0" + status = @{ "xdebug" = $true; "opcache" = $false } + }} # Mock Write-Host { } } @@ -174,6 +180,15 @@ Describe "Invoke-PVMInstall Tests" { $version -eq "8.1" } } + + It "Should return -1 when detected PHP version is already installed" { + $arguments = @("auto") + Mock Auto-Select-PHP-Version { return @{ code = 0; version = "8.2" } } + + $result = Invoke-PVMInstall -arguments $arguments + + $result | Should -Be -1 + } } Describe "Invoke-PVMUninstall Tests" { @@ -484,6 +499,17 @@ Describe "Invoke-PVMProfile Tests" { $profileName -eq "myprofile" } } + + It "Should take first and ignore extra arguments" { + $arguments = @("load", "myprofile", "to-be-ignored") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Load-PHP-Profile -Times 1 -ParameterFilter { + $profileName -eq "myprofile" + } + } } Context "List action" { @@ -519,6 +545,17 @@ Describe "Invoke-PVMProfile Tests" { $profileName -eq "myprofile" } } + + It "Should take first and ignore extra arguments" { + $arguments = @("show", "myprofile", "to-be-ignored") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Show-PHP-Profile -Times 1 -ParameterFilter { + $profileName -eq "myprofile" + } + } } Context "Delete action" { @@ -543,6 +580,17 @@ Describe "Invoke-PVMProfile Tests" { $profileName -eq "myprofile" } } + + It "Should take first and ignore extra arguments" { + $arguments = @("delete", "myprofile", "to-be-ignored") + + $result = Invoke-PVMProfile -arguments $arguments + $result | Should -Be 0 + + Assert-MockCalled Delete-PHP-Profile -Times 1 -ParameterFilter { + $profileName -eq "myprofile" + } + } } Context "Export action" { @@ -680,6 +728,7 @@ Describe "Get-Actions Tests" { Mock Invoke-PVMIni { } Mock Invoke-PVMLog { } Mock Invoke-PVMTest { } + Mock Invoke-PVMProfile { } } It "Should return ordered hashtable with all actions" { @@ -786,6 +835,13 @@ Describe "Get-Actions Tests" { Assert-MockCalled Invoke-PVMTest -Times 1 } + + It "Should execute profile action" { + $actions = Get-Actions -arguments @("save") + $actions["profile"].action.Invoke() + + Assert-MockCalled Invoke-PVMProfile -Times 1 + } } }