diff --git a/.github/workflows/pester-tests.yml b/.github/workflows/pester-tests.yml new file mode 100644 index 000000000000..57fbe83d692a --- /dev/null +++ b/.github/workflows/pester-tests.yml @@ -0,0 +1,42 @@ +name: Powershell Pester Tests + +on: + push: + branches: [ main, SOLR-17508-* ] + paths: + - 'solr/packaging/powershell-tests/**' + - 'solr/packaging/build.gradle' + - 'solr/bin/solr.cmd' + - 'solr/bin/solr.in.cmd' + pull_request: + branches: [ main ] + +jobs: + pester-tests: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + + - name: Build Solr distribution + run: | + .\gradlew.bat --no-daemon -p solr/packaging installFullDist + shell: powershell + + - name: Run Pester tests + run: | + .\gradlew.bat --no-daemon -p solr/packaging pesterTests + shell: powershell + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: pester-test-results + path: solr/packaging/build/test-output/ diff --git a/solr/packaging/build.gradle b/solr/packaging/build.gradle index 8d0e8723b617..66296326e15f 100644 --- a/solr/packaging/build.gradle +++ b/solr/packaging/build.gradle @@ -16,6 +16,7 @@ */ import org.apache.tools.ant.util.TeeOutputStream +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform // This project puts together a "distribution", assembling dependencies from // various other projects. @@ -273,6 +274,67 @@ task integrationTests(type: BatsTask) { environment BATS_LIB_PREFIX: "$nodeProjectDir/node_modules" } +task pesterTests { + dependsOn installFullDist + onlyIf { DefaultNativePlatform.getCurrentOperatingSystem().isWindows() } + + def integrationTestOutput = "$buildDir/powershell-test-output" + def solrHome = "$integrationTestOutput/solr-home" + def solrTestFailuresDir = "$integrationTestOutput/failure-snapshots" + var solrPort = Integer.parseInt((String) project.findProperty('pester.port') ?: System.getProperty("pester.port", "-1")) + while (solrPort > 64000 || solrPort < 0) { // We need room for +1000 for ZK + try (ServerSocket s = new ServerSocket(0)) { + solrPort = s.getLocalPort() + } catch (Exception e) { + println("WARN: Could not assign random port for Pester tests. Using default port 8983.") + solrPort = 8983 + } + } + + inputs.dir(distDir) + outputs.dir(integrationTestOutput) + + doFirst { + delete integrationTestOutput + mkdir integrationTestOutput + mkdir solrHome + mkdir solrTestFailuresDir + + println("Running Pester tests with Solr base port ${solrPort}") + } + + doLast { + exec { + executable "powershell" + args = [ + "-Command", + "& { " + + "Import-Module Pester -MinimumVersion 5.0; " + + "\$config = New-PesterConfiguration; " + + "\$config.Run.Path = 'powershell-tests'; " + + "\$config.Run.Exit = \$true; " + + "\$config.Output.Verbosity = 'Normal'; " + + "\$config.TestResult.OutputFormat = 'NUnitXml'; " + + "\$config.TestResult.OutputPath = '${integrationTestOutput}/test-results.xml'; " + + "\$env:SOLR_TIP = '${distDir}'; " + + "\$env:SOLR_HOME = '${solrHome}'; " + + "\$env:SOLR_PID_DIR = '${solrHome}'; " + + "\$env:SOLR_PORT = ${solrPort}; " + + "\$env:SOLR2_PORT = ${solrPort + 1}; " + + "\$env:SOLR3_PORT = ${solrPort + 2}; " + + "\$env:ZK_PORT = ${solrPort + 1000}; " + + "\$env:SOLR_LOGS_DIR = '${solrHome}/logs'; " + + "\$env:TEST_OUTPUT_DIR = '${integrationTestOutput}'; " + + "\$env:TEST_FAILURE_DIR = '${solrTestFailuresDir}'; " + + "Invoke-Pester -Configuration \$config; " + + "}" + ] + + workingDir projectDir.toString() + } + } +} + class BatsTask extends Exec { @InputDirectory String testDir = 'test' diff --git a/solr/packaging/powershell-tests/Help.Tests.ps1 b/solr/packaging/powershell-tests/Help.Tests.ps1 new file mode 100644 index 000000000000..e71d9c59ce0b --- /dev/null +++ b/solr/packaging/powershell-tests/Help.Tests.ps1 @@ -0,0 +1,153 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +<# + Pester tests for Solr help command + Ported from test/test_help.bats +#> + +BeforeAll { + # Get the Solr installation directory from environment variable + $script:SolrTip = $env:SOLR_TIP + + if (-not $SolrTip) { + throw "SOLR_TIP environment variable is not set" + } + + # Determine the Solr executable based on OS + $script:SolrCmd = Join-Path $SolrTip "bin\solr.cmd" + + if (-not (Test-Path $SolrCmd)) { + throw "Solr executable not found at: $SolrCmd" + } + + Write-Host "Using Solr installation at: $SolrTip" + Write-Host "Using Solr command: $SolrCmd" + + function Test-HelpOutput { + param( + [string[]]$Arguments, + [string]$ExpectedPattern, + [string]$TestName + ) + + Write-Host "Testing help: $TestName" + Write-Host "Running: $SolrCmd $($Arguments -join ' ')" + + $output = & $SolrCmd @Arguments 2>&1 + $outputStr = $output | Out-String + + Write-Host "Exit Code: $LASTEXITCODE" + if ($outputStr.Length -gt 0) { + Write-Host "Output (first 500 chars): $($outputStr.Substring(0, [Math]::Min(500, $outputStr.Length)))" + } else { + Write-Host "WARNING: Output is empty!" + } + + return $outputStr + } +} + +Describe "Solr Help Command" { + Context "Main help commands" { + It "solr --help flag prints help" { + $output = Test-HelpOutput @("--help") "Usage: solr COMMAND OPTIONS" "solr --help" + $output | Should -Match "Usage: solr COMMAND OPTIONS" + $output | Should -Not -cmatch "ERROR" + } + + It "solr with no flags prints help" { + $output = Test-HelpOutput @() "Usage: solr COMMAND OPTIONS" "solr (no flags)" + $output | Should -Match "Usage: solr COMMAND OPTIONS" + $output | Should -Not -cmatch "ERROR" + } + } + + Context "Command-specific help" { + It "start --help flag prints help" { + $output = Test-HelpOutput @("start", "--help") "Usage: solr start" "start --help" + $output | Should -Match "Usage: solr start" + $output | Should -Not -cmatch "ERROR" + } + + It "start -h flag prints help" { + $output = Test-HelpOutput @("start", "-h") "Usage: solr start" "start -h" + $output | Should -Match "Usage: solr start" + $output | Should -Not -Match "ERROR: Hostname is required when using the -h option!" + } + + It "stop --help flag prints help" { + $output = Test-HelpOutput @("stop", "--help") "Usage: solr stop" "stop --help" + $output | Should -Match "Usage: solr stop" + $output | Should -Not -cmatch "ERROR" + } + + It "restart --help flag prints help" { + $output = Test-HelpOutput @("restart", "--help") "Usage: solr restart" "restart --help" + $output | Should -Match "Usage: solr restart" + $output | Should -Not -cmatch "ERROR" + } + + It "status --help flag prints help" { + $output = Test-HelpOutput @("status", "--help") "usage: bin/solr status" "status --help" + $output | Should -Match "usage: bin/solr status" + $output | Should -Not -cmatch "ERROR" + } + + It "healthcheck --help flag prints help" { + $output = Test-HelpOutput @("healthcheck", "--help") "usage: bin/solr healthcheck" "healthcheck --help" + $output | Should -Match "usage: bin/solr healthcheck" + $output | Should -Not -cmatch "ERROR" + } + + It "create --help flag prints help" { + $output = Test-HelpOutput @("create", "--help") "usage: bin/solr create" "create --help" + $output | Should -Match "usage: bin/solr create" + $output | Should -Not -cmatch "ERROR" + } + + It "delete -h flag prints help" { + $output = Test-HelpOutput @("delete", "-h") "usage: bin/solr delete" "delete -h" + $output | Should -Match "usage: bin/solr delete" + $output | Should -Not -cmatch "ERROR" + } + + It "zk --help flag prints help" { + $output = Test-HelpOutput @("zk", "--help") "usage:" "zk --help" + $output | Should -Match "usage:" + $output | Should -Match "bin/solr zk ls" + $output | Should -Not -cmatch "ERROR" + } + + It "auth --help flag prints help" { + $output = Test-HelpOutput @("auth", "--help") "bin/solr auth enable" "auth --help" + $output | Should -Match "bin/solr auth enable" + $output | Should -Not -cmatch "ERROR" + } + + It "assert --help flag prints help" { + $output = Test-HelpOutput @("assert", "--help") "usage: bin/solr assert" "assert --help" + $output | Should -Match "usage: bin/solr assert" + $output | Should -Not -cmatch "ERROR" + } + + It "post --help flag prints help" { + $output = Test-HelpOutput @("post", "--help") "usage: bin/solr post" "post --help" + $output | Should -Match "usage: bin/solr post" + $output | Should -Not -cmatch "ERROR" + } + } +} diff --git a/solr/packaging/powershell-tests/Version.Tests.ps1 b/solr/packaging/powershell-tests/Version.Tests.ps1 new file mode 100644 index 000000000000..6c14dc2c8c45 --- /dev/null +++ b/solr/packaging/powershell-tests/Version.Tests.ps1 @@ -0,0 +1,54 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +<# + Pester tests for Solr version command + Ported from test/test_version.bats +#> + +BeforeAll { + # Get the Solr installation directory from environment variable + $script:SolrTip = $env:SOLR_TIP + if (-not $SolrTip) { + throw "SOLR_TIP environment variable is not set" + } + + # Determine the Solr executable based on OS + $script:SolrCmd = Join-Path $SolrTip "bin\solr.cmd" + + if (-not (Test-Path $SolrCmd)) { + throw "Solr executable not found at: $SolrCmd" + } + + Write-Host "Using Solr installation at: $SolrTip" + Write-Host "Using Solr command: $SolrCmd" +} + +Describe "Solr Version Command" { + Context "When using --version flag" { + It "--version returns Solr version" { + $output = & $SolrCmd --version 2>&1 + $output | Out-String | Should -Match "Solr version is:" + } + } + + Context "When using version as direct command" { + It "version as direct tool call still runs" { + $output = & $SolrCmd version 2>&1 + $output | Out-String | Should -Match "Solr version is:" + } + } +} diff --git a/solr/packaging/powershell-tests/Zk.Tests.ps1 b/solr/packaging/powershell-tests/Zk.Tests.ps1 new file mode 100644 index 000000000000..877fa51791a6 --- /dev/null +++ b/solr/packaging/powershell-tests/Zk.Tests.ps1 @@ -0,0 +1,304 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +<# + Pester tests for Solr zk command + Ported from test/test_zk.bats +#> + +BeforeAll { + # Get the Solr installation directory from environment variable + $script:SolrTip = $env:SOLR_TIP + + # Note: $SolrTip cannot be used directly in -Skip statements in tests, use $env:SOLR_TIP instead + if (-not $SolrTip) { + throw "SOLR_TIP environment variable is not set" + } + + # Determine the Solr executable based on OS + $script:SolrCmd = Join-Path $SolrTip "bin\solr.cmd" + + if (-not (Test-Path $SolrCmd)) { + throw "Solr executable not found at: $SolrCmd" + } + + Write-Host "Using Solr installation at: $SolrTip" + Write-Host "Using Solr command: $SolrCmd" + + # Get ZooKeeper port from environment or default + $script:ZK_PORT = if ($env:ZK_PORT) { $env:ZK_PORT } else { "9983" } + $script:SOLR_PORT = if ($env:SOLR_PORT) { $env:SOLR_PORT } else { "8983" } + + # Check if Solr is already running + $solrAlreadyRunning = $false + try { + $response = Invoke-WebRequest -Uri "http://localhost:$SOLR_PORT/solr/" -UseBasicParsing -TimeoutSec 2 -ErrorAction SilentlyContinue + if ($response.StatusCode -eq 200) { + $solrAlreadyRunning = $true + Write-Host "Solr is already running on port $SOLR_PORT" + } + } catch { + $solrAlreadyRunning = $false + } + + $script:SolrStartedByTests = $false + + # Start Solr if it's not already running + if (-not $solrAlreadyRunning) { + Write-Host "Starting Solr in cloud mode..." + + # Start Solr with embedded ZooKeeper using Start-Process to run asynchronously + $startArgs = @("start", "-p", "$SOLR_PORT") + Write-Host "Running: $SolrCmd $($startArgs -join ' ')" + + # Use Start-Job with cmd.exe to properly parse arguments and maintain process hierarchy + $solrJob = Start-Job -ScriptBlock { + param($cmd, $argString) + cmd /c "`"$cmd`" $argString" 2>&1 + } -ArgumentList $SolrCmd, $startArgs + + # Wait a bit for Solr to start + Start-Sleep -Seconds 5 + + Write-Host "Solr starting on port $SOLR_PORT (Job ID: $($solrJob.Id))" + + # Check if there were any immediate errors + $jobOutput = Receive-Job -Job $solrJob -Keep + if ($jobOutput) { + Write-Host "Solr output: $jobOutput" + } + + $script:SolrStartedByTests = $true + + # Wait for Solr to be ready (max 60 seconds) + Write-Host "Waiting for Solr to be ready..." + $maxWaitTime = 60 + $waitInterval = 2 + $elapsed = 0 + $solrReady = $false + + while ($elapsed -lt $maxWaitTime) { + Start-Sleep -Seconds $waitInterval + $elapsed += $waitInterval + + try { + $response = Invoke-WebRequest -Uri "http://localhost:$SOLR_PORT/solr/" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop + + if ($response.StatusCode -eq 200) { + $solrReady = $true + Write-Host "Solr is ready! (took $elapsed seconds)" + break + } + } catch { + Write-Host "Still waiting... ($elapsed seconds elapsed) - Error: $($_.Exception.Message)" + } + } + + if (-not $solrReady) { + throw "Solr did not become ready within $maxWaitTime seconds" + } + + # Give it a bit more time to fully initialize ZooKeeper + Start-Sleep -Seconds 3 + } + + $script:SolrRunning = $true + + function Test-CommandOutput { + param( + [string[]]$Arguments, + [string]$TestName + ) + + Write-Host "Testing: $TestName" + Write-Host "Running: $SolrCmd $($Arguments -join ' ')" + + # Note that CLI warnings are interpreted as NativeCommandError, since logged to std.err, and therefore may match + # strings like "Error" + $output = & $SolrCmd @Arguments 2>&1 + $outputStr = $output | Out-String + + Write-Host "Exit Code: $LASTEXITCODE" + if ($outputStr.Length -gt 0) { + Write-Host "Output (first 500 chars): $($outputStr.Substring(0, [Math]::Min(500, $outputStr.Length)))" + } else { + Write-Host "WARNING: Output is empty!" + } + + return $outputStr + } +} + +AfterAll { + # Stop Solr only if we started it + if ($script:SolrStartedByTests) { + Write-Host "Stopping Solr..." + $stopArgs = @("stop", "-p", $script:SOLR_PORT) + Write-Host "Running: $script:SolrCmd $($stopArgs -join ' ')" + + $stopOutput = & $script:SolrCmd @stopArgs 2>&1 + $stopOutputStr = $stopOutput | Out-String + Write-Host $stopOutputStr + + if ($LASTEXITCODE -ne 0) { + Write-Warning "Failed to stop Solr cleanly. Exit code: $LASTEXITCODE" + } else { + Write-Host "Solr stopped successfully" + } + } +} + +Describe "Solr Zk Command" { + Context "Help commands" { + It "short help" { + $output = Test-CommandOutput @("zk", "ls", "-h") "zk ls -h" + $output | Should -Match "usage: bin/solr zk" + } + + It "short help is inferred" { + $output = Test-CommandOutput @("zk", "ls") "zk ls" + $output | Should -Match "usage: bin/solr zk" + } + + It "long help" { + $output = Test-CommandOutput @("zk", "-h") "zk -h" + $output | Should -Match "bin/solr zk ls" + $output | Should -Match "bin/solr zk updateacls" + $output | Should -Match "Pass --help or -h after any COMMAND" + } + + It "running subcommands with zk is prevented" { + $output = Test-CommandOutput @("ls", "/", "-z", "localhost:$ZK_PORT") "ls / -z localhost:$ZK_PORT" + $output | Should -Match "You must invoke this subcommand using the zk command" + } + } + + Context "Zk operations (requires running Solr with ZooKeeper)" -Skip:(-not (Test-Path (Join-Path $env:SOLR_TIP "server\solr"))) { + BeforeAll { + # Check if Solr is running by trying to connect to the port + $solrRunning = $false + try { + $response = Invoke-WebRequest -Uri "http://localhost:$SOLR_PORT/solr/" -UseBasicParsing -ErrorAction SilentlyContinue + if ($response.StatusCode -eq 200) { + $solrRunning = $true + } + } catch { + $solrRunning = $false + } + + $script:SolrRunning = $solrRunning + if (-not $solrRunning) { + Write-Host "WARNING: Solr does not appear to be running on port $SOLR_PORT" + } + } + + It "listing out files" { + if (-not $script:SolrRunning) { Set-ItResult -Skipped -Because "Solr is not running"; return } + Start-Sleep -Seconds 1 + $output = Test-CommandOutput @("zk", "ls", "/", "-z", "localhost:$ZK_PORT", "--recursive") "zk ls / -z localhost:$ZK_PORT --recursive" + $output | Should -Match "aliases\.json" + } + + It "connecting via solr-url" { + if (-not $script:SolrRunning) { Set-ItResult -Skipped -Because "Solr is not running"; return } + Start-Sleep -Seconds 1 + $output = Test-CommandOutput @("zk", "ls", "/", "--solr-url", "http://localhost:$SOLR_PORT") "zk ls / --solr-url http://localhost:$SOLR_PORT" + $output | Should -Match "aliases\.json" + } + + It "connecting via -s flag" { + if (-not $script:SolrRunning) { Set-ItResult -Skipped -Because "Solr is not running"; return } + Start-Sleep -Seconds 1 + $output = Test-CommandOutput @("zk", "ls", "/", "-s", "http://localhost:$SOLR_PORT") "zk ls / -s http://localhost:$SOLR_PORT" + $output | Should -Match "aliases\.json" + } + + It "connecting via default (localhost)" { + if (-not $script:SolrRunning) { Set-ItResult -Skipped -Because "Solr is not running"; return } + Start-Sleep -Seconds 1 + $output = Test-CommandOutput @("zk", "ls", "/") "zk ls /" + $output | Should -Match "aliases\.json" + } + + It "connecting via -z flag" { + if (-not $script:SolrRunning) { Set-ItResult -Skipped -Because "Solr is not running"; return } + Start-Sleep -Seconds 1 + $output = Test-CommandOutput @("zk", "ls", "/", "-z", "localhost:$ZK_PORT") "zk ls / -z localhost:$ZK_PORT" + $output | Should -Match "aliases\.json" + } + + It "connecting via --zk-host flag" { + if (-not $script:SolrRunning) { Set-ItResult -Skipped -Because "Solr is not running"; return } + Start-Sleep -Seconds 1 + $output = Test-CommandOutput @("zk", "ls", "/", "--zk-host", "localhost:$ZK_PORT") "zk ls / --zk-host localhost:$ZK_PORT" + $output | Should -Match "aliases\.json" + } + + It "copying files around" { + if (-not $script:SolrRunning) { Set-ItResult -Skipped -Because "Solr is not running"; return } + $testFile = "testfile_$([System.IO.Path]::GetRandomFileName()).txt" + try { + # Create test file + "test content" | Out-File -FilePath $testFile -Encoding UTF8 + + # Copy to ZK + $output = Test-CommandOutput @("zk", "cp", $testFile, "zk:/$testFile", "-z", "localhost:$ZK_PORT") "zk cp $testFile to ZK" + $output | Should -Match "Copying from" + + Start-Sleep -Seconds 1 + + # Verify file is in ZK + $listOutput = Test-CommandOutput @("zk", "ls", "/", "-z", "localhost:$ZK_PORT") "zk ls / to verify copy" + $listOutput | Should -Match $testFile + } finally { + # Cleanup + if (Test-Path $testFile) { + Remove-Item $testFile -Force + } + } + } + + It "upconfig" { + if (-not $script:SolrRunning) { Set-ItResult -Skipped -Because "Solr is not running"; return } + $sourceConfigsetDir = Join-Path $SolrTip "server\solr\configsets\sample_techproducts_configs" + if (Test-Path $sourceConfigsetDir) { + $output = Test-CommandOutput @("zk", "upconfig", "-d", $sourceConfigsetDir, "-n", "techproducts_ps_test", "-z", "localhost:$ZK_PORT") "zk upconfig" + $output | Should -Match "Uploading" + $LASTEXITCODE | Should -Match 0 + } else { + Set-ItResult -Skipped -Because "sample_techproducts_configs not found" + } + } + + It "downconfig" { + if (-not $script:SolrRunning) { Set-ItResult -Skipped -Because "Solr is not running"; return } + $tempDir = [System.IO.Path]::GetTempPath() + $downloadDir = Join-Path $tempDir "downconfig_$([System.IO.Path]::GetRandomFileName())" + New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null + + try { + $output = Test-CommandOutput @("zk", "downconfig", "-z", "localhost:$ZK_PORT", "-n", "_default", "-d", $downloadDir) "zk downconfig" + $output | Should -Match "Downloading" + $LASTEXITCODE | Should -Match 0 + } finally { + # Cleanup + if (Test-Path $downloadDir) { + Remove-Item $downloadDir -Recurse -Force + } + } + } + } +}