diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 70e4712..e71763f 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -152,6 +152,7 @@ jobs: integration-tests: name: Integration Tests (${{ matrix.os }}) runs-on: ${{ matrix.os }} + timeout-minutes: 90 # Only run for pushes to main or PRs from the same repo (has access to secrets) if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository needs: [unit-tests, unit-tests-ps51] @@ -237,6 +238,7 @@ jobs: - name: Run Integration Tests if: steps.check-secrets.outputs.secrets_configured == 'true' + timeout-minutes: 75 shell: pwsh env: PLEX_SERVER_URI: ${{ secrets.PLEX_SERVER_URI }} diff --git a/tests/Integration/IntegrationTestHelpers.psm1 b/tests/Integration/IntegrationTestHelpers.psm1 index 112ea96..6f71f1a 100644 --- a/tests/Integration/IntegrationTestHelpers.psm1 +++ b/tests/Integration/IntegrationTestHelpers.psm1 @@ -249,6 +249,87 @@ function Remove-IntegrationTestServers { } } +function Start-SyncProgressMonitor { + <# + .SYNOPSIS + Starts a background timer that reports download progress to CI logs + + .PARAMETER Path + The destination directory to monitor for downloaded files + + .PARAMETER IntervalSeconds + How often to report progress (default: 30 seconds) + + .OUTPUTS + PSCustomObject + Returns a monitor object to pass to Stop-SyncProgressMonitor + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Path, + + [Parameter(Mandatory = $false)] + [int]$IntervalSeconds = 30 + ) + + $timer = [System.Timers.Timer]::new($IntervalSeconds * 1000) + $timer.AutoReset = $true + + $startTime = [DateTime]::UtcNow + $sourceId = "SyncProgressMonitor-$([Guid]::NewGuid())" + $eventAction = Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier $sourceId -MessageData @{ + Path = $Path + StartTime = $startTime + } -Action { + $destination = $Event.MessageData.Path + $elapsed = [DateTime]::UtcNow - $Event.MessageData.StartTime + $elapsedFormatted = '{0}m{1}s' -f [math]::Floor($elapsed.TotalMinutes), $elapsed.Seconds + + if (Test-Path $destination) { + $files = Get-ChildItem -Path $destination -Recurse -File -ErrorAction SilentlyContinue + $totalBytes = ($files | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum + $totalMB = [math]::Round($totalBytes / 1MB, 1) + Write-Host " [SYNC PROGRESS] $elapsedFormatted elapsed | $($files.Count) files | $totalMB MB downloaded" + } + else { + Write-Host " [SYNC PROGRESS] $elapsedFormatted elapsed | Waiting for first file..." + } + } + + $timer.Start() + + return [PSCustomObject]@{ + Timer = $timer + EventSubscription = $sourceId + StartTime = $startTime + } +} + +function Stop-SyncProgressMonitor { + <# + .SYNOPSIS + Stops a progress monitor started by Start-SyncProgressMonitor + + .PARAMETER Monitor + The monitor object returned by Start-SyncProgressMonitor + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [PSCustomObject]$Monitor + ) + + $Monitor.Timer.Stop() + Unregister-Event -SourceIdentifier $Monitor.EventSubscription -ErrorAction SilentlyContinue + $Monitor.Timer.Dispose() + + $elapsed = [DateTime]::UtcNow - $Monitor.StartTime + $elapsedFormatted = '{0}m{1}s' -f [math]::Floor($elapsed.TotalMinutes), $elapsed.Seconds + Write-Host " [SYNC COMPLETE] Total time: $elapsedFormatted" +} + Export-ModuleMember -Function Test-IntegrationPrerequisites, Get-IntegrationTestContext, ` Get-IntegrationConfigPath, Invoke-IntegrationTestWithRetry, Backup-ServerConfiguration, ` - Restore-ServerConfiguration, Remove-IntegrationTestServers + Restore-ServerConfiguration, Remove-IntegrationTestServers, Start-SyncProgressMonitor, ` + Stop-SyncProgressMonitor diff --git a/tests/Integration/Public/MediaSync.Integration.tests.ps1 b/tests/Integration/Public/MediaSync.Integration.tests.ps1 index c58823a..3280970 100644 --- a/tests/Integration/Public/MediaSync.Integration.tests.ps1 +++ b/tests/Integration/Public/MediaSync.Integration.tests.ps1 @@ -255,8 +255,14 @@ Describe 'Sync-PatMedia Integration Tests' -Skip:(-not $script:integrationEnable It 'Downloads media files from playlist' { if (-not $script:hasItems) { Set-ItResult -Skipped -Because 'Travel playlist has no items' } - # This actually downloads! Only runs if playlist has items - $result = Sync-PatMedia -Destination $script:syncDestination -PassThru -Confirm:$false + # Monitor download progress in CI logs (every 30s) + $monitor = Start-SyncProgressMonitor -Path $script:syncDestination + try { + $result = Sync-PatMedia -Destination $script:syncDestination -PassThru -Confirm:$false + } + finally { + Stop-SyncProgressMonitor -Monitor $monitor + } $result | Should -Not -BeNullOrEmpty @@ -273,8 +279,14 @@ Describe 'Sync-PatMedia Integration Tests' -Skip:(-not $script:integrationEnable It 'Is idempotent - second run downloads nothing' { if (-not $script:hasItems) { Set-ItResult -Skipped -Because 'Travel playlist has no items' } - # Run sync again - should detect existing files and skip - $result = Sync-PatMedia -Destination $script:syncDestination -PassThru -Confirm:$false + # Monitor for visibility even though this should be fast + $monitor = Start-SyncProgressMonitor -Path $script:syncDestination -IntervalSeconds 10 + try { + $result = Sync-PatMedia -Destination $script:syncDestination -PassThru -Confirm:$false + } + finally { + Stop-SyncProgressMonitor -Monitor $monitor + } # ItemsToAdd should be 0 since files already exist $result.ItemsToAdd | Should -Be 0