diff --git a/readme.md b/readme.md index 1deb2b1..9c18bd3 100644 --- a/readme.md +++ b/readme.md @@ -1,217 +1,218 @@ -# PHP Version Manager for Windows - -PVM (PHP Version Manager) is a lightweight PowerShell tool for Windows that makes it easy to install, switch, and manage multiple PHP versions. - -## Installation & Setup - -Clone the repo and add the directory to your PATH variable. - -```sh -git clone https://github.com/drissboumlik/pvm -cd pvm - -# Run this command to setup pvm -pvm setup -``` - -## Usage - - -```sh -# Display the available options -pvm help - -# Display help for a specific command -pvm help -# Example: pvm help setup - -# Display information about the current PHP (version, path, extensions, settings) -pvm info # pvm ini info - -# Display information about the current PHP extensions -pvm info extensions # pvm ini info extensions - -# Display information about the current PHP settings -pvm info settings # pvm ini info settings - -# Display information about the current PHP (version, path, extensions, settings) with 'cache' in their name -pvm info --search= # pvm ini info --search= -# Example: pvm info --search=cache - -# Display active PHP version -pvm current - -# List installed PHP versions -pvm list # pvm ls - -# List installed versions with 8.2 in the name -pvm list --search= -# Example: pvm list --search=8.2 - -# List installable PHP versions from remote source -pvm list available # pvm ls available - -# List available versions with 8.2 in the name -pvm list available --search= -# Example: pvm list available --search=8.2 - -# Install a specific version. -pvm install # pvm i -# Example: pvm install 8.4 # pvm i 8.4 - -# Install the php version specified on your project. -pvm install auto # pvm i auto - -# Uninstall a specific version -pvm uninstall # pvm rm -# Example: pvm uninstall 8.4 # pvm rm 8.4 - -# Switch to use the specified version -pvm use -# Example: pvm use 8.4 - -# Switch to use the detected PHP version from .php-version or composer.json in your current project/directory -pvm use auto -``` - -### Manage php.ini settings and extensions directly from the CLI. - -```sh -# Check status of multiple extensions -pvm ini status # It shows all matching extensions -# Example: pvm ini status xdebug opcache -# Example: pvm ini status sql - -# Enable or disable multiple extensions -pvm ini enable # It shows all matching extensions -# Example: pvm ini enable xdebug opcache -# Example: pvm ini enable sql -pvm ini disable # It shows all matching extensions -# Example: pvm ini disable xdebug opcache -# Example: pvm ini disable sql - -# Set or Get multiple settings values and change the status -pvm ini set = [--disable] # Default is enabling the setting -pvm ini set [--disable] # It shows all matching settings -# Example: pvm ini set memory_limit=512M max_file_uploads=20 -# Example: pvm ini set max_input_time=60 --disable -# Example: pvm ini set memory=1G -# Example: pvm ini set memory -pvm ini get # It shows all matching settings -# Example: pvm ini get memory_limit max_file_uploads -# Example: pvm ini get memory - -# Install extensions from remote source -pvm ini install -# Example: pvm ini install opcache - -# List installed extensions -pvm ini list - -# List available extensions from remote source -pvm ini list available - -# List installed extensions with 'zip' in their name -pvm ini list --search= -# Example: pvm ini list --search=zip - -# List available extensions with 'zip' in their name -pvm ini list available --search= -# Example: pvm ini list available --search=zip - -# Restore backup -pvm ini restore - -# Check logs -pvm log --pageSize=[number] # Default value is 5 -# Example: pvm log --pageSize=3 -``` - -### Manage PHP Configuration Profiles - -Save, load, and share PHP settings and extensions using JSON profiles: - -```sh -# Save current PHP configuration to a profile -pvm profile save [description] -# Example: pvm profile save development -# Example: pvm profile save production "Production configuration" - -# Load and apply a saved profile -pvm profile load -# Example: pvm profile load development - -# List all available profiles -pvm profile list - -# Show detailed profile contents -pvm profile show -# Example: pvm profile show development - -# Delete a profile -pvm profile delete -# Example: pvm profile delete old-profile - -# Export profile to a JSON file -pvm profile export [path] -# Example: pvm profile export development -# Example: pvm profile export dev ./backup.json - -# Import profile from a JSON file -pvm profile import [name] -# Example: pvm profile import ./my-profile.json -# Example: pvm profile import ./profile.json custom-name -``` - -**Profile Structure**: Profiles are stored as JSON files in `storage/data/profiles/` and contain: -- Popular/common PHP settings (key-value pairs with enabled/disabled state) -- Popular/common extensions (enabled/disabled state and type) -- Metadata (name, description, creation date, PHP version) - -**Note**: Only popular/common settings and extensions are saved in profiles. This keeps profiles focused and manageable. - -## Running Tests -Run tests against the PowerShell scripts in the repo — especially useful for contributors verifying changes before submitting a pull request: - -### Requirements - -To run tests with, you need to have the Pester testing framework installed. Pester is a testing framework for PowerShell. - -Open PowerShell as Administrator and run: - -```powershell -Install-Module -Name Pester -Force -SkipPublisherCheck -``` -> 💡 If prompted to trust the repository, type Y and press Enter. - -You can verify the installation with: -```powershell -Get-Module -ListAvailable Pester -``` - -### Run the tests - -```sh -pvm test [files = (files inside the tests/ directory)] [--coverage[=]] [--verbosity=(None|Normal|Detailed|Diagnostic)] [--tag=] - -# Examples: -pvm test # .............................. Runs all tests with Normal (default) verbosity. -pvm test use install # .................. Runs only 'use.tests.ps1' and 'install.tests.ps1' files with Normal verbosity. -pvm test --verbosity=Detailed # ......... Runs all tests with Detailed verbosity. -pvm test --coverage # ................... Runs all tests and generates coverage report (target: 75%) -pvm test --coverage=80.5 # .............. Runs all tests and generates coverage report (target: 80.5%) -pvm test --tag=myTag #................... Runs helpers.tests.ps1 and list.tests.ps1 with Diagnostic verbosity and only runs tests with tag "myTag". -``` - -## Contributing - -Please see [CONTRIBUTING](CONTRIBUTING.md) for details. - -## Credits - -- [Driss](https://github.com/drissboumlik) -- [All Contributors](https://github.com/drissboumlik/pvm/contributors) - -## License - +# PHP Version Manager for Windows + +PVM (PHP Version Manager) is a lightweight PowerShell tool for Windows that makes it easy to install, switch, and manage multiple PHP versions. + +## Installation & Setup + +Clone the repo and add the directory to your PATH variable. + +```sh +git clone https://github.com/drissboumlik/pvm +cd pvm + +# Run this command to setup pvm +pvm setup +``` + +## Usage + + +```sh +# Display the available options +pvm help + +# Display help for a specific command +pvm help +# Example: pvm help setup + +# Display information about the current PHP (version, path, extensions, settings) +pvm info # pvm ini info + +# Display information about the current PHP extensions +pvm info extensions # pvm ini info extensions + +# Display information about the current PHP settings +pvm info settings # pvm ini info settings + +# Display information about the current PHP (version, path, extensions, settings) with 'cache' in their name +pvm info --search= # pvm ini info --search= +# Example: pvm info --search=cache + +# Display active PHP version +pvm current + +# List installed PHP versions +pvm list # pvm ls + +# List installed versions with 8.2 in the name +pvm list --search= +# Example: pvm list --search=8.2 + +# List installable PHP versions from remote source +pvm list available # pvm ls available + +# List available versions with 8.2 in the name +pvm list available --search= +# Example: pvm list available --search=8.2 + +# Install a specific version. +pvm install # pvm i +# Example: pvm install 8.4 # pvm i 8.4 + +# Install the php version specified on your project. +pvm install auto # pvm i auto + +# Uninstall a specific version +pvm uninstall # pvm rm +# Example: pvm uninstall 8.4 # pvm rm 8.4 + +# Switch to use the specified version +pvm use +# Example: pvm use 8.4 + +# Switch to use the detected PHP version from .php-version or composer.json in your current project/directory +pvm use auto +``` + +### Manage php.ini settings and extensions directly from the CLI. + +```sh +# Check status of multiple extensions +pvm ini status # It shows all matching extensions +# Example: pvm ini status xdebug opcache +# Example: pvm ini status sql + +# Enable or disable multiple extensions +pvm ini enable # It shows all matching extensions +# Example: pvm ini enable xdebug opcache +# Example: pvm ini enable sql +pvm ini disable # It shows all matching extensions +# Example: pvm ini disable xdebug opcache +# Example: pvm ini disable sql + +# Set or Get multiple settings values and change the status +pvm ini set = [--disable] # Default is enabling the setting +pvm ini set [--disable] # It shows all matching settings +# Example: pvm ini set memory_limit=512M max_file_uploads=20 +# Example: pvm ini set max_input_time=60 --disable +# Example: pvm ini set memory=1G +# Example: pvm ini set memory +pvm ini get # It shows all matching settings +# Example: pvm ini get memory_limit max_file_uploads +# Example: pvm ini get memory + +# Install extensions from remote source +pvm ini install +# Example: pvm ini install opcache + +# List installed extensions +pvm ini list + +# List available extensions from remote source +pvm ini list available + +# List installed extensions with 'zip' in their name +pvm ini list --search= +# Example: pvm ini list --search=zip + +# List available extensions with 'zip' in their name +pvm ini list available --search= +# Example: pvm ini list available --search=zip + +# Restore backup +pvm ini restore + +# Check logs +pvm log --pageSize=[number] # Default value is 5 +# Example: pvm log --pageSize=3 +``` + +### Manage PHP Configuration Profiles + +Save, load, and share PHP settings and extensions using JSON profiles: + +```sh +# Save current PHP configuration to a profile +pvm profile save [description] +# Example: pvm profile save development +# Example: pvm profile save production "Production configuration" + +# Load and apply a saved profile +pvm profile load +# Example: pvm profile load development + +# List all available profiles +pvm profile list + +# Show detailed profile contents +pvm profile show +# Example: pvm profile show development + +# Delete a profile +pvm profile delete +# Example: pvm profile delete old-profile + +# Export profile to a JSON file +pvm profile export [path] +# Example: pvm profile export development +# Example: pvm profile export dev ./backup.json + +# Import profile from a JSON file +pvm profile import [name] +# Example: pvm profile import ./my-profile.json +# Example: pvm profile import ./profile.json custom-name +``` + +**Profile Structure**: Profiles are stored as JSON files in `storage/data/profiles/` and contain: +- Popular/common PHP settings (key-value pairs with enabled/disabled state) +- Popular/common extensions (enabled/disabled state and type) +- Metadata (name, description, creation date, PHP version) + +**Note**: Only popular/common settings and extensions are saved in profiles. This keeps profiles focused and manageable. + +## Running Tests +Run tests against the PowerShell scripts in the repo — especially useful for contributors verifying changes before submitting a pull request: + +### Requirements + +To run tests with, you need to have the Pester testing framework installed. Pester is a testing framework for PowerShell. + +Open PowerShell as Administrator and run: + +```powershell +Install-Module -Name Pester -Force -SkipPublisherCheck +``` +> 💡 If prompted to trust the repository, type Y and press Enter. + +You can verify the installation with: +```powershell +Get-Module -ListAvailable Pester +``` + +### Run the tests + +```sh +pvm test [files = (files inside the tests/ directory)] [--coverage[=]] [--verbosity=(None|Normal|Detailed|Diagnostic)] [--tag=] [--sort=[coverage|duration|file]] + +# Examples: +pvm test # .............................. Runs all tests with Normal (default) verbosity. +pvm test use install # .................. Runs only 'use.tests.ps1' and 'install.tests.ps1' files with Normal verbosity. +pvm test --verbosity=Detailed # ......... Runs all tests with Detailed verbosity. +pvm test --coverage # ................... Runs all tests and generates coverage report (target: 75%) +pvm test --coverage=80.5 # .............. Runs all tests and generates coverage report (target: 80.5%) +pvm test --sort=duration # .............. Runs all tests and sort results by duration +pvm test --tag=myTag #................... Runs helpers.tests.ps1 and list.tests.ps1 with Diagnostic verbosity and only runs tests with tag "myTag". +``` + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Credits + +- [Driss](https://github.com/drissboumlik) +- [All Contributors](https://github.com/drissboumlik/pvm/contributors) + +## License + The MIT License (MIT). Please see [License File](LICENSE) for more information. \ No newline at end of file diff --git a/src/actions/common.ps1 b/src/actions/common.ps1 index 85d2773..a1a5267 100644 --- a/src/actions/common.ps1 +++ b/src/actions/common.ps1 @@ -37,17 +37,62 @@ 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 { + if (Test-Path "$($_.FullName)\php.exe"){ + $phpInfo = Get-PHPInstallInfo -path $_.FullName + + return $phpInfo + } + return $null + } + + $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, $buildType = $null) try { - $directories = Get-All-Subdirectories -path "$STORAGE_PATH\php" - $names = $directories | ForEach-Object { - if (Test-Path "$($_.FullName)\php.exe"){ - return $_.Name - } - return $null + $installedVersions = Get-OrUpdateCache -cacheFileName "installed_php_versions" -depth 1 -compute { + Get-Installed-PHP-Versions-From-Directory + } + + if ($null -eq $installedVersions) { + return @() + } + + if ($arch) { + $installedVersions = $installedVersions | Where-Object { $_.Arch -eq $arch } + } + + if ($buildType) { + $installedVersions = $installedVersions | Where-Object { $_.BuildType -eq $buildType } } - return ($names | Sort-Object { [version]$_ }) + + $installedVersions = $installedVersions | Sort-Object { [version]$_.Version } + + return $installedVersions } catch { $logged = Log-Data -data @{ header = "$($MyInvocation.MyCommand.Name) - Failed to retrieve installed PHP versions" @@ -64,47 +109,48 @@ function Get-UserSelected-PHP-Version { if (-not $installedVersions -or $installedVersions.Count -eq 0) { return $null } - if ($installedVersions.Count -eq 1) { - $version = $($installedVersions) + if ($installedVersions.Length -eq 1) { + $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 = $_ + $_ | Add-Member -NotePropertyName "index" -NotePropertyValue $index -Force $isCurrent = "" - if ($currentVersion -eq $versionNumber) { + 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 { param ($version) try { - $installedVersions = Get-Installed-PHP-Versions # You should have this function - - $matchingVersions = @() - foreach ($v in $installedVersions) { - if ($v -like "$version*") { - $matchingVersions += ($v -replace 'php', '') - } - } + $installedVersions = Get-Installed-PHP-Versions + $matchingVersions = $installedVersions | Where-Object { $_.Version -like "$version*" } + return $matchingVersions } catch { $logged = Log-Data -data @{ @@ -120,8 +166,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/current.ps1 b/src/actions/current.ps1 index 723aeb1..0a15c4b 100644 --- a/src/actions/current.ps1 +++ b/src/actions/current.ps1 @@ -1,61 +1,64 @@ - -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.Arch + buildType = $phpInfo.BuildType + 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/ini.ps1 b/src/actions/ini.ps1 index 7878a74..0059abf 100644 --- a/src/actions/ini.ps1 +++ b/src/actions/ini.ps1 @@ -1,4 +1,29 @@ +function getXdebugConfigV2 { + param($XDebugPath) + + return @" + + [xdebug] + ;zend_extension="$XDebugPath" + xdebug.remote_enable=1 + xdebug.remote_host=127.0.0.1 + xdebug.remote_port=9000 +"@ +} + +function getXdebugConfigV3 { + param($XDebugPath) + + return @" + + [xdebug] + ;zend_extension="$XDebugPath" + xdebug.mode=debug + xdebug.client_host=127.0.0.1 + xdebug.client_port=9003 +"@ +} function Get-XDebug-FROM-URL { param ($url, $version) @@ -7,22 +32,32 @@ function Get-XDebug-FROM-URL { $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 { '' } - $filteredLinks = $links | Where-Object { - $_.href -match "php_xdebug-[\d\.a-zA-Z]+-$version-.*$sysArch\.dll" - } - # Return the filtered links (PHP version names) $formattedList = @() - $filteredLinks = $filteredLinks | ForEach-Object { - $fileName = $_.href -split "/" - $fileName = $fileName[$fileName.Count - 1] + $links | ForEach-Object { + if (-not $_.href) { return } + + $fileName = [System.IO.Path]::GetFileName($_.href) + + if ($fileName -notmatch '^php_xdebug-.*\.dll$') { return } + + if ($fileName -notmatch "php_xdebug-[\d\.a-zA-Z]+-$version-") { return } + $xDebugVersion = "2.0" - if ($_.href -match "php_xdebug-([^-]+)") { + if ($fileName -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 '(?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 + } } return $formattedList @@ -36,36 +71,245 @@ function Get-XDebug-FROM-URL { } -function Add-Missing-PHPExtension { - param ($iniPath, $extName, $enable = $true) +function Get-Extension-Matching-Categories-By-Page { + param ($extName, $link, $page = 1) + + $html = Invoke-WebRequest -Uri "$PECL_BASE_URL/$($link.TrimStart('/'))&pageID=$page" + $hasMore = $false + $resultLinks = $html.Links | Where-Object { + if (-not $_.href) { return $false } + if ($_.href -match '^/packages\.php\?catpid=\d+&catname=[A-Za-z+]+&pageID=(\d+)$') { + $hasMore = ($page -eq ($matches[1] - 1)) + return $false + } + + return ($_.href -like "/package/*$extName*") + } + + return @{ + hasMore = $hasMore + resultLinks = $resultLinks + } +} + +function Get-Extension-Matching-Categories { + param ($extName) + + $html_cat = Invoke-WebRequest -Uri $PECL_PACKAGES_URL + $linksMatchingExtName = @() + $resultCat = $html_cat.Links | Where-Object { + if (-not $_.href) { return $false } + + if ($_.href -notmatch '^/packages\.php\?catpid=\d+&catname=([A-Za-z+]+)$') { + return $false + } + + $page = 1 + $category = $matches[1] -replace '\+', ' ' + Write-Host "- Checking category '$category'..." -ForegroundColor Gray + do { + $hasMore = $false + $result = Get-Extension-Matching-Categories-By-Page -extName $extName -link $_.href -page $page + $hasMore = $result.hasMore + $page++ + + if ($result.resultLinks.Count -gt 0) { + $linksMatchingExtName += $result.resultLinks + } + } while ($hasMore) + + return $false + } + + return $linksMatchingExtName +} + +function Filter-Extension-Links-From-URL { + param ($extName) + + $html = Invoke-WebRequest -Uri "$PECL_PACKAGE_ROOT_URL/$extName" + $links = $html.Links | Where-Object { + $_.href -match "/package/$extName/([^/]+)/windows$" + } + + return $links +} + +function Get-Extension-Links-From-URL { + param ($extName, $version) try { - $phpCurrentVersion = Get-Current-PHP-Version - if (-not $phpCurrentVersion -or -not $phpCurrentVersion.version) { - Write-Host "`nFailed to get current PHP version." -ForegroundColor DarkYellow + $links = Get-OrUpdateCache -cacheFileName "available_$($extName)_versions_$version" -compute { + Filter-Extension-Links-From-URL -extName $extName + } + } catch { + Write-Host "`nExtension '$extName' not found, Loading matching extensions..." + + $linksMatchingExtName = Get-Extension-Matching-Categories -extName $extName + + if ($linksMatchingExtName.Count -eq 0) { + Write-Host "`nExtension '$extName' not found" -ForegroundColor DarkYellow + return $null + } + + if ($linksMatchingExtName.Count -eq 1) { + $chosenItem = $($linksMatchingExtName) + } else { + Write-Host "`nMatching '$extName' extension:" + $index = 0 + $linksMatchingExtName | ForEach-Object { + $extItem = $_.href -replace "/package/", "" + Write-Host "[$index] $extItem" + $index++ + } + + do { + $choiceRaw = Read-Host "`nInsert the [number] you want to install" + $choiceRaw = $choiceRaw.Trim() + if ([string]::IsNullOrWhiteSpace($choiceRaw)) { + Write-Host "`nInstallation cancelled" + return $null + } + + $choice = $null + if (-not [int]::TryParse($choiceRaw, [ref]$choice)) { + Write-Host "Please enter a valid positive number." -ForegroundColor Yellow + continue + } + + if ($choice -lt 0 -or $choice -gt $linksMatchingExtName.Length - 1) { + Write-Host "Number must be between 0 and $($linksMatchingExtName.Length - 1)." -ForegroundColor Yellow + continue + } + + break + } while ($true) + + $chosenItem = $linksMatchingExtName[$choice] + if (-not $chosenItem) { + Write-Host "`nYou chose the wrong index: $choice" -ForegroundColor DarkYellow + return $null + } + } + + $extName = $chosenItem.href -replace "/package/", "" + $links = Get-OrUpdateCache -cacheFileName "available_$($extName)_versions_$version" -compute { + Filter-Extension-Links-From-URL -extName $extName + } + } + + return @{ + extName = $extName + links = $links + } +} + +function Get-Packages-From-Source-Links { + param( $extName, $version, $links) + + $formattedList = @() + $links | ForEach-Object { + try { + $extVersion = $_.href -replace "/package/$extName/", "" -replace "/windows", "" + $html = Invoke-WebRequest -Uri "$PECL_PACKAGE_ROOT_URL/$extName/$extVersion/windows" + $html.Links | ForEach-Object { + if (-not $_.href) { return } + + $fileName = [System.IO.Path]::GetFileName($_.href) + + if ($fileName -notmatch "^php_$extName-.*\.zip$") { return } + + # if ($fileName -notmatch "php_$extName-$version-") { return } + if ($fileName -notmatch "^php_$extName-[\d\.]+(?:[a-z]+\d+)?-$version-") { return } + + $formattedList += @{ + href = $_.href + version = $version + extVersion = $extVersion + arch = if ($fileName -match '(x86_64|x64)(?=\.zip$)') { 'x64' } else { 'x86' } + buildType = if ($fileName -match '(?i)(?:^|-)nts(?:-|\.zip$)') { 'NTS' } else { 'TS' } + compiler = if ($fileName -match '(?i)\b(vs|vc)\d+\b') { $matches[0].ToUpper() } else { 'unknown' } + fileName = $fileName + } + } + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to find packages for $extName v$extVersion" + exception = $_ + } + } + } + + return $formattedList +} + +function Get-Extension-From-URL { + param ($extName, $version) + + $linksObj = Get-Extension-Links-From-URL -extName $extName -version $version + + if (($null -eq $linksObj) -or ($linksObj.Count -eq 0) -or ($null -eq $linksObj.links) -or ($linksObj.links.Count -eq 0)) { + $extName = if ($linksObj -and $linksObj.extName) { $linksObj.extName } else { $extName } + Write-Host "`nNo versions found for $extName" -ForegroundColor DarkYellow + return @{ extName = $extName; data = $null } + } + + $formattedList = Get-OrUpdateCache -cacheFileName "packages_links_for_$($linksObj.extName)_php_$version" -compute { + Get-Packages-From-Source-Links -extName $linksObj.extName -version $version -links $linksObj.links + } + + return @{ + extName = $linksObj.extName + data = $formattedList + } +} + +function Add-Missing-PHPExtension-To-Ini { + param ($iniPath, $extFileName, $enable = $true) + + try { + if (-not (Test-Path $iniPath)) { + Write-Host "`nphp.ini file not found: $iniPath" -ForegroundColor DarkYellow return -1 } Backup-IniFile $iniPath - $extName = $extName -replace '^php_', '' -replace '\.dll$', '' - $extName = "php_$extName.dll" + $phpDirectory = Split-Path -Path $iniPath -Parent + $extDirectory = Join-Path -Path $phpDirectory -ChildPath "ext" + + if (-not (Test-Path $extDirectory)) { + Write-Host "`nExtensions directory not found: $extDirectory" -ForegroundColor DarkYellow + return -1 + } + + if (-not (Test-Path "$extDirectory\$extFileName")) { + Write-Host "`nExtension file not found: $extFileName" -ForegroundColor DarkYellow + return -1 + } $lines = Get-Content $iniPath + foreach ($line in $lines) { + if ($line -match "^(;)?\s*(zend_)?extension\s*=\s*$extFileName\s*") { + Write-Host "- Extension '$extFileName' already exists in php.ini" -ForegroundColor DarkGray + return 0 + } + } + $commented = if ($enable) { '' } else { ';' } - $isZendExtension = Get-Zend-Extensions-List | Where-Object { $extName -like "*$_*" } + $isZendExtension = Get-Zend-Extensions-List | Where-Object { $extFileName -like "*$_*" } if ($isZendExtension) { - $lines += "`n$commented" + "zend_extension=$extName" + $lines += "`n$commented" + "zend_extension=$extFileName" } else { - $lines += "`n$commented" + "extension=$extName" + $lines += "`n$commented" + "extension=$extFileName" } Set-Content $iniPath $lines -Encoding UTF8 - Write-Host "- '$extName' added successfully." -ForegroundColor DarkGreen + Write-Host "- '$extFileName' added successfully." -ForegroundColor DarkGreen return 0 } catch { $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to add extension '$extName'" + header = "$($MyInvocation.MyCommand.Name) - Failed to add extension '$extFileName'" exception = $_ } return -1 @@ -328,7 +572,7 @@ function Set-IniSetting { foreach ($line in $lines) { if ($line -match $pattern) { $matchesList += @{ - Index = $matchesList.Length + 1 + Index = $matchesList.Length Key = $matches['key'].Trim() Value = $matches['value'].Trim() Enabled = -not ($line -match '^[#;]') @@ -366,15 +610,15 @@ function Set-IniSetting { continue } - if ($choice -lt 1 -or $choice -gt $matchesList.Length) { - Write-Host "Number must be between 1 and $($matchesList.Length)." -ForegroundColor Yellow + if ($choice -lt 0 -or $choice -gt $matchesList.Length - 1) { + Write-Host "Number must be between 0 and $($matchesList.Length - 1)." -ForegroundColor Yellow continue } break } while ($true) - $selected = $matchesList[$choice - 1] + $selected = $matchesList[$choice] } else { $selected = $($matchesList) } @@ -432,7 +676,7 @@ function Enable-IniExtension { Write-Host "`nMultiple extensions match '$extName':`n" -ForegroundColor Cyan $maxLineLength = ($matchesListStatus.name | Measure-Object -Maximum Length).Maximum + 10 - $index = 1 + $index = 0 $matchesListStatus | ForEach-Object { $name = "$($_.name) ".PadRight($maxLineLength, '.') Write-Host "[$index] $name " -NoNewline @@ -449,15 +693,15 @@ function Enable-IniExtension { continue } - if ($choice -lt 1 -or $choice -gt $matchesListStatus.Length) { - Write-Host "Number must be between 1 and $($matchesListStatus.Length)." -ForegroundColor Yellow + if ($choice -lt 0 -or $choice -gt $matchesListStatus.Length - 1) { + Write-Host "Number must be between 0 and $($matchesListStatus.Length - 1)." -ForegroundColor Yellow continue } break } while ($true) - $selected = $matchesListStatus[$choice - 1] + $selected = $matchesListStatus[$choice] } else { $selected = $($matchesListStatus) } @@ -521,7 +765,7 @@ function Disable-IniExtension { Write-Host "`nMultiple extensions match '$extName':`n" -ForegroundColor Cyan $maxLineLength = ($matchesListStatus.name | Measure-Object -Maximum Length).Maximum + 10 - $index = 1 + $index = 0 $matchesListStatus | ForEach-Object { $name = "$($_.name) ".PadRight($maxLineLength, '.') Write-Host "[$index] $name " -NoNewline @@ -538,15 +782,15 @@ function Disable-IniExtension { continue } - if ($choice -lt 1 -or $choice -gt $matchesListStatus.Length) { - Write-Host "Number must be between 1 and $($matchesListStatus.Length)." -ForegroundColor Yellow + if ($choice -lt 0 -or $choice -gt $matchesListStatus.Length - 1) { + Write-Host "Number must be between 0 and $($matchesListStatus.Length - 1)." -ForegroundColor Yellow continue } break } while ($true) - $selected = $matchesListStatus[$choice - 1] + $selected = $matchesListStatus[$choice] } else { $selected = $($matchesListStatus) } @@ -776,23 +1020,39 @@ function Install-XDebug-Extension { param ($iniPath) try { - $currentVersion = (Get-Current-PHP-Version).version -replace '^(\d+\.\d+)\..*$', '$1' - $xDebugList = Get-XDebug-FROM-URL -url $XDEBUG_HISTORICAL_URL -version $currentVersion - $xDebugList = $xDebugList | Sort-Object { [version]$_.xDebugVersion } -Descending + $currentVersionObj = Get-Current-PHP-Version + $currentVersion = $currentVersionObj.version -replace '^(\d+\.\d+)\..*$', '$1' + $xDebugList = Get-OrUpdateCache -cacheFileName "available_xdebug_versions_$currentVersion" -compute { + Get-XDebug-FROM-URL -url $XDEBUG_HISTORICAL_URL -version $currentVersion + } if ($null -eq $xDebugList -or $xDebugList.Count -eq 0) { Write-Host "`nNo match was found, check the '$LOG_ERROR_PATH' for any potentiel errors" return -1 } + + $xDebugList = $xDebugList | Where-Object { + if ($currentVersionObj.arch -ne $null) { + if ($_.arch -ne $currentVersionObj.arch) { return $false } + } + + if ($currentVersionObj.buildType -ne $null) { + if ($_.buildType -ne $currentVersionObj.buildType) { return $false } + } + + return $true + } $xDebugListGrouped = [ordered]@{} + $index = 0 $xDebugList | + Select-Object -First $LATEST_VERSION_COUNT | Group-Object xDebugVersion | Sort-Object ` @{ Expression = { # extract numeric version [version]($_.Name -replace '(alpha|beta|rc).*','') - }; Descending = $true }, + }; Descending = $true }, @{ Expression = { # prerelease weight if ($_.Name -match 'alpha') { 1 } @@ -809,18 +1069,33 @@ 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 } + + $sortedGroup | ForEach-Object { + $_ | Add-Member -NotePropertyName "index" -NotePropertyValue $index -Force + $index++ + } + + $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() - Write-Host " [$index] $text" - $index++ + $text = "PHP XDebug $($_.version) $($_.compiler) $($_.buildType) $($_.arch)" + Write-Host " [$($_.index)] $text" } } + Write-Host "`nThis is a partial list. For a complete list, visit: $XDEBUG_HISTORICAL_URL" $packageIndex = Read-Host "`nInsert the [number] you want to install" $packageIndex = $packageIndex.Trim() @@ -829,7 +1104,11 @@ function Install-XDebug-Extension { return -1 } - $chosenItem = $xDebugList[$packageIndex] + $chosenItem = $xDebugListGrouped.GetEnumerator() | ForEach-Object { + $_.Value | Where-Object { + $_.index -eq $packageIndex + } + } if (-not $chosenItem) { Write-Host "`nYou chose the wrong index: $packageIndex" -ForegroundColor DarkYellow return -1 @@ -852,8 +1131,23 @@ 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 + } + + Write-Host "`nXDebug installed successfully" -ForegroundColor DarkGreen return 0 } catch { @@ -869,117 +1163,100 @@ function Install-Extension { param ($iniPath, $extName) try { - try { - $html = Invoke-WebRequest -Uri "$PECL_PACKAGE_ROOT_URL/$extName" - } catch { - Write-Host "`nExtension '$extName' not found, Loading matching extensions..." - # check by match - $html_cat = Invoke-WebRequest -Uri $PECL_PACKAGES_URL - $linksMatchnigExtName = @() - $resultCat = $html_cat.Links | Where-Object { - if (-not $_.href) { return $false } - if ($_.href -match '^/packages\.php\?catpid=\d+&catname=[A-Za-z+]+$') { - $html = Invoke-WebRequest -Uri "$PECL_BASE_URL/$($_.href.TrimStart('/'))" - $resultLinks = $html.Links | Where-Object { - if (-not $_.href) { return $false } - return ($_.href -like "/package/*$extName*") - } - if ($resultLinks.Count -eq 0) { - return $false - } - $linksMatchnigExtName += $resultLinks - return $true - } - } - - if ($linksMatchnigExtName.Count -eq 0) { - Write-Host "`nExtension '$extName' not found" -ForegroundColor DarkYellow - return -1 + $currentVersionObj = Get-Current-PHP-Version + $currentVersion = $currentVersionObj.version -replace '^(\d+\.\d+)\..*$', '$1' + $extensionLinksObj = Get-Extension-From-URL -extName $extName -version $currentVersion + + if (($null -eq $extensionLinksObj) -or ($extensionLinksObj.Count -eq 0) -or ($null -eq $extensionLinksObj.data) -or ($extensionLinksObj.data.Count -eq 0)) { + $extName = if ($extensionLinksObj) { $extensionLinksObj.extName } else { $extName } + Write-Host "`nNo packages found for $extName" -ForegroundColor DarkYellow + return -1 + } + + $extensionLinks = $extensionLinksObj.data | Where-Object { + if ($currentVersionObj.arch -ne $null) { + if ($_.arch -ne $currentVersionObj.arch) { return $false } } - - if ($linksMatchnigExtName.Count -eq 1) { - $chosenItem = $linksMatchnigExtName[0] - } else { - Write-Host "`nMatching '$extName' extension:" - $index = 0 - $linksMatchnigExtName | ForEach-Object { - $extItem = $_.href -replace "/package/", "" - Write-Host "[$index] $extItem" - $index++ - } - $extIndex = Read-Host "`nInsert the [number] you want to install" - $extIndex = $extIndex.Trim() - if ([string]::IsNullOrWhiteSpace($extIndex)) { - Write-Host "`nInstallation cancelled" - return -1 - } - - $chosenItem = $linksMatchnigExtName[$extIndex] - if (-not $chosenItem) { - Write-Host "`nYou chose the wrong index: $extIndex" -ForegroundColor DarkYellow - return -1 - } + + if ($currentVersionObj.buildType -ne $null) { + if ($_.buildType -ne $currentVersionObj.buildType) { return $false } } - $extName = $chosenItem.href -replace "/package/", "" - $html = Invoke-WebRequest -Uri "$PECL_PACKAGE_ROOT_URL/$extName" + return $true } - $links = $html.Links | Where-Object { - $_.href -match "/package/$extName/([^/]+)/windows$" - } - if ($links.Count -eq 0) { - Write-Host "`nNo versions found for $extName" -ForegroundColor DarkYellow + if ($null -eq $extensionLinks -or $extensionLinks.Count -eq 0) { + Write-Host "`nNo packages found for '$extName' matching current PHP architecture/build type" -ForegroundColor DarkYellow return -1 } - $currentVersion = (Get-Current-PHP-Version).version -replace '^(\d+\.\d+)\..*$', '$1' - $pachagesGroupLinks = @() - $links | ForEach-Object { - $extVersion = $_.href -replace "/package/$extName/", "" -replace "/windows", "" - try { - $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$") { - $phpVersion = $matches[1] - return ($phpVersion -eq $currentVersion) - } - return $false - } - - if ($packageLinks -and $packageLinks.Count -gt 0) { - Write-Host "`n$extName v$extVersion :" - $index = $pachagesGroupLinks.Count - foreach ($link in $packageLinks) { - $link | Add-Member -NotePropertyName "extVersion" -NotePropertyValue $extVersion -Force - $pachagesGroupLinks += $link - $text = ($link.outerHTML -replace '<.*?>|.zip','').Trim() - Write-Host " [$index] $text" + $extName = $extensionLinksObj.extName + if ($extensionLinks.Length -eq 1) { + $chosenItem = $($extensionLinks) + } else { + $extensionLinksGrouped = [ordered]@{} + $index = 0 + $extensionLinks | + Select-Object -First $LATEST_VERSION_COUNT | + Group-Object extVersion | + Sort-Object ` + @{ Expression = { + # extract numeric version + [version]($_.Name -replace '(alpha|beta|rc).*','') + }; Descending = $true }, + @{ Expression = { + # prerelease weight + if ($_.Name -match 'alpha') { 1 } + elseif ($_.Name -match 'beta') { 2 } + elseif ($_.Name -match 'rc') { 3 } + else { 4 } # stable + }; Descending = $true }, + @{ Expression = { + # prerelease number (alpha3, rc2, etc) + if ($_.Name -match '(alpha|beta|rc)(\d+)') { + [int]$matches[2] + } else { + [int]::MaxValue + } + }; Descending = $true } | + ForEach-Object { + $sortedGroup = $_.Group | Sort-Object ` + @{ Expression = { $_.buildType -eq 'NTS' }; Descending = $true }, + @{ Expression = { + switch ($_.arch) { + 'x86_64' { 2 } + 'x64' { 2 } + 'x86' { 1 } + default { 0 } + } + }; Descending = $true } + $sortedGroup | ForEach-Object { + $_ | Add-Member -NotePropertyName "index" -NotePropertyValue $index -Force $index++ } + + $extensionLinksGrouped[$_.Name] = $sortedGroup } - } catch { - $logged = Log-Data -data @{ - header = "$($MyInvocation.MyCommand.Name) - Failed to find packages for $extName v$extVersion" - exception = $_ + + $extensionLinksGrouped.GetEnumerator() | ForEach-Object { + Write-Host "`n$extName $($_.Key)" + $_.Value | ForEach-Object { + $text = "PHP $extName $($_.version) $($_.compiler) $($_.buildType) $($_.arch)" + Write-Host " [$($_.index)] $text" } } - } - - if ($pachagesGroupLinks.Count -eq 0) { - Write-Host "`nNo packages found for $extName" -ForegroundColor DarkYellow - return -1 - } + Write-Host "`nThis is a partial list. For a complete list, visit: $PECL_PACKAGE_ROOT_URL/$extName" - $packageIndex = Read-Host "`nInsert the [number] you want to install" - $packageIndex = $packageIndex.Trim() - if ([string]::IsNullOrWhiteSpace($packageIndex)) { - Write-Host "`nInstallation cancelled" - return -1 + $packageIndex = Read-Host "`nInsert the [number] you want to install" + $packageIndex = $packageIndex.Trim() + if ([string]::IsNullOrWhiteSpace($packageIndex)) { + Write-Host "`nInstallation cancelled" + return -1 + } + + $chosenItem = $extensionLinks | Where-Object { $_.index -eq $packageIndex } } - $chosenItem = $pachagesGroupLinks[$packageIndex] if (-not $chosenItem) { Write-Host "`nYou chose the wrong index: $packageIndex" -ForegroundColor DarkYellow return -1 @@ -991,7 +1268,7 @@ function Install-Extension { Remove-Item -Path "$STORAGE_PATH\php\$fileNamePath.zip" $files = Get-ChildItem -Path "$STORAGE_PATH\php\$fileNamePath" $extFile = $files | Where-Object { - ($_.Name -match "($extName|php_$extName).dll") + ($_.Name -match "^php_$extName.*\.dll$") } if (-not $extFile) { Write-Host "`nFailed to find $extName" -ForegroundColor DarkYellow @@ -1009,11 +1286,13 @@ function Install-Extension { } Move-Item -Path $extFile.FullName -Destination "$phpPath\ext" Remove-Item -Path "$STORAGE_PATH\php\$fileNamePath" -Force -Recurse - $code = Add-Missing-PHPExtension -iniPath $iniPath -extName $extName -enable $false + $code = Add-Missing-PHPExtension-To-Ini -iniPath $iniPath -extFileName $extFile.Name -enable $false if ($code -ne 0) { Write-Host "`nFailed to add $extName" -ForegroundColor DarkYellow return -1 } + Write-Host "`n$extName installed successfully" -ForegroundColor DarkGreen + return 0 } catch { $logged = Log-Data -data @{ @@ -1039,14 +1318,8 @@ function Install-IniExtension { $code = Install-Extension -iniPath $iniPath -extName $extName } - if ($code -ne 0) { - throw "`nFailed to install $extName" - } - - Write-Host "`n$extName installed successfully" - return 0 + return $code } catch { - Write-Host "`nFailed to install $extName" -ForegroundColor DarkYellow $logged = Log-Data -data @{ header = "$($MyInvocation.MyCommand.Name) - Failed to install '$extName'" exception = $_ @@ -1055,34 +1328,59 @@ function Install-IniExtension { } } +function Get-Extension-Categories-By-Page { + param ($extCategory, $link, $page = 1) + + $availableExtensions = @() + $html = Invoke-WebRequest -Uri "$PECL_BASE_URL/$($link.TrimStart('/'))&pageID=$page" + $hasMore = $false + $resultLinks = $html.Links | Where-Object { + if (-not $_.href) { return $false } + if ($_.href -match '^/packages\.php\?catpid=\d+&catname=[A-Za-z+]+&pageID=(\d+)$') { + $hasMore = ($page -eq ($matches[1] - 1)) + return $false + } + if ($_.href -notmatch '^/package/[A-Za-z0-9_]+$') { + return $false + } + $extName = ($_.href -replace '/package/', '').Trim() + $_ | Add-Member -NotePropertyName "extName" -NotePropertyValue $extName -Force + $_ | Add-Member -NotePropertyName "extCategory" -NotePropertyValue $extCategory -Force + $availableExtensions += $_ + return $true + } + + return @{ + hasMore = $hasMore + availableExtensions = $availableExtensions + } +} + function Get-PHPExtensions-From-Source { $availableExtensions = @{} try { $html_cat = Invoke-WebRequest -Uri $PECL_PACKAGES_URL $resultCat = $html_cat.Links | Where-Object { if (-not $_.href) { return $false } - if ($_.href -match '^/packages\.php\?catpid=\d+&catname=[A-Za-z+]+$') { - $extCategory = ($_.outerHTML -replace '<[^>]*>', '').Trim() - $availableExtensions[$extCategory] = @() - - # fetch the extensions from the category - $html = Invoke-WebRequest -Uri "$PECL_BASE_URL/$($_.href.TrimStart('/'))" - $resultLinks = $html.Links | Where-Object { - if (-not $_.href) { return $false } - if ($_.href -match '^/package/[A-Za-z0-9_]+$') { - $extName = ($_.href -replace '/package/', '').Trim() - $_ | Add-Member -NotePropertyName "extName" -NotePropertyValue $extName -Force - $_ | Add-Member -NotePropertyName "extCategory" -NotePropertyValue $extCategory -Force - $availableExtensions[$extCategory] += $_ - return $true - } - } - if ($availableExtensions[$extCategory].Count -eq 0) { - $availableExtensions.Remove($extCategory) - } - return $true + + if ($_.href -notmatch '^/packages\.php\?catpid=\d+&catname=([A-Za-z+]+)$') { + return $false } - return $false + + $page = 1 + $extCategory = $matches[1] -replace '\+', ' ' + do { + $hasMore = $false + $result = Get-Extension-Categories-By-Page -extCategory $extCategory -link $_.href -page $page + $availableExtensions[$extCategory] += $result.availableExtensions + $hasMore = $result.hasMore + $page++ + } while ($hasMore) + + if ($availableExtensions[$extCategory].Count -eq 0) { + $availableExtensions.Remove($extCategory) + } + return $true } $availableExtensions["XDebug"] = @( @{ @@ -1132,21 +1430,17 @@ 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" if ($availableExtensions.Count -eq 0) { $availableExtensions = Get-PHPExtensions-From-Source + $availableExtensions = [pscustomobject] $availableExtensions } } else { $availableExtensions = Get-PHPExtensions-From-Source + $availableExtensions = [pscustomobject] $availableExtensions } if ($availableExtensions.Count -eq 0) { @@ -1155,7 +1449,7 @@ function List-PHP-Extensions { } $availableExtensionsPartialList = @{} - $availableExtensions.GetEnumerator() | ForEach-Object { + $availableExtensions.PSObject.Properties | ForEach-Object { $searchResult = $_.Value if ($term) { if ($_.Key -notlike "*$term*") { @@ -1166,7 +1460,7 @@ function List-PHP-Extensions { } } if ($searchResult.Count -gt 0) { - $availableExtensionsPartialList[$_.Key] = $searchResult | Select-Object -Last 10 + $availableExtensionsPartialList[$_.Name] = $searchResult # | Select-Object -Last 10 } } @@ -1189,8 +1483,32 @@ function List-PHP-Extensions { $key = "$($_.Key) " $vals = ($_.Value | ForEach-Object { $_.extName }) -join ", " - $line = $key.PadRight($maxLineLength, '.') + " $vals" - Write-Host $line + $label = " $key" + $maxDescLength = $Host.UI.RawUI.WindowSize.Width - ($maxLineLength + 20) + if ($maxDescLength -lt 100) { $maxDescLength = 100 } + + $descLines = @() + $remaining = $vals + while ($remaining.Length -gt $maxDescLength) { + $breakPos = $remaining.LastIndexOf(' ', $maxDescLength) + if ($breakPos -lt 0) { $breakPos = $maxDescLength } + $descLines += $remaining.Substring(0, $breakPos) + $remaining = $remaining.Substring($breakPos).Trim() + } + if ($remaining) { $descLines += $remaining } + + if ($descLines.Count -eq 0) { + $line = $label.PadRight($maxLineLength, '.') + Write-Host $line + } else { + $line = $label.PadRight($maxLineLength, '.') + " $($descLines[0])" + Write-Host $line + + $indent = ' ' * ($maxLineLength + 1) + for ($i = 1; $i -lt $descLines.Count; $i++) { + Write-Host "$indent$($descLines[$i])" + } + } } $msg = "`nThis is a partial list. For a complete list, visit:" diff --git a/src/actions/install.ps1 b/src/actions/install.ps1 index 8bcddd3..c23ccb2 100644 --- a/src/actions/install.ps1 +++ b/src/actions/install.ps1 @@ -8,19 +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] - $formattedList += @{ href = $_.href; version = $version; fileName = $fileName } + $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 @@ -34,7 +39,7 @@ function Get-PHP-Versions-From-Url { } function Get-PHP-Versions { - param ($version) + param ($version, $arch = $null, $buildType = $null) try { $urls = Get-Source-Urls @@ -45,14 +50,18 @@ 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 { $_.arch -eq $arch } + } + if ($null -ne $buildType) { + $fetched = $fetched | Where-Object { $_.buildType -eq $buildType } + } if ($fetched.Count -eq 0) { continue } $fetchedVersions[$key] = @() - $fetched | ForEach-Object { + $fetched | ForEach-Object { if ($found -notcontains $_.fileName) { $fetchedVersions[$key] += $_ $found += $_.fileName @@ -97,6 +106,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 @@ -105,7 +116,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] @@ -167,37 +178,11 @@ function Extract-And-Configure { } -function getXdebugConfigV2 { - param($XDebugPath) - - return @" - - [xdebug] - ;zend_extension="$XDebugPath" - xdebug.remote_enable=1 - xdebug.remote_host=127.0.0.1 - xdebug.remote_port=9000 -"@ -} - -function getXdebugConfigV3 { - param($XDebugPath) - - return @" - - [xdebug] - ;zend_extension="$XDebugPath" - xdebug.mode=debug - xdebug.client_host=127.0.0.1 - xdebug.client_port=9003 -"@ -} - 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 +211,7 @@ function Configure-Opcache { } function Select-Version { - param ($matchingVersions) + param ($matchingVersions, $version, $arch = $null, $buildType = $null) $matchingVersionsPartialList = [ordered]@{} $matchingVersions.GetEnumerator() | ForEach-Object { @@ -238,7 +223,15 @@ function Select-Version { # There is exactly one key with one item $selectedVersionObject = $matchingKeys } else { - Write-Host "`nMatching PHP versions:" + $text = "`nMatching PHP versions: $version" + if ($null -ne $arch) { + $text += " $arch" + } + if ($null -ne $buildType) { + $text += " $buildType" + } + Write-Host $text + $index = 0 $matchingVersionsPartialList.GetEnumerator() | ForEach-Object { $key = $_.Key $versionsList = $_.Value @@ -247,8 +240,9 @@ function Select-Version { } Write-Host "`n$key versions:`n" $versionsList | ForEach-Object { - $versionItem = $_.version -replace '/downloads/releases/archives/|/downloads/releases/|php-|-Win.*|.zip', '' - Write-Host " $versionItem" + $_ | Add-Member -NotePropertyName "index" -NotePropertyValue $index -Force + Write-Host " [$index] $($_.version) $($_.arch) $($_.BuildType)" + $index++ } } @@ -256,18 +250,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,32 +273,32 @@ function Select-Version { } function Install-PHP { - param ($version) + param ($version, $arch = $null, $buildType = $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) { 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 (Is-Two-PHP-Versions-Equal -version1 $currentVersion -version2 $_) { $isCurrent = "(Current)" } - Write-Host " - $versionNumber $isCurrent" + $metaData = $metaData.Trim() + $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() @@ -316,7 +310,7 @@ function Install-PHP { } Write-Host "`nLoading the matching versions..." - $matchingVersions = Get-PHP-Versions -version $version + $matchingVersions = Get-PHP-Versions -version $version -arch $arch -buildType $buildType if ($matchingVersions.Count -eq 0) { $msg = "No matching PHP versions found for '$version', Check one of the following:" @@ -327,12 +321,12 @@ function Install-PHP { return @{ code = -1; message = $msg } } - $selectedVersionObject = Select-Version -matchingVersions $matchingVersions + $selectedVersionObject = Select-Version -matchingVersions $matchingVersions -version $version -arch $arch -buildType $buildType if (-not $selectedVersionObject) { 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 } @@ -345,13 +339,16 @@ function Install-PHP { } Write-Host "`nExtracting the downloaded zip ..." - Extract-And-Configure -path "$destination\$($selectedVersionObject.fileName)" -fileNamePath "$destination\$($selectedVersionObject.version)" + $phpDirectoryName = "$($selectedVersionObject.version)_$($selectedVersionObject.BuildType)_$($selectedVersionObject.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" + $cacheRefreshed = Refresh-Installed-PHP-Versions-Cache + return @{ code = 0; message = $message; color = "DarkGreen" } } catch { $logged = Log-Data -data @{ diff --git a/src/actions/list.ps1 b/src/actions/list.ps1 index 15df647..72eb9a6 100644 --- a/src/actions/list.ps1 +++ b/src/actions/list.ps1 @@ -9,22 +9,30 @@ 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] + + $filteredLinks += @{ + Version = ($_.href -replace '/downloads/releases/archives/|/downloads/releases/|php-|-nts|-Win.*|\.zip', '') + Arch = ($fileName -replace '.*\b(x64|x86)\b.*', '$1') + BuildType = if ($fileName -match 'nts') { 'NTS' } else { 'TS' } + Link = $_.href + } + } } # Return the filtered links (PHP version names) - $fetchedVersions = $fetchedVersions + ($filteredLinks | ForEach-Object { $_.href }) + $fetchedVersions = $fetchedVersions + $filteredLinks # ($filteredLinks | ForEach-Object { $_.href }) } - $arch = if (Is-OS-64Bit) { 'x64' } else { 'x86' } - $fetchedVersions = $fetchedVersions | Where-Object { $_ -match "$arch" } - - $fetchedVersionsGrouped = [ordered]@{ - 'Archives' = $fetchedVersions | Where-Object { $_ -match "archives" } - 'Releases' = $fetchedVersions | Where-Object { $_ -notmatch "archives" } + $fetchedVersionsGrouped = @{ + 'Archives' = $fetchedVersions | Where-Object { $_.Link -match "archives" } + 'Releases' = $fetchedVersions | Where-Object { $_.Link -notmatch "archives" } } if ($fetchedVersionsGrouped.Count -eq 0 -or @@ -33,8 +41,6 @@ function Get-From-Source { return @{} } - $cached = Cache-Data -cacheFileName "available_php_versions" -data $fetchedVersionsGrouped -depth 3 - return $fetchedVersionsGrouped } catch { $logged = Log-Data -data @{ @@ -47,23 +53,15 @@ function Get-From-Source { function Get-PHP-List-To-Install { try { - $cacheFile = "$CACHE_PATH\available_php_versions.json" - $fetchedVersionsGrouped = @{} - $useCache = $false + $fetchedVersionsGrouped = Get-OrUpdateCache -cacheFileName "available_php_versions" -compute { + Get-From-Source + } - if (Test-Path $cacheFile) { - $fileAgeHours = (New-TimeSpan -Start (Get-Item $cacheFile).LastWriteTime -End (Get-Date)).TotalHours - $useCache = ($fileAgeHours -lt $CACHE_MAX_HOURS) + if (-not $fetchedVersionsGrouped) { + return @{} } - if ($useCache) { - $fetchedVersionsGrouped = Get-Data-From-Cache -cacheFileName "available_php_versions" - if (-not $fetchedVersionsGrouped -or $fetchedVersionsGrouped.Count -eq 0) { - $fetchedVersionsGrouped = Get-From-Source - } - } else { - $fetchedVersionsGrouped = Get-From-Source - } + $fetchedVersionsGrouped = [pscustomobject] $fetchedVersionsGrouped return $fetchedVersionsGrouped } catch { @@ -76,7 +74,7 @@ function Get-PHP-List-To-Install { } function Get-Available-PHP-Versions { - param($term = $null) + param($term = $null, $arch = $null, $buildType = $null) try { Write-Host "`nLoading available PHP versions..." @@ -88,16 +86,20 @@ function Get-Available-PHP-Versions { return -1 } - $fetchedVersionsGroupedPartialList = @{} - $fetchedVersionsGrouped.GetEnumerator() | ForEach-Object { + $fetchedVersionsGroupedPartialList = [ordered]@{} + $fetchedVersionsGrouped.PSObject.Properties | ForEach-Object { $searchResult = $_.Value + if ($null -ne $arch) { + $searchResult = $searchResult | Where-Object { $_.Arch -eq $arch } + } + if ($null -ne $buildType) { + $searchResult = $searchResult | Where-Object { $_.BuildType -eq $buildType } + } if ($term) { - $searchResult = $searchResult | Where-Object { - $_ -like "*php-$term*" - } + $searchResult = $searchResult | Where-Object { $_.Version -like "$term*" } } if ($searchResult.Count -ne 0) { - $fetchedVersionsGroupedPartialList[$_.Key] = $searchResult | Select-Object -Last $LATEST_VERSION_COUNT + $fetchedVersionsGroupedPartialList[$_.Name] = $searchResult | Select-Object -Last $LATEST_VERSION_COUNT } } @@ -117,8 +119,8 @@ 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" + $versionNumber = "$($_.Version) ".PadRight(15, '.') + Write-Host " $versionNumber $($_.Arch) $($_.BuildType)" } } @@ -137,14 +139,11 @@ function Get-Available-PHP-Versions { } function Display-Installed-PHP-Versions { - param ($term) + param ($term = $null, $arch = $null, $buildType = $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 -buildType $buildType if ($installedPhp.Count -eq 0) { Write-Host "`nNo PHP versions found" @@ -152,7 +151,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 @@ -163,14 +162,23 @@ function Display-Installed-PHP-Versions { Write-Host "------------------" $duplicates = @() $installedPhp | ForEach-Object { - $versionNumber = $_ - if ($duplicates -notcontains $versionNumber) { - $duplicates += $versionNumber + $versionNumber = $_.Version + $versionID = "$($_.Version)_$($_.buildType)_$($_.Arch)" + if ($duplicates -notcontains $versionID) { + $duplicates += $versionID $isCurrent = "" - if ($currentVersion -eq $versionNumber) { + $metaData = "" + if ($_.Arch) { + $metaData += $_.Arch + " " + } + if ($_.BuildType) { + $metaData += $_.BuildType + } + if (Is-Two-PHP-Versions-Equal -version1 $currentVersion -version2 $_) { $isCurrent = "(Current)" } - Write-Host " $versionNumber $isCurrent" + $versionNumber = "$versionNumber ".PadRight(15, '.') + Write-Host " $versionNumber $metaData $isCurrent" } } return 0 @@ -186,12 +194,12 @@ function Display-Installed-PHP-Versions { function Get-PHP-Versions-List { - param($available = $false, $term = $null) + param($available = $false, $term = $null, $arch = $null, $buildType = $null) if ($available) { - $result = Get-Available-PHP-Versions -term $term + $result = Get-Available-PHP-Versions -term $term -arch $arch -buildType $buildType } else { - $result = Display-Installed-PHP-Versions -term $term + $result = Display-Installed-PHP-Versions -term $term -arch $arch -buildType $buildType } return $result 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/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 + } +} + + + diff --git a/src/actions/test.ps1 b/src/actions/test.ps1 index 112185d..244d579 100644 --- a/src/actions/test.ps1 +++ b/src/actions/test.ps1 @@ -75,17 +75,17 @@ function Run-Test-File { $rawDuration = $testResult.Duration.TotalSeconds $duration = Format-Seconds -totalSeconds $rawDuration if ($duration -ne -1) { - $durationText = $duration + $durationText = '{0,5:0.0}' -f $duration } $message = ( - 'Passed : {0,-4} | Failed : {1,-4} | Duration : {2,-6}' -f + 'Passed : {0,-4} | Failed : {1,-3} | Duration : {2,-5}' -f $testResult.PassedCount, $testResult.FailedCount, $durationText ) if ($coverageRaw) { $message = ( - 'Passed : {0,-4} | Failed : {1,-4} | Duration : {2,-6} | Coverage : {3,-7}' -f + 'Passed : {0,-4} | Failed : {1,-3} | Duration : {2,-5} | Coverage : {3,-7}' -f $testResult.PassedCount, $testResult.FailedCount, $durationText, @@ -160,6 +160,7 @@ function Run-Tests { $maxFileNameLength = ($testSummary.Name | Measure-Object -Maximum Length).Maximum $maxLineLength = $maxFileNameLength + 20 # padding + $testSummary = SortBy -data $testSummary -sortByColumn $options.sortBy $testSummary | ForEach-Object { $label = " - $($_.Name) " $line = $label.PadRight($maxLineLength, '.') + " $($_.Message)" @@ -188,4 +189,40 @@ function Run-Tests { } } +function SortBy { + param ($data, $sortByColumn = $null) + + if ($sortByColumn -ne $null) { + $direction = $sortByColumn -match "^-" + $sortByColumn = $sortByColumn -replace '-', '' + } + + switch ($sortByColumn) { + "duration" { + return $data | Sort-Object ` + @{ Expression = { + if ($null -eq $_.testResultData.duration) { + [double]::PositiveInfinity + } else { + [double]$_.testResultData.duration + } + }; Descending = $direction } + } + "coverage" { + return $data | Sort-Object ` + @{ Expression = { + if ($null -eq $_.testResultData.coverageRaw) { + [double]::PositiveInfinity + } else { + [double]$_.testResultData.coverageRaw + } + }; Descending = $direction } + } + "file" { + return $data | Sort-Object @{ Expression = { [string]$_.Name }; Descending = $direction } + } + } + + return $data; +} diff --git a/src/actions/uninstall.ps1 b/src/actions/uninstall.ps1 index 051ea8c..128adfe 100644 --- a/src/actions/uninstall.ps1 +++ b/src/actions/uninstall.ps1 @@ -1,48 +1,40 @@ - - -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 { + + $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"} + } + + if ($pathVersionObject.code -ne 0) { + return $pathVersionObject + } + + $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 + + $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 @{ + 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/actions/use.ps1 b/src/actions/use.ps1 index 2c3a936..8a1d995 100644 --- a/src/actions/use.ps1 +++ b/src/actions/use.ps1 @@ -1,98 +1,92 @@ -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 { + $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"} + } + + if ($pathVersionObject.code -ne 0) { + return $pathVersionObject + } + + $currentVersion = Get-Current-PHP-Version + if ($currentVersion -and $currentVersion.version) { + if (Is-Two-PHP-Versions-Equal -version1 $currentVersion -version2 $pathVersionObject) { + return @{ code = 0; message = "Already using PHP $($pathVersionObject.version)"; color = "DarkCyan"} + } + } + + $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) $($pathVersionObject.buildType) $($pathVersionObject.arch)").Trim() + + 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 8168e22..bf13ac8 100644 --- a/src/core/router.ps1 +++ b/src/core/router.ps1 @@ -21,7 +21,14 @@ 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.buildType) { + $text += " $($result.buildType)" + } + 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 @@ -43,8 +50,11 @@ function Invoke-PVMCurrent { function Invoke-PVMList{ param($arguments) + $arch = Resolve-Arch -arguments $arguments + $buildType = Resolve-BuildType -arguments $arguments + $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 -buildType $buildType return $result } @@ -71,7 +81,10 @@ function Invoke-PVMInstall { return -1 } - $result = Install-PHP -version $version + $arch = Resolve-Arch -arguments $arguments + $buildType = Resolve-BuildType -arguments $arguments + + $result = Install-PHP -version $version -arch $arch -buildType $buildType Display-Msg-By-ExitCode -result $result return 0 } @@ -126,7 +139,10 @@ function Invoke-PVMIni { return -1 } - $remainingArgs = if ($arguments.Count -gt 1) { $arguments[1..($arguments.Count - 1)] } else { @() } + $remainingArgs = if ($arguments.Count -gt 1) { + $arguments[1..($arguments.Count - 1)] | Where-Object { $_ -ne $arch } + } else { @() } + $exitCode = Invoke-PVMIniAction -action $action -params $remainingArgs return $exitCode @@ -141,8 +157,13 @@ function Invoke-PVMTest { coverage = $false tag = $null target = 75 + sortBy = $null } $files = $arguments | Where-Object { + if ($_ -match '^--sort=(.+)$') { + $options.sortBy = $Matches[1] + return $false + } if ($_ -match '^--tag=(.+)$') { $options.tag = $Matches[1] return $false @@ -500,7 +521,7 @@ function Get-Actions { command = "pvm test"; description = "Run tests."; usage = [ordered]@{ - USAGE = "pvm test [files] [--coverage[=]] [--verbosity=] [--tag=]" + USAGE = "pvm test [files] [--coverage[=]] [--verbosity=] [--tag=] [--sort=[coverage|duration|file]]" DESCRIPTION = @( "Runs the PVM test suite to verify that the installation and configuration" "are working correctly. This includes testing PHP version switching," @@ -513,11 +534,13 @@ function Get-Actions { "pvm test --coverage .............. Runs all tests and generates coverage report (target: 75%)" "pvm test --coverage=80 ........... Runs all tests and generates coverage report (target: 80%)" "pvm test --tag=unit .............. Runs only tests with tag 'unit'" + "pvm test --sort=coverage ......... Runs all tests and sort results by coverage" ) ARGUMENTS = @( "files ............................ Run only specific test files (e.g. use, install)" ) OPTIONS = @( + "--sort=[coverage|duration|file] .. Sort tests results by coverage, duration or file names" "--coverage[=] ............ Generate coverage report with optional target percentage (default: 75%)" "--verbosity= .......... Set verbosity level (None, Normal (Default), Detailed, Diagnostic)" "--tag= ...................... Run only tests with specific tag" diff --git a/src/functions/helpers.ps1 b/src/functions/helpers.ps1 index 7348f5f..7836d91 100644 --- a/src/functions/helpers.ps1 +++ b/src/functions/helpers.ps1 @@ -3,21 +3,35 @@ function Get-Zend-Extensions-List { return @('xdebug', 'opcache') } -function Get-Data-From-Cache { +function Can-Use-Cache { param ($cacheFileName) - $path = "$CACHE_PATH\$cacheFileName.json" - $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 + $path = "$CACHE_PATH\$cacheFileName.json" + $useCache = $false + + if (Test-Path $path) { + $fileAgeHours = (New-TimeSpan -Start (Get-Item $path).LastWriteTime -End (Get-Date)).TotalHours + $useCache = ($fileAgeHours -lt $CACHE_MAX_HOURS) } - return $list + + return $useCache + } catch { + $logged = Log-Data -data @{ + header = "$($MyInvocation.MyCommand.Name) - Failed to get data from cache" + exception = $_ + } + + return $false + } +} + +function Get-Data-From-Cache { + param ($cacheFileName) + + try { + $jsonData = Get-Content "$CACHE_PATH\$cacheFileName.json" | ConvertFrom-Json + return $jsonData } catch { $logged = Log-Data -data @{ header = "$($MyInvocation.MyCommand.Name) - Failed to get data from cache" @@ -49,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 { @@ -123,22 +158,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) @@ -353,6 +372,10 @@ function Format-Seconds { param ($totalSeconds) try { + if ($totalSeconds -ne $null) { + $totalSeconds = [Single] $totalSeconds + } + if ($null -eq $totalSeconds -or $totalSeconds -lt 0) { $totalSeconds = 0 } @@ -383,3 +406,96 @@ function Format-Seconds { function Is-OS-64Bit { return [System.Environment]::Is64BitOperatingSystem } + +function Resolve-BuildType { + param ($arguments, $choseDefault = $false) + + $buildType = $arguments | Where-Object { @('ts', 'nts') -contains $_ } | Select-Object -First 1 + + if ($null -eq $buildType -and $choseDefault) { + $buildType = "ts"; + } + + if ($buildType -ne $null) { + $buildType = $buildType.ToLower() + } + + return $buildType +} + +function Resolve-Arch { + param ($arguments, $choseDefault = $false) + + $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' } + } + + if ($arch -ne $null) { + $arch = $arch.ToLower() + } + + return $arch +} + +function Get-PHPInstallInfo { + param ($path) + + $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 + } + + return @{ + Version = $dll.VersionInfo.ProductVersion + Arch = Get-BinaryArchitecture-From-DLL -path $dll.FullName + BuildType = $buildType + 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" } + } +} + +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 diff --git a/tests/common.tests.ps1 b/tests/common.tests.ps1 index 31554ae..35c3880 100644 --- a/tests/common.tests.ps1 +++ b/tests/common.tests.ps1 @@ -1,6 +1,8 @@ -# Load required modules and functions -. "$PSScriptRoot\..\src\actions\common.ps1" +BeforeAll { + $global:CACHE_PATH = "TestDrive:\cache" + New-Item -ItemType Directory -Path $global:CACHE_PATH -Force | Out-Null +} Describe "Get-Source-Urls" { It "Should return correct URL structure" { @@ -49,6 +51,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" { @@ -74,7 +84,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 @@ -91,63 +101,93 @@ 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] } } 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 } 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" + } + + 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" + } + + It "Should filter the right build type input" { + Mock Get-OrUpdateCache { + return @( + @{version = "5.6"; arch = "x64"; buildType = "nts"} + @{version = "5.6"; arch = "x64"; buildType = "ts"} + @{version = "7.4"; arch = "x64"; buildType = "nts"} + @{version = "8.0"; arch = "x64"; buildType = "nts"} + @{version = "8.0"; arch = "x64"; buildType = "ts"} + ) + } + + $result = Get-Installed-PHP-Versions -buildType "nts" + + $result.Count | Should -Be 3 + $result[0].version | Should -Be "5.6" + $result[1].version | Should -Be "7.4" + $result[2].version | Should -Be "8.0" } } 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" } - Mock Log-Data { return $true } + It "Should return empty array and log error when Get-Installed-PHP-Versions-From-Directory throws exception" { + Mock Get-OrUpdateCache { throw "Test exception" } + Mock Log-Data { return 0 } $result = Get-Installed-PHP-Versions $result.Count | Should -Be 0 @@ -166,7 +206,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,66 +214,100 @@ 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 { } - Mock Get-PHP-Path-By-Version { return "C:\php\8.1" } - $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" } + + 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" { 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" { 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 @@ -243,7 +317,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 @@ -260,46 +334,44 @@ 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 } } 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 @@ -311,31 +383,209 @@ 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" +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 0 } + + $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 0 } + + $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 0 } + + $result = Refresh-Installed-PHP-Versions-Cache + $result | Should -Be -1 + } + } +} + +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"} + } + } - Mock Get-Installed-PHP-Versions { - return @("7.4", "8.0", "8.1", "8.1.9", "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'} + } } - Mock Log-Data { return $true } + $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 } - # 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" + $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 - $pvmSetup | Should -Be $true - $installedVersions -contains "8.1" | Should -Be $true - $matchingVersions -contains "8.1" | Should -Be $true - $isInstalled | Should -Be $true + Assert-MockCalled Get-All-Subdirectories -Exactly 1 -ParameterFilter { + $path -eq "$STORAGE_PATH\php" + } } } -} \ No newline at end of file +} diff --git a/tests/current.tests.ps1 b/tests/current.tests.ps1 index 1957242..057e8da 100644 --- a/tests/current.tests.ps1 +++ b/tests/current.tests.ps1 @@ -1,381 +1,432 @@ -# 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 - } - } -} + +BeforeAll { + # Mock dependencies + $global:LOG_ERROR_PATH = "TestDrive:\logs\error.log" + $global:PHP_CURRENT_VERSION_PATH = "TestDrive:\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 + } + + 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 + } + } +} + +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" { + # 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 + Assert-MockCalled Get-PHP-Status -Times 1 -ParameterFilter { $phpPath -eq "C:\php\8.2.0" } + } + } + + Context "When PHP current version path does not exist" { + + 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 + + # 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 Get-Item { throw "Path does not exist" } + Mock Log-Data { return 0 } + + # 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 + } + } +} diff --git a/tests/helpers.tests.ps1 b/tests/helpers.tests.ps1 index 98df322..7ede0b6 100644 --- a/tests/helpers.tests.ps1 +++ b/tests/helpers.tests.ps1 @@ -1,5 +1,3 @@ -# Load required modules and functions -. "$PSScriptRoot\..\src\functions\helpers.ps1" BeforeAll { @@ -178,7 +176,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" } @@ -310,37 +307,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" { @@ -699,7 +665,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" { @@ -708,3 +674,679 @@ 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 + } + } +} + +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" { + $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 + } + } +} + +Describe "Resolve-BuildType" { + Context "When searching in arguments" { + It "Returns nts when nts is in arguments" { + $arguments = @("some_arg", "nts", "another_arg") + $result = Resolve-BuildType -arguments $arguments + $result | Should -Be "nts" + } + + It "Returns ts when ts is in arguments" { + $arguments = @("some_arg", "ts", "another_arg") + $result = Resolve-BuildType -arguments $arguments + $result | Should -Be "ts" + } + + It "Returns first matching architecture when multiple are present" { + $arguments = @("ts", "nts", "other") + $result = Resolve-BuildType -arguments $arguments + $result | Should -Be "ts" + } + + It "Returns null when no matching architecture in arguments" { + $arguments = @("some_arg", "another_arg", "third_arg") + $result = Resolve-BuildType -arguments $arguments + $result | Should -BeNullOrEmpty + } + } + + Context "Case insensitivity" { + It "Returns lowercase nts when uppercase NTS provided" { + $arguments = @("NTS") + $result = Resolve-BuildType -arguments $arguments + $result | Should -Be "nts" + } + + It "Returns lowercase TS when mixed case TS provided" { + $arguments = @("TS") + $result = Resolve-BuildType -arguments $arguments + $result | Should -Be "TS" + } + } + + Context "With default choice" { + It "Returns ts as default when choseDefault is true" { + $arguments = @("some_arg", "other_arg") + + $result = Resolve-BuildType -arguments $arguments -choseDefault $true + $result | Should -Be "ts" + } + + It "Returns argument arch even when choseDefault is true" { + $arguments = @("nts", "some_arg") + + $result = Resolve-BuildType -arguments $arguments -choseDefault $true + $result | Should -Be "nts" + } + } + + Context "With empty or null inputs" { + It "Returns null when arguments array is empty and choseDefault is false" { + $arguments = @() + $result = Resolve-BuildType -arguments $arguments -choseDefault $false + $result | Should -BeNullOrEmpty + } + + It "Returns default when arguments array is empty and choseDefault is true" { + $arguments = @() + + $result = Resolve-BuildType -arguments $arguments -choseDefault $true + $result | Should -Be "ts" + } + + It "Returns null when arguments is null" { + $result = Resolve-BuildType -arguments $null + $result | Should -BeNullOrEmpty + } + } +} + +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 + } + } +} + +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 + } + } +} + + +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" + } + } +} diff --git a/tests/ini.tests.ps1 b/tests/ini.tests.ps1 index f3c95e1..e185180 100644 --- a/tests/ini.tests.ps1 +++ b/tests/ini.tests.ps1 @@ -1,5 +1,3 @@ -# Load required modules and functions -. "$PSScriptRoot\..\src\actions\ini.ps1" BeforeAll { $testDrivePath = Get-PSDrive TestDrive | Select-Object -ExpandProperty Root @@ -7,6 +5,9 @@ BeforeAll { $extDirectory = Join-Path $testDrivePath "ext" $testBackupPath = "$testIniPath.bak" + $global:CACHE_PATH = "TestDrive:\cache" + New-Item -ItemType Directory -Path $global:CACHE_PATH -Force | Out-Null + Mock Write-Host {} function Reset-Ini-Content { @@ -78,7 +79,7 @@ max_execution_time = 30 } } -Describe "Add-Missing-PHPExtension" { +Describe "Add-Missing-PHPExtension-To-Ini" { BeforeEach { Reset-Ini-Content Remove-Item $testBackupPath -ErrorAction SilentlyContinue @@ -86,13 +87,17 @@ Describe "Add-Missing-PHPExtension" { It "Returns -1 when current PHP version is null" { Mock Get-Current-PHP-Version { return @{ version = $null; path = $null } } - $result = Add-Missing-PHPExtension -iniPath $testIniPath -extName "curl" + $result = Add-Missing-PHPExtension-To-Ini -iniPath $testIniPath -extFileName "curl" $result | Should -Be -1 } It "Adds and configures xdebug in ini file" { - $result = Add-Missing-PHPExtension -iniPath $testIniPath -extName "xdebug" + Mock Test-Path { return $true } + $result = Add-Missing-PHPExtension-To-Ini -iniPath $testIniPath -extFileName "php_xdebug.dll" $result | Should -Be 0 + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { + $Object -eq "- Extension 'php_xdebug.dll' already exists in php.ini" + } } It "Adds any extension to ini file" { @@ -100,9 +105,14 @@ Describe "Add-Missing-PHPExtension" { zend_extension=php_opcache.dll extension=php_mbstring.dll "@ | Set-Content $testIniPath - $result = Add-Missing-PHPExtension -iniPath $testIniPath -extName "curl" + + Mock Test-Path { return $true } + $result = Add-Missing-PHPExtension-To-Ini -iniPath $testIniPath -extFileName "php_curl.dll" $result | Should -Be 0 (Get-Content $testIniPath) -match "extension=php_curl.dll" | Should -Be $true + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { + $Object -eq "- 'php_curl.dll' added successfully." + } } It "Adds any extension in disabled state to ini file" { @@ -110,7 +120,9 @@ extension=php_mbstring.dll zend_extension=php_opcache.dll ;extension=php_mbstring.dll "@ | Set-Content $testIniPath - $result = Add-Missing-PHPExtension -iniPath $testIniPath -extName "curl" -enable $false + + Mock Test-Path { return $true } + $result = Add-Missing-PHPExtension-To-Ini -iniPath $testIniPath -extFileName "php_curl.dll" -enable $false $result | Should -Be 0 (Get-Content $testIniPath) -match ";extension=php_curl.dll" | Should -Be $true } @@ -120,8 +132,10 @@ zend_extension=php_opcache.dll zend_extension=php_opcache.dll extension=php_mbstring.dll "@ | Set-Content $testIniPath + + Mock Test-Path { return $true } Mock Get-Current-PHP-Version { return @{ version = "7.1.0"; path = "TestDrive:\php\7.1.0" } } - $result = Add-Missing-PHPExtension -iniPath $testIniPath -extName "curl" + $result = Add-Missing-PHPExtension-To-Ini -iniPath $testIniPath -extFileName "php_curl.dll" $result | Should -Be 0 (Get-Content $testIniPath) -match "extension=php_curl.dll" | Should -Be $true } @@ -130,15 +144,52 @@ extension=php_mbstring.dll @" extension=php_mbstring.dll "@ | Set-Content $testIniPath + + Mock Test-Path { return $true } Mock Get-Current-PHP-Version { return @{ version = "7.1.0"; path = "TestDrive:\php\7.1.0" } } - $result = Add-Missing-PHPExtension -iniPath $testIniPath -extName "opcache" + $result = Add-Missing-PHPExtension-To-Ini -iniPath $testIniPath -extFileName "php_opcache.dll" $result | Should -Be 0 (Get-Content $testIniPath) -match "zend_extension=php_opcache.dll" | Should -Be $true } + It "Returns -1 for non-existent ini file" { + Mock Test-Path { return $false } + $result = Add-Missing-PHPExtension-To-Ini -iniPath "nonexistent.ini" -extFileName "php_curl.dll" + $result | Should -Be -1 + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { + $Object -eq "`nphp.ini file not found: nonexistent.ini" + } + } + + It "Returns -1 when extension directory doesn't exist" { + Mock Test-Path -ParameterFilter { $Path -eq $testIniPath } { return $true } + Mock Test-Path -ParameterFilter { $Path -eq $extDirectory } { return $false } + + $result = Add-Missing-PHPExtension-To-Ini -iniPath $testIniPath -extFileName "php_curl.dll" + + $result | Should -Be -1 + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { + $Object -eq "`nExtensions directory not found: $extDirectory" + } + } + + It "Returns -1 when extension file doesn't exist" { + Mock Test-Path -ParameterFilter { $Path -eq $testIniPath } { return $true } + Mock Test-Path -ParameterFilter { $Path -eq $extDirectory } { return $true } + Mock Test-Path -ParameterFilter { $Path -eq "$extDirectory\php_curl.dll" } { return $false } + + $result = Add-Missing-PHPExtension-To-Ini -iniPath $testIniPath -extFileName "php_curl.dll" + + $result | Should -Be -1 + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { + $Object -eq "`nExtension file not found: php_curl.dll" + } + } + It "Handles exception gracefully" { - Mock Get-Content { throw "Access denied" } - $result = Add-Missing-PHPExtension -iniPath $testIniPath -extName "curl" + Mock Log-Data { return 0 } + Mock Backup-IniFile { throw "Access denied" } + $result = Add-Missing-PHPExtension-To-Ini -iniPath $testIniPath -extFileName "curl" $result | Should -Be -1 } } @@ -190,6 +241,330 @@ Describe "Get-XDebug-FROM-URL Tests" { $result | Should -Be @() } } + +Describe "Filter-Extension-Links-From-URL" { + It "Returns filtered links for given extension" { + Mock Invoke-WebRequest -ParameterFilter { $Uri -eq "$PECL_PACKAGE_ROOT_URL/memcache" } -MockWith { + return @{ + Content = "Mocked memcache content" + Links = @( + @{ href = "/package/memcache/3.4.0/windows" }, + @{ href = "/package/memcache/3.3.0/windows" } + @{ href = "/package/memcache/3.2.0/windows" } + @{ href = $null } + @{ href = "random_link" } + ) + } + } + + $result = Filter-Extension-Links-From-URL -extName "memcache" + + $result.Count | Should -Be 3 + $result[0].href | Should -Be "/package/memcache/3.4.0/windows" + $result[1].href | Should -Be "/package/memcache/3.3.0/windows" + $result[2].href | Should -Be "/package/memcache/3.2.0/windows" + } +} + +Describe "Get-Packages-From-Source-Links Tests" { + It "Returns formatted list for matching packages" { + Mock Log-Data { return 0 } + Mock Invoke-WebRequest -ParameterFilter { $Uri -eq "$PECL_PACKAGE_ROOT_URL/memcache/3.4.0/windows" } -MockWith { + return @{ + Content = "Mocked PHP memcache 3.4.0 content" + Links = @( + @{ href = "other_link" }, + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/memcache/3.4.0/php_memcache-3.4.0-8.2-ts-vs16-x86.zip" }, + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/memcache/3.4.0/php_memcache-3.4.0-8.2-ts-vs16-x64.zip" } + ) + } + } + Mock Invoke-WebRequest -ParameterFilter { $Uri -eq "$PECL_PACKAGE_ROOT_URL/memcache/3.3.0/windows" } -MockWith { + return @{ + Content = "Mocked PHP memcache 3.4.0 content" + Links = @( + @{ href = "other_link" }, + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/memcache/3.3.0/php_memcache-3.3.0-8.2-ts-vs16-x86.zip" }, + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/memcache/3.3.0/php_memcache-3.3.0-8.2-ts-vs16-x64.zip" } + ) + } + } + Mock Invoke-WebRequest -ParameterFilter { $Uri -eq "$PECL_PACKAGE_ROOT_URL/memcache/3.2.0/windows" } -MockWith { + return @{ + Content = "Mocked PHP memcache 3.4.0 content" + Links = @( + @{ href = "other_link" }, + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/memcache/3.2.0/php_memcache-3.2.0-8.2-nts-vs16-x86.zip" }, + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/memcache/3.2.0/php_memcache-3.2.0-8.2-ts-x64.zip" } + ) + } + } + + $result = Get-Packages-From-Source-Links -extName "memcache" -version "8.2" -links @( + @{ href = "/package/memcache/3.4.0/windows" }, + @{ href = "/package/memcache/3.3.0/windows" }, + @{ href = "/package/memcache/3.2.0/windows" } + ) + + $result.Count | Should -Be 6 + $result[0].extVersion | Should -Be "3.4.0" + $result[1].arch | Should -Be "x64" + $result[2].arch | Should -Be "x86" + $result[3].extVersion | Should -Be "3.3.0" + $result[4].buildType | Should -Be "NTS" + $result[5].compiler | Should -Be "unknown" + } + + It "Handles exception gracefully" { + Mock Invoke-WebRequest { throw "Network error" } + + $result = Get-Packages-From-Source-Links -extName "memcache" -version "8.2" -links @( @{ href = "/package/memcache/3.4.0/windows" } ) + + $result.Count | Should -Be 0 + } +} + +Describe "Get-Extension-Matching-Categories-By-Page Tests" { + It "Returns matching categories links by page" { + Mock Invoke-WebRequest -ParameterFilter { $Uri -eq "$($PECL_PACKAGES_URL)?catpid=3&catname=Caching&pageID=1" } -MockWith { + return @{ + Content = "Mocked PHP extension Caching content" + Links = @( + @{ href = "/package/APC" } + @{ href = "/package/APCu" } + @{ href = "/package/memcache" } + @{ href = "/package/memcached" } + ) + } + } + + $result = Get-Extension-Matching-Categories-By-Page -extName "mem" -link "/packages.php?catpid=3&catname=Caching" -page 1 + + $result.resultLinks.Count | Should -Be 2 + $result.resultLinks[0].href | Should -Be "/package/memcache" + $result.resultLinks[1].href | Should -Be "/package/memcached" + $result.hasMore | Should -Be $false + } + + It "Sets hasMore to true when next page link exists" { + Mock Invoke-WebRequest -ParameterFilter { $Uri -eq "$($PECL_PACKAGES_URL)?catpid=3&catname=Caching&pageID=1" } -MockWith { + return @{ + Content = "Mocked PHP extension Caching content" + Links = @( + @{ href = $null } + @{ href = "/packages.php?catpid=3&catname=Caching&pageID=2" } + @{ href = "/package/APC" } + @{ href = "/package/APCu" } + @{ href = "/package/memcache" } + @{ href = "/package/memcached" } + ) + } + } + + $result = Get-Extension-Matching-Categories-By-Page -extName "mem" -link "/packages.php?catpid=3&catname=Caching" -page 1 + + $result.hasMore | Should -Be $true + } +} + +Describe "Get-Extension-Matching-Categories Tests" { + BeforeAll { + Mock Invoke-WebRequest -ParameterFilter { $Uri -eq $PECL_PACKAGES_URL } -MockWith { + return @{ + Content = "Mocked PHP extensions content" + Links = @( + @{ href = $null } + @{ href = "random_link" } + @{ href = "/packages.php?catpid=1&catname=Authentication"; + outerHTML = 'Authentication' } + @{ href = "/packages.php?catpid=3&catname=Caching"; + outerHTML = 'Caching' } + @{ href = "/packages.php?catpid=7&catname=EmptyCat"; + outerHTML = 'EmptyCat' } + ) + } + } + Mock Get-Extension-Matching-Categories-By-Page { + param ($link) + if ($link -eq "/packages.php?catpid=1&catname=Authentication") { + return @{ hasMore = $false; resultLinks = @() } + } + if ($link -eq "/packages.php?catpid=3&catname=Caching") { + return @{ + hasMore = $false + resultLinks = @( + @{ href = "/package/memcache" } + @{ href = "/package/memcached" } + ) + } + } + if ($link -eq "/packages.php?catpid=7&catname=EmptyCat") { + return @{ hasMore = $false; resultLinks = @() } + } + } + } + + It "Returns matching categories links" { + $result = Get-Extension-Matching-Categories -extName "mem" + + $result.Count | Should -Be 2 + $result[0].href | Should -Be "/package/memcache" + $result[1].href | Should -Be "/package/memcached" + } + + It "Loops for next page when hasMore is true" { + Mock Get-Extension-Matching-Categories-By-Page { + if ($link -eq "/packages.php?catpid=3&catname=Caching") { + if ($page -eq 1) { + return @{ hasMore = $true; resultLinks = @( @{ href = "/package/memcache" } ) } + } else { + return @{ hasMore = $false; resultLinks = @( @{ href = "/package/memcached" } ) } + } + } + } + + $result = Get-Extension-Matching-Categories -extName "mem" + + $result.Count | Should -Be 2 + } +} + +Describe "Get-Extension-Links-From-URL Tests" { + It "Returns filtered links" { + Mock Filter-Extension-Links-From-URL { + return @( + @{ href = "/package/memcache/3.4.0/windows" }, + @{ href = "/package/memcache/3.3.0/windows" }, + @{ href = "/package/memcache/3.2.0/windows" } + ) + } + + $result = Get-Extension-Links-From-URL -extName "memcache" -version "8.2" + + $result.extName | Should -Be "memcache" + $result.links.Count | Should -Be 3 + } + + Context "When extension has no direct link" { + BeforeEach { + Mock Can-Use-Cache { return $false } + Mock Filter-Extension-Links-From-URL -ParameterFilter { $extName -eq "mem" } { throw "Error" } + Mock Filter-Extension-Links-From-URL -ParameterFilter { $extName -eq "memcache" } { + @{ href = "/package/memcache/3.4.0/windows" }, + @{ href = "/package/memcache/3.3.0/windows" }, + @{ href = "/package/memcache/3.2.0/windows" } + } + } + + It "Returns null when no matching categories links found" { + Mock Get-Extension-Matching-Categories { return @() } + + $result = Get-Extension-Links-From-URL -extName "mem" -version "8.2" + + $result | Should -Be $null + } + + It "Takes the only link found" { + Mock Get-Extension-Matching-Categories { return @( @{ href = "/package/memcache" } ) } + + $result = Get-Extension-Links-From-URL -extName "mem" -version "8.2" + + $result.extName | Should -Be "memcache" + $result.links.Count | Should -Be 3 + } + } + Context "When multiple matching categories links found" { + BeforeEach { + Mock Get-Extension-Matching-Categories { return @( + @{ href = "/package/memcache" }, + @{ href = "/package/memcached" } + ) } + } + + It "Prompts user to select link when multiple found and returns selected" { + Mock Read-Host -ParameterFilter { $Prompt -eq "`nInsert the [number] you want to install" } -MockWith { return "0" } + + $result = Get-Extension-Links-From-URL -extName "mem" -version "8.2" + + $result.extName | Should -Be "memcache" + $result.links.Count | Should -Be 3 + } + + It "Returns null when user skips selection" { + Mock Read-Host -ParameterFilter { $Prompt -eq "`nInsert the [number] you want to install" } -MockWith { return "" } + + $result = Get-Extension-Links-From-URL -extName "mem" -version "8.2" + + $result | Should -Be $null + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { + $Object -eq "`nInstallation cancelled" + } + } + + It "Reprompts user when typing invalid choice" { + $script:callCount = 0 + Mock Read-Host -ParameterFilter { $Prompt -eq "`nInsert the [number] you want to install" } -MockWith { + $script:callCount++ + if ($script:callCount -eq 1) { return "A" } + if ($script:callCount -eq 2) { return "-1" } + else { return "0" } + } + + $result = Get-Extension-Links-From-URL -extName "mem" -version "8.2" + + $result.extName | Should -Be "memcache" + $result.links.Count | Should -Be 3 + } + } +} + +Describe "Get-Extension-From-URL Tests" { + BeforeAll { + + } + BeforeEach { + Mock Write-Host { } + } + + It "Should parse extension versions correctly" { + Mock Can-Use-Cache { return $false } + Mock Get-Extension-Links-From-URL { + return @{ + extName = "memcache" + links = @( + @{ href = "/package/memcache/3.4.0/windows" }, + @{ href = "/package/memcache/3.3.0/windows" }, + @{ href = "/package/memcache/3.2.0/windows" } + ) + } + } + Mock Get-Packages-From-Source-Links { + return @( + @{ href = "/package/memcache/3.4.0/windows"; version = "8.2"; extVersion = "3.4.0"; fileName = "/memcache/3.4.0/php_memcache-3.4.0-8.2-ts-vs16-x64.zip" } + @{ href = "/package/memcache/3.3.0/windows"; version = "8.2"; extVersion = "3.3.0"; fileName = "/memcache/3.3.0/php_memcache-3.3.0-8.2-ts-vs16-x64.zip" } + @{ href = "/package/memcache/3.2.0/windows"; version = "8.2"; extVersion = "3.2.0"; fileName = "/memcache/3.2.0/php_memcache-3.2.0-8.2-ts-vs16-x64.zip" } + ) + } + $result = Get-Extension-From-URL -extName "memcache" -version "8.2" + + $result.data.Count | Should -Be 3 + $result.data[0].extVersion | Should -Be "3.4.0" + $result.data[1].extVersion | Should -Be "3.3.0" + $result.data[2].extVersion | Should -Be "3.2.0" + } + + It "Returns null when no version found for extension" { + Mock Get-Extension-Links-From-URL { return $null } + + $result = Get-Extension-From-URL -extName "cache" -version "8.2" + + Assert-MockCalled Write-Host -Times 1 -ParameterFilter { + $Object -eq "`nNo versions found for cache" + } + $result.data | Should -Be $null + } +} + Describe "Backup-IniFile" { It "Creates a backup when none exists" { Remove-Item $testBackupPath -ErrorAction SilentlyContinue @@ -316,7 +691,7 @@ Describe "Set-IniSetting" { opcache.protect_memory=1 "@ | Set-Content $testIniPath - Mock Read-Host -ParameterFilter { $Prompt -eq "`nSelect a number" } -MockWith { return 1 } + Mock Read-Host -ParameterFilter { $Prompt -eq "`nSelect a number" } -MockWith { return 0 } Mock Read-Host -ParameterFilter { $Prompt -eq "Enter new value for 'memory_limit'" } -MockWith { return "4G" } Set-IniSetting -iniPath $testIniPath -key "memory" | Should -Be 0 @@ -329,7 +704,7 @@ opcache.protect_memory=1 opcache.protect_memory=1 "@ | Set-Content $testIniPath - Mock Read-Host -ParameterFilter { $Prompt -eq "`nSelect a number" } -MockWith { return 1 } + Mock Read-Host -ParameterFilter { $Prompt -eq "`nSelect a number" } -MockWith { return 0 } Set-IniSetting -iniPath $testIniPath -key "memory=2G" | Should -Be 0 (Get-Content $testIniPath) -match "^memory_limit\s*=\s*2G" | Should -Be $true @@ -446,7 +821,7 @@ extension=sqlite3 @{ BaseName = "sqlite3"; Name = "sqlite3.dll"; FullName = "$extDirectory\sqlite3.dll" } ) } - Mock Read-Host -ParameterFilter { $Prompt -eq "`nSelect a number" } -MockWith { return 1 } + Mock Read-Host -ParameterFilter { $Prompt -eq "`nSelect a number" } -MockWith { return 0 } Enable-IniExtension -iniPath $testIniPath -extName "sql" | Should -Be 0 @@ -467,7 +842,7 @@ extension=sqlite3 $script:callCount++ if ($script:callCount -eq 1) { 'A' } if ($script:callCount -eq 2) { -1 } - else { 4 } + else { 3 } } Mock Get-ChildItem { @@ -554,7 +929,7 @@ extension=pgsql @{ BaseName = "sqlite3"; Name = "sqlite3.dll"; FullName = "$extDirectory\sqlite3.dll" } ) } - Mock Read-Host -ParameterFilter { $Prompt -eq "`nSelect a number" } -MockWith { return 1 } + Mock Read-Host -ParameterFilter { $Prompt -eq "`nSelect a number" } -MockWith { return 0 } Disable-IniExtension -iniPath $testIniPath -extName "sql" | Should -Be 0 @@ -575,7 +950,7 @@ extension=pgsql $script:callCount++ if ($script:callCount -eq 1) { 'A' } if ($script:callCount -eq 2) { -1 } - else { 4 } + else { 3 } } Mock Get-ChildItem { @@ -825,6 +1200,9 @@ Describe "Install-Extension" { "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-ts-vs16-x86.zip" = @{ Content = "Mocked PHP curl 1.4.0 zip content" } + "$PECL_WIN_EXT_DOWNLOAD_URL/courierauth/1.4.0/php_courierauth-1.4.0-8.2-ts-vs16-x64.zip" = @{ + Content = "Mocked PHP courierauth 1.4.0 zip content" + } "$PECL_PACKAGE_ROOT_URL/curl/2.1.0/windows" = @{ Content = "Mocked PHP curl 2.1.0 content" Links = @() @@ -879,17 +1257,44 @@ Describe "Install-Extension" { return "y" } Mock Move-Item { } - Mock Add-Missing-PHPExtension { return -1 } + Mock Add-Missing-PHPExtension-To-Ini { return -1 } + + $code = Install-Extension -iniPath $testIniPath -extName "curl" + $code | Should -Be -1 + } + + It "Returns -1 when no extension matching installed php version (arch & build type)" { + Mock Get-Current-PHP-Version { return @{ version = "8.2.0"; path = "TestDrive:\php\8.2.0"; arch = "x64"; buildType = "ts" }} + Mock Get-Extension-From-URL { + return @{ + extName = "curl" + data = @( + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.1/php_curl-1.4.1-8.2-ts-vs16-x86.zip"; arch = "x86"; buildType = "ts" ; version = "8.2"; extVersion = "1.4.0" } + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.1/php_curl-1.4.1-8.2-nts-vs16-x86.zip"; arch = "x86"; buildType = "nts" ; version = "8.2"; extVersion = "1.4.0" } + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-nts-vs16-x64.zip"; arch = "x64"; buildType = "nts" ; version = "8.2"; extVersion = "1.4.0" } + ) + } + } $code = Install-Extension -iniPath $testIniPath -extName "curl" $code | Should -Be -1 } It "Installs extension successfully" { - Mock Test-Path { return $false } - Mock Read-Host -ParameterFilter { $Prompt -eq "`nphp_curl.dll already exists. Would you like to overwrite it? (y/n)" } -MockWith { - return "y" + Mock Get-Current-PHP-Version { return @{ version = "8.2.0"; path = "TestDrive:\php\8.2.0"; arch = "x86"; buildType = "ts" }} + Mock Get-Extension-From-URL { + return @{ + extName = "curl" + data = @( + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-ts-vs16-x86.zip"; arch = "x86"; buildType = "ts" ; version = "8.2"; extVersion = "1.4.0"; fileName = "php_curl-1.4.0-8.2-ts-vs16-x86.zip"; outerHTML = "8.2 Thread Safe (TS) x86" } + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-nts-vs16-x86.zip"; arch = "x86"; buildType = "nts" ; version = "8.2"; extVersion = "1.4.0"; fileName = "php_curl-1.4.0-8.2-nts-vs16-x86.zip"; outerHTML = "8.2 Non Thread Safe (NTS) x86" } + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-ts-vs16-x64.zip"; arch = "x64"; buildType = "ts" ; version = "8.2"; extVersion = "1.4.0"; fileName = "php_curl-1.4.0-8.2-ts-vs16-x64.zip"; outerHTML = "8.2 Thread Safe (TS) x64" } + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-nts-vs16-x64.zip"; arch = "x64"; buildType = "nts" ; version = "8.2"; extVersion = "1.4.0"; fileName = "php_curl-1.4.0-8.2-nts-vs16-x64.zip"; outerHTML = "8.2 Non Thread Safe (NTS) x64" } + ) + } } + Mock Test-Path { return $false } + Mock Add-Missing-PHPExtension-To-Ini { return 0 } $code = Install-Extension -iniPath $testIniPath -extName "curl" $code | Should -Be 0 @@ -971,10 +1376,23 @@ Describe "Install-Extension" { } } It "Falls back to matching links if extension direct link is not found" { + Mock Get-Current-PHP-Version { return @{ version = "8.2.0"; path = "TestDrive:\php\8.2.0"; arch = "x64"; buildType = "ts" }} + Mock Get-Extension-From-URL { + return @{ + extName = "courierauth" + data = @( + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/courierauth/1.4.0/php_courierauth-1.4.0-8.2-ts-vs16-x64.zip"; arch = "x64"; buildType = "ts" ; version = "8.2"; extVersion = "1.4.0"; fileName = "php_courierauth-1.4.0-8.2-ts-vs16-x64.zip"; outerHTML = "8.2 Thread Safe (TS) x64" } + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/courierauth/1.4.0/php_courierauth-1.4.0-8.2-nts-vs16-x64.zip"; arch = "x64"; buildType = "nts" ; version = "8.2"; extVersion = "1.4.0"; fileName = "php_courierauth-1.4.0-8.2-nts-vs16-x64.zip"; outerHTML = "8.2 Non Thread Safe (NTS) x64" } + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/courierauth/1.4.0/php_courierauth-1.4.0-8.2-ts-vs16-x86.zip"; arch = "x86"; buildType = "ts" ; version = "8.2"; extVersion = "1.4.0"; fileName = "php_courierauth-1.4.0-8.2-ts-vs16-x86.zip"; outerHTML = "8.2 Thread Safe (TS) x86" } + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/courierauth/1.4.0/php_courierauth-1.4.0-8.2-nts-vs16-x86.zip"; arch = "x86"; buildType = "nts" ; version = "8.2"; extVersion = "1.4.0"; fileName = "php_courierauth-1.4.0-8.2-nts-vs16-x86.zip"; outerHTML = "8.2 Non Thread Safe (NTS) x86" } + ) + } + } Mock Test-Path { return $false } Mock Read-Host -ParameterFilter { $Prompt -eq "`nphp_curl.dll already exists. Would you like to overwrite it? (y/n)" } -MockWith { return "y" } + Mock Add-Missing-PHPExtension-To-Ini { return 0 } $code = Install-Extension -iniPath $testIniPath -extName "cour" $code | Should -Be 0 @@ -988,11 +1406,6 @@ Describe "Install-Extension" { $code = Install-Extension -iniPath $testIniPath -extName "cache" $code | Should -Be -1 } - It "Returns -1 when user does choose a non valid dll extension version to install" { - Mock Read-Host -ParameterFilter { $Prompt -eq "`nInsert the [number] you want to install" } -MockWith { '-10' } - $code = Install-Extension -iniPath $testIniPath -extName "cache" - $code | Should -Be -1 - } } It "Handles thrown exception" { @@ -1026,24 +1439,66 @@ Describe "Install-IniExtension" { $code = Install-IniExtension -iniPath $testIniPath -extName "curl" $code | Should -Be -1 } + + It "Handles thrown exception" { + Mock Log-Data { return 0 } + Mock Install-Extension { throw "Network error" } + $code = Install-IniExtension -iniPath $testIniPath -extName "curl" + $code | Should -Be -1 + } } -Describe "Get-PHPExtensions-From-Source" { - BeforeAll { - Mock Cache-Data { return 0 } - $global:MockFileSystem = @{ - Directories = @() - Files = @{} - WebResponses = @{} - DownloadFails = $false +Describe "Get-Extension-Categories-By-Page Tests" { + It "Returns extensions links by page" { + Mock Invoke-WebRequest -ParameterFilter { $Uri -eq "$($PECL_PACKAGES_URL)?catpid=3&catname=Caching&pageID=1" } -MockWith { + return @{ + Content = "Mocked PHP extension Caching content" + Links = @( + @{ href = "/package/APC" } + @{ href = "/package/APCu" } + @{ href = "/package/memcache" } + @{ href = "/package/memcached" } + ) + } } + + $result = Get-Extension-Categories-By-Page -extCategory "Caching" -link "/packages.php?catpid=3&catname=Caching" -page 1 + + $result.availableExtensions.Count | Should -Be 4 + $result.availableExtensions[0].href | Should -Be "/package/APC" + $result.availableExtensions[1].href | Should -Be "/package/APCu" + $result.availableExtensions[2].href | Should -Be "/package/memcache" + $result.availableExtensions[3].href | Should -Be "/package/memcached" + $result.hasMore | Should -Be $false } - BeforeEach { - $global:getRandomFile = $false - $global:MockFileSystem.DownloadFails = $false - $global:MockFileSystem.WebResponses = @{ - $PECL_PACKAGES_URL = @{ + It "Sets hasMore to true when more pages are available" { + Mock Invoke-WebRequest -ParameterFilter { $Uri -eq "$($PECL_PACKAGES_URL)?catpid=3&catname=Caching&pageID=1" } -MockWith { + return @{ + Content = "Mocked PHP extension Caching content" + Links = @( + @{ href = $null } + @{ href = "random_link.php" } + @{ href = "/packages.php?catpid=3&catname=Caching&pageID=2" } + @{ href = "/package/APC" } + @{ href = "/package/APCu" } + @{ href = "/package/memcache" } + @{ href = "/package/memcached" } + ) + } + } + + $result = Get-Extension-Categories-By-Page -extCategory "Caching" -link "/packages.php?catpid=3&catname=Caching" -page 1 + + $result.hasMore | Should -Be $true + } +} + +Describe "Get-PHPExtensions-From-Source" { + BeforeAll { + Mock Cache-Data { return 0 } + Mock Invoke-WebRequest -ParameterFilter { $Uri -eq $PECL_PACKAGES_URL } -MockWith { + return @{ Content = "Mocked PHP extensions content" Links = @( @{ href = $null } @@ -1056,24 +1511,29 @@ Describe "Get-PHPExtensions-From-Source" { outerHTML = 'EmptyCat' } ) } - "$($PECL_PACKAGES_URL)?catpid=1&catname=Authentication" = @{ - Content = "Mocked PHP extension Auth content" - Links = @( - @{ href = $null } - @{ href = "/package/courierauth" } - @{ href = "/package/krb5" } - ) + } + Mock Get-Extension-Categories-By-Page { + param ($link) + if ($link -eq "/packages.php?catpid=1&catname=Authentication") { + return @{ + hasMore = $false + availableExtensions = @( + @{ href = "/package/courierauth" } + @{ href = "/package/krb5" } + ) + } } - "$($PECL_PACKAGES_URL)?catpid=3&catname=Caching" = @{ - Content = "Mocked PHP extension Caching content" - Links = @( - @{ href = "/package/APC" } - @{ href = "/package/APCu" } - ) + if ($link -eq "/packages.php?catpid=3&catname=Caching") { + return @{ + hasMore = $false + availableExtensions = @( + @{ href = "/package/memcache" } + @{ href = "/package/memcached" } + ) + } } - "$($PECL_PACKAGES_URL)?catpid=7&catname=EmptyCat" = @{ - Content = "Mocked PHP extension EmptyCat content" - Links = @() + if ($link -eq "/packages.php?catpid=7&catname=EmptyCat") { + return @{ hasMore = $false; availableExtensions = @() } } } } @@ -1084,7 +1544,7 @@ Describe "Get-PHPExtensions-From-Source" { } It "Handles thrown exception" { - $global:MockFileSystem.DownloadFails = $true + Mock Get-Extension-Categories-By-Page { throw "Network error" } $list = Get-PHPExtensions-From-Source $list.Count | Should -Be 0 } @@ -1213,8 +1673,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 } @@ -1222,13 +1681,13 @@ Describe "List-PHP-Extensions" { Describe "Install-XDebug-Extension" { BeforeAll { - Mock Get-Current-PHP-Version { return @{ version = "7.1.0"; path = "TestDrive:\php\7.1.0" }} + Mock Get-Current-PHP-Version { return @{ version = "8.1"; arch = "x64"; buildType = "ts"; path = "TestDrive:\php\8.1.0" }} Mock Get-XDebug-FROM-URL { return @( - @{ href = "/download/php_xdebug-3.1.0-8.1-vs16-x64.dll"; version = "3.1.0"; xDebugVersion = "3.1.0"; fileName = "php_xdebug-3.1.0-8.1-vs16-x64.dll"; outerHTML = "php_xdebug-3.1.0-8.1-vs16-x64.dll" } - @{ href = "/download/php_xdebug-2.9.0-8.1-vs16-x86_64.dll"; version = "2.9.0"; xDebugVersion = "2.9.0"; fileName = "php_xdebug-2.9.0-8.1-vs16-x86_64.dll"; outerHTML = "php_xdebug-2.9.0-8.1-vs16-x86_64.dll" } - @{ href = "/download/php_xdebug-3.1.0-8.1-nts-vs16-x86_64.dll"; version = "3.1.0"; xDebugVersion = "3.1.0"; fileName = "php_xdebug-3.1.0-8.1-nts-vs16-x86_64.dll"; outerHTML = "php_xdebug-3.1.0-8.1-nts-vs16-x86_64.dll" } - @{ href = "/download/php_xdebug-2.9.0-8.1-nts-vc16-x86_64.dll"; version = "2.9.0"; xDebugVersion = "2.9.0"; fileName = "php_xdebug-2.9.0-8.1-nts-vc16-x86_64.dll"; outerHTML = "php_xdebug-2.9.0-8.1-nts-vc16-x86_64.dll" } + @{ href = "/download/php_xdebug-3.1.0-8.1-vs16-x64.dll"; arch = "x64"; buildType = "ts"; version = "8.1"; xDebugVersion = "3.1.0"; fileName = "php_xdebug-3.1.0-8.1-vs16-x64.dll"; outerHTML = "php_xdebug-3.1.0-8.1-vs16-x64.dll" } + @{ href = "/download/php_xdebug-2.9.0-8.1-vs16-x64.dll"; arch = "x64"; buildType = "ts"; version = "8.1"; xDebugVersion = "2.9.0"; fileName = "php_xdebug-2.9.0-8.1-vs16-x86_64.dll"; outerHTML = "php_xdebug-2.9.0-8.1-vs16-x86_64.dll" } + @{ href = "/download/php_xdebug-3.1.0-8.1-nts-vs16-x64.dll"; arch = "x64"; buildType = "nts"; version = "8.1"; xDebugVersion = "3.1.0"; fileName = "php_xdebug-3.1.0-8.1-nts-vs16-x86_64.dll"; outerHTML = "php_xdebug-3.1.0-8.1-nts-vs16-x86_64.dll" } + @{ href = "/download/php_xdebug-2.9.0-8.1-nts-vc16-x64.dll"; arch = "x64"; buildType = "nts"; version = "8.1"; xDebugVersion = "2.9.0"; fileName = "php_xdebug-2.9.0-8.1-nts-vc16-x86_64.dll"; outerHTML = "php_xdebug-2.9.0-8.1-nts-vc16-x86_64.dll" } ) } Mock Read-Host { @@ -1313,6 +1772,13 @@ opcache.enable = 1 $code = Install-XDebug-Extension -iniPath $testIniPath $code | Should -Be -1 } + + It "Returns -1 when no compatible extension version is found" { + Mock Can-Use-Cache { return $false } + Mock Get-XDebug-FROM-URL { return @() } + $code = Install-XDebug-Extension -iniPath $testIniPath + $code | Should -Be -1 + } } Describe "Invoke-PVMIniAction" { @@ -1451,40 +1917,40 @@ extension=php_curl.dll Directories = @() Files = @{} WebResponses = @{ - "$PECL_PACKAGE_ROOT_URL/nonexistent_ext" = @{ - Content = "Mocked PHP nonexistent_ext content" - Links = @() - } - "$PECL_PACKAGE_ROOT_URL/pdo_mysql" = @{ - Content = "Mocked pdo_mysql content" - Links = @( - @{ href = "/package/pdo_mysql/1.4.0/windows" }, - @{ href = "/package/pdo_mysql/2.1.0/windows" } - ) - } - "$PECL_PACKAGE_ROOT_URL/curl" = @{ - Content = "Mocked curl content" - Links = @( - @{ href = "/package/curl/1.4.0/windows" }, - @{ href = "/package/curl/2.1.0/windows" } - ) - } - "$PECL_PACKAGE_ROOT_URL/curl/1.4.0/windows" = @{ - Content = "Mocked PHP curl 1.4.0 content" - Links = @( - @{ href = "other_link" }, - @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-ts-vs16-x86.zip" }, - @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-ts-vs16-x64.zip" } - ) - } - "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-ts-vs16-x86.zip" = @{ - Content = "Mocked PHP curl 1.4.0 zip content" - } - "$PECL_PACKAGE_ROOT_URL/curl/2.1.0/windows" = @{ - Content = "Mocked PHP curl 2.1.0 content" - Links = @() - } - } + "$PECL_PACKAGE_ROOT_URL/nonexistent_ext" = @{ + Content = "Mocked PHP nonexistent_ext content" + Links = @() + } + "$PECL_PACKAGE_ROOT_URL/pdo_mysql" = @{ + Content = "Mocked pdo_mysql content" + Links = @( + @{ href = "/package/pdo_mysql/1.4.0/windows" }, + @{ href = "/package/pdo_mysql/2.1.0/windows" } + ) + } + "$PECL_PACKAGE_ROOT_URL/curl" = @{ + Content = "Mocked curl content" + Links = @( + @{ href = "/package/curl/1.4.0/windows" }, + @{ href = "/package/curl/2.1.0/windows" } + ) + } + "$PECL_PACKAGE_ROOT_URL/curl/1.4.0/windows" = @{ + Content = "Mocked PHP curl 1.4.0 content" + Links = @( + @{ href = "other_link" }, + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-ts-vs16-x86.zip" }, + @{ href = "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-ts-vs16-x64.zip" } + ) + } + "$PECL_WIN_EXT_DOWNLOAD_URL/curl/1.4.0/php_curl-1.4.0-8.2-ts-vs16-x86.zip" = @{ + Content = "Mocked PHP curl 1.4.0 zip content" + } + "$PECL_PACKAGE_ROOT_URL/curl/2.1.0/windows" = @{ + Content = "Mocked PHP curl 2.1.0 content" + Links = @() + } + } DownloadFails = $false } diff --git a/tests/install.tests.ps1 b/tests/install.tests.ps1 index 729b1b2..6efbb72 100644 --- a/tests/install.tests.ps1 +++ b/tests/install.tests.ps1 @@ -1,6 +1,3 @@ -# Comprehensive Test Suite for PHP Installation Functions -# Load required modules and functions -. "$PSScriptRoot\..\src\actions\install.ps1" BeforeAll { Mock Write-Host {} @@ -266,13 +263,6 @@ BeforeAll { return -1 } } - - function Is-PHP-Version-Installed { - param($version) - - $envVars = Get-All-EnvVars - return $envVars.ContainsKey("php$version") - } } # Test Suites @@ -303,9 +293,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 +318,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" } } @@ -347,19 +339,27 @@ Describe "Get-PHP-Versions Tests" { Set-MockWebResponse -url $PHP_WIN_ARCHIVES_URL -links $mockLinks Set-MockWebResponse -url $PHP_WIN_RELEASES_URL -links $mockLinks - $result = Get-PHP-Versions -version "8.1" + $result = Get-PHP-Versions -version "8.1" -arch "x64" + + $result.Count | Should -BeGreaterThan 0 + } + + It "Should return versions for NTS Build type" { + $mockLinks = @( + @{ href = "/downloads/releases/php-8.1.0-Win32-vs16-nts-x64.zip" }, + @{ href = "/downloads/releases/php-8.1.0-Win32-vs16-x64.zip" } + ) + + Set-MockWebResponse -url $PHP_WIN_ARCHIVES_URL -links $mockLinks + Set-MockWebResponse -url $PHP_WIN_RELEASES_URL -links $mockLinks + + $result = Get-PHP-Versions -version "8.1" -buildType "nts" $result.Count | Should -BeGreaterThan 0 } 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" @@ -543,13 +543,13 @@ Describe "Select-Version Tests" { Mock Write-Host { } $versions = @{ "Archives" = @( - @{ version = "8.1.0"; fileName = "php-8.1.0.zip" }, - @{ version = "8.1.1"; fileName = "php-8.1.1.zip" } + @{ version = "8.1.0"; arch = "x64"; buildType = "TS"; fileName = "php-8.1.0.zip" }, + @{ version = "8.1.1"; arch = "x64"; buildType = "TS"; fileName = "php-8.1.1.zip" } ) } - $global:MockUserInput = "8.1.1" + $global:MockUserInput = "1" - $result = Select-Version -matchingVersions $versions + $result = Select-Version -matchingVersions $versions -version "8.1" -arch "x64" -buildType "TS" $result.version | Should -Be "8.1.1" } @@ -557,7 +557,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" @@ -589,7 +589,12 @@ Describe "Install-PHP Integration Tests" { It "Returns -1 when user declines family version install" { Mock Get-Current-PHP-Version { return @{ version = "7.4.9" } } - Mock Get-Matching-PHP-Versions { return @("7.4.9", "8.0.9", "8.1.9", "8.1.12") } + Mock Get-Matching-PHP-Versions { return @( + @{ version = "7.4.9"; arch = "x64"; buildType = "TS"; fileName = "php-7.4.9-Win32-vs16-x64.zip" }, + @{ version = "8.0.9"; arch = "x64"; buildType = "TS"; fileName = "php-8.0.9-Win32-vs16-x64.zip" }, + @{ version = "8.1.9"; arch = "x64"; buildType = "TS"; fileName = "php-8.1.9-Win32-vs16-x64.zip" }, + @{ version = "8.1.12"; arch = "x64"; buildType = "TS"; fileName = "php-8.1.12-Win32-vs16-x64.zip" } + ) } $global:MockUserInput = "n" $result = Install-PHP -version "8" @@ -622,11 +627,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 +762,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 "5.6" + $result[1].version | Should -Be "7.4" } It "Get-Installed-PHP-Versions should handle registry errors" { @@ -783,21 +790,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 } } diff --git a/tests/list.tests.ps1 b/tests/list.tests.ps1 index 2414a29..f2f33af 100644 --- a/tests/list.tests.ps1 +++ b/tests/list.tests.ps1 @@ -1,6 +1,3 @@ -# PHP Version Management Tests -# Load required modules and functions -. "$PSScriptRoot\..\src\actions\list.ps1" BeforeAll { # Mock global variables that would be defined in the main script @@ -9,17 +6,8 @@ BeforeAll { Mock Write-Host { } # Mock external functions that aren't defined in the provided code - Mock Make-Directory { param($path) - if (-not (Test-Path -Path $path)) { - $parent = Split-Path -Parent $path - if ($parent -and -not (Test-Path -Path $parent)) { - Make-Directory -path $parent - } - New-Item -Path $path -ItemType Directory -Force | Out-Null - } - return 0 - } - Mock Log-Data { param($logPath, $message, $data) return "Logged: $message - $data" } + Mock Make-Directory { return 0 } + Mock Log-Data { param($logPath, $message, $data) return 0 } Mock Get-Source-Urls { return @{ 'releases' = $PHP_WIN_RELEASES_URL @@ -77,27 +65,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 - - $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' - } - It "Should return empty list" { Mock Invoke-WebRequest { return @{ Links = @() } @@ -112,7 +79,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 @@ -123,6 +90,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") @@ -137,15 +112,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-Data-From-Cache -Exactly 1 } 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 +130,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 +148,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 } @@ -192,6 +165,74 @@ 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" { + 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 handle TS build type" { + 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-nts-x64.zip" + BuildType = "NTS"; Arch = "x64"; Version = "7.1.0" + }) + }} + + $code = Get-Available-PHP-Versions -buildType "ts" + + $code | Should -Be 0 + } + + It "Should handle NTS build type" { + 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-nts-x64.zip" + BuildType = "NTS"; Arch = "x64"; Version = "7.1.0" + }) + }} + + $code = Get-Available-PHP-Versions -buildType "nts" + + $code | Should -Be 0 + } + It "Should read from cache by default" { Mock Get-Data-From-Cache { return @{ @@ -210,6 +251,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 +281,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 @{ @@ -263,8 +317,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 } @@ -296,7 +360,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 @@ -310,8 +374,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 +394,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 +413,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 +432,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 @@ -358,7 +445,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/log.tests.ps1 b/tests/log.tests.ps1 index 881e0c8..52787fd 100644 --- a/tests/log.tests.ps1 +++ b/tests/log.tests.ps1 @@ -1,121 +1,171 @@ - -. "$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" - } - } -} - + +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 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 } + + $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" + } + } + + It "Handles exceptions gracefully" { + Mock Test-Path { return $true } + Mock Get-Content { throw "File read error" } + + $result = Show-Log -pageSize 1 + + $result | Should -Be -1 + } +} + 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..63b1f5f 100644 --- a/tests/profile.tests.ps1 +++ b/tests/profile.tests.ps1 @@ -1,6 +1,4 @@ -. "$PSScriptRoot\..\src\actions\profile.ps1" - BeforeAll { # Mock global variables $global:PROFILES_PATH = "TestDrive:\\profiles" @@ -115,7 +113,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 +215,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 +358,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 +819,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 +1110,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 +1319,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/router.tests.ps1 b/tests/router.tests.ps1 index ade8888..eaaa727 100644 --- a/tests/router.tests.ps1 +++ b/tests/router.tests.ps1 @@ -1,8 +1,3 @@ -# Comprehensive Tests for PVM Actions - -# Load required modules and functions -. "$PSScriptRoot\..\src\core\router.ps1" - BeforeAll { Mock Write-Host {} @@ -75,8 +70,14 @@ 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 Write-Host { } + 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 { } } It "Should display current PHP version and extensions when version is set" { @@ -139,7 +140,7 @@ Describe "Invoke-PVMList Tests" { Describe "Invoke-PVMInstall Tests" { BeforeEach { Mock Install-PHP { 0 } - # Mock Write-Host { } + Mock Write-Host { } } It "Should return -1 when no version is provided" { @@ -165,6 +166,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 @@ -173,6 +175,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" { @@ -180,7 +191,7 @@ Describe "Invoke-PVMUninstall Tests" { Mock Get-Current-PHP-Version { @{ version = "8.1.0" } } Mock Uninstall-PHP { @{ code = 0; message = "Uninstalled successfully" } } Mock Display-Msg-By-ExitCode { } - # Mock Write-Host { } + Mock Write-Host { } Mock Read-Host { } } @@ -221,7 +232,7 @@ Describe "Invoke-PVMUse Tests" { Mock Auto-Select-PHP-Version { @{ code = 0; version = "8.2.0" } } Mock Update-PHP-Version { @{ code = 0; message = "Version updated" } } Mock Display-Msg-By-ExitCode { } - # Mock Write-Host { } + Mock Write-Host { } } It "Should return -1 when no version is provided" { @@ -271,7 +282,7 @@ Describe "Invoke-PVMUse Tests" { Describe "Invoke-PVMIni Tests" { BeforeEach { Mock Invoke-PVMIniAction { 0 } - # Mock Write-Host { } + Mock Write-Host { } } It "Should return -1 when no action is provided" { @@ -382,7 +393,10 @@ Describe "Invoke-PVMTest Tests" { } It "Should call Run-Tests with provided arguments" { - $result = Invoke-PVMTest -arguments @("TestFile.ps1", "TestFile2.ps1", "--coverage=80", "--verbosity=detailed", "--tag=unit") + $result = Invoke-PVMTest -arguments @( + "TestFile.ps1", "TestFile2.ps1", + "--coverage=80", "--verbosity=detailed", "--tag=unit", "--sort=coverage" + ) $result | Should -Be 0 } @@ -399,6 +413,306 @@ 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" + } + } + + 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" { + 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" + } + } + + 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" { + 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" + } + } + + 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" { + 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 { @@ -412,6 +726,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" { @@ -518,6 +833,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 + } } } @@ -532,7 +854,7 @@ Describe "Integration Tests" { Mock Get-Current-PHP-Version { @{ version = "8.2.0"; status = @{ "xdebug" = $true }; path = "C:\PHP\8.2.0" } } Mock Install-PHP { 0 } Mock Update-PHP-Version { @{ code = 0; message = "Version updated" } } - # Mock Write-Host { } + Mock Write-Host { } } It "Should handle complete workflow: setup -> install -> use -> current" { @@ -566,7 +888,7 @@ Describe "Integration Tests" { Mock Setup-PVM { @{ code = 1; message = "Setup failed" } } Mock Optimize-SystemPath { 1 } Mock Display-Msg-By-ExitCode { } - # Mock Write-Host { } + Mock Write-Host { } $result = Invoke-PVMSetup $result | Should -Be 0 diff --git a/tests/setup.tests.ps1 b/tests/setup.tests.ps1 index d002810..167360e 100644 --- a/tests/setup.tests.ps1 +++ b/tests/setup.tests.ps1 @@ -1,225 +1,223 @@ -# 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 - } + +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 0 } + + # 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 { return 0 } + Mock Log-Data -MockWith { return 0 } + 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 diff --git a/tests/uninstall.tests.ps1 b/tests/uninstall.tests.ps1 index 4b1c572..6714578 100644 --- a/tests/uninstall.tests.ps1 +++ b/tests/uninstall.tests.ps1 @@ -1,207 +1,208 @@ -# 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 - } + +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 0 + } +} + +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 { 0 } + } + + 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 + $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-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 + + 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-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 + $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-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 + + 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 { 0 } + } + + 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 { 0 } + } + + 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 { 0 } + } + + 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 { 0 } + } + + It "Should return version not found message" { + 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!" + $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" { + 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 + $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..20442bd 100644 --- a/tests/use.tests.ps1 +++ b/tests/use.tests.ps1 @@ -1,207 +1,193 @@ -# 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" - } -} + +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-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" + } + + 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-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" + } + + 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 + } +} + +# 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" + } +}