Files
subtrack/scripts/portability-test.ps1
T

167 lines
8.1 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据可移植性测试:单文件、只拷 daili.db 无损迁移、删库即重置
# 用法: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\portability-test.ps1
param(
[string]$Exe = '',
[string]$Work = '',
[int]$PortA = 17781,
[int]$PortB = 17782
)
$Root = Split-Path -Parent $PSScriptRoot
if (-not $Exe) { $Exe = Join-Path $Root 'daili.exe' }
if (-not $Work) { $Work = Join-Path $Root 'portability-run' }
$Evidence = Join-Path $Root 'test-evidence'
if (-not (Test-Path $Exe)) { throw "找不到被测程序:$Exe(先构建或已用 -Exe 指定)" }
$ErrorActionPreference = 'Continue'
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Add-Type -AssemblyName System.Net.Http
$script:fail = 0
$script:results = @()
function Check {
param([string]$Id, [string]$Name, $Cond, [string]$Detail = '')
if ($Cond -eq $true) {
Write-Host (" [PASS] {0} {1}" -f $Id, $Name) -ForegroundColor Green
$script:results += "PASS|$Id|$Name|$Detail"
} else {
Write-Host (" [FAIL] {0} {1} {2}" -f $Id, $Name, $Detail) -ForegroundColor Red
$script:results += "FAIL|$Id|$Name|$Detail"
$script:fail++
}
}
function New-Client {
$h = New-Object System.Net.Http.HttpClientHandler
$h.UseCookies = $true
$h.AllowAutoRedirect = $true
$c = New-Object System.Net.Http.HttpClient($h)
$c.Timeout = [TimeSpan]::FromSeconds(10)
return $c
}
function Post-Form($client, $url, $kv) {
$pairs = New-Object 'System.Collections.Generic.List[System.Collections.Generic.KeyValuePair[string,string]]'
foreach ($k in $kv.Keys) { $pairs.Add([System.Collections.Generic.KeyValuePair[string,string]]::new($k, [string]$kv[$k])) }
return $client.PostAsync($url, [System.Net.Http.FormUrlEncodedContent]::new($pairs)).Result
}
function Get-Page($client, $url) {
try {
$r = $client.GetAsync($url).Result
return @{ Code = [int]$r.StatusCode; Path = $r.RequestMessage.RequestUri.AbsolutePath; Body = $r.Content.ReadAsStringAsync().Result }
} catch { return @{ Code = 0; Path = ''; Body = '' } }
}
function Wait-Up([int]$port, [int]$seconds = 15) {
$c = New-Client
for ($i = 0; $i -lt ($seconds * 4); $i++) {
if ((Get-Page $c "http://127.0.0.1:$port/healthz").Code -eq 200) { return $true }
Start-Sleep -Milliseconds 250
}
return $false
}
function Start-App([string]$dir, [int]$port) {
$p = Start-Process -FilePath (Join-Path $dir 'daili.exe') -ArgumentList @('-addr', "127.0.0.1:$port", '-no-open') `
-WorkingDirectory $dir -WindowStyle Hidden -PassThru `
-RedirectStandardOutput (Join-Path $dir 'stdout.log') -RedirectStandardError (Join-Path $dir 'stderr.log')
return @{ Proc = $p; Up = (Wait-Up $port) }
}
function Stop-App($h) {
if ($h -and $h.Proc) { Stop-Process -Id $h.Proc.Id -Force -ErrorAction SilentlyContinue }
Start-Sleep -Milliseconds 700
}
function New-AppDir([string]$name) {
$dir = Join-Path $Work $name
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Copy-Item $Exe (Join-Path $dir 'daili.exe') -Force
return $dir
}
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Work | Out-Null
$today = (Get-Date).Date.ToString('yyyy-MM-dd')
$newEnd = (Get-Date).Date.AddDays(45).ToString('yyyy-MM-dd')
Write-Host "`n=== 数据可移植性测试 ===" -ForegroundColor Cyan
# ---------- 1. A 目录造数据 ----------
Write-Host "`n-- 1. 在 A 目录造数据 --" -ForegroundColor Cyan
$dirA = New-AppDir 'A'
$hA = Start-App $dirA $PortA
Check 'P-01' 'A 目录启动成功' $hA.Up ""
$cA = New-Client
Post-Form $cA "http://127.0.0.1:$PortA/setup" @{ username = 'admin'; display_name = '老板'; password = 'daili123'; password2 = 'daili123' } | Out-Null
Post-Form $cA "http://127.0.0.1:$PortA/packages" @{ name = '包月日常'; unit = 'day'; default_amount = '30'; price = '300'; color = '#2F5D50'; note = ''; sort_order = '1' } | Out-Null
Post-Form $cA "http://127.0.0.1:$PortA/customers" @{ tail = '8001'; name = '迁移客户'; game_name = '王者荣耀'; game_server = ''; contact = 'wx:8001'; tags = '老客户'; source = ''; note = '迁移前数据' } | Out-Null
Post-Form $cA "http://127.0.0.1:$PortA/customers/1/subscriptions" @{ package_id = '1'; start_date = $today; days = '30'; note = '首单' } | Out-Null
$detail = (Get-Page $cA "http://127.0.0.1:$PortA/customers/1").Body
$subId = [regex]::Match($detail, 'id="sub-(\d+)"').Groups[1].Value
Post-Form $cA "http://127.0.0.1:$PortA/subscriptions/$subId/extend" @{ new_end_date = $newEnd; note = '迁移前续费'; back = '/customers/1' } | Out-Null
Post-Form $cA "http://127.0.0.1:$PortA/subscriptions/$subId/checkin" @{ date = $today; status = 'done'; note = '迁移前打卡'; back = '/customers/1' } | Out-Null
$detail = (Get-Page $cA "http://127.0.0.1:$PortA/customers/1").Body
Check 'P-02' '造数成功(剩 46 天 + 打卡)' ($detail -match '剩 46 天' -and $detail -match '迁移前打卡') ""
Stop-App $hA
# ---------- 2. 关闭后应当只有一个数据库文件 ----------
Write-Host "`n-- 2. 关闭后检查文件形态 --" -ForegroundColor Cyan
$dbA = Join-Path $dirA 'daili.db'
$sidecars = @()
foreach ($s in '-wal', '-shm', '-journal') {
if (Test-Path ($dbA + $s)) { $sidecars += $s }
}
Check 'P-03' '关闭后只有 daili.db(无 -wal/-shm/-journal)' ((Test-Path $dbA) -and $sidecars.Count -eq 0) "sidecars=$($sidecars -join ',')"
$sizeA = (Get-Item $dbA).Length
Check 'P-04' '主库文件包含全部数据(远大于空库 4KB)' ($sizeA -gt 20000) "size=$sizeA"
# ---------- 3. 只拷贝 daili.db 到 B 目录 ----------
Write-Host "`n-- 3. 只拷贝 daili.db 到 B 目录 --" -ForegroundColor Cyan
$dirB = New-AppDir 'B'
Copy-Item $dbA (Join-Path $dirB 'daili.db') -Force
$hB = Start-App $dirB $PortB
Check 'P-05' 'B 目录用拷贝的库启动成功' $hB.Up ""
$cB = New-Client
$login = Post-Form $cB "http://127.0.0.1:$PortB/login" @{ username = 'admin'; password = 'daili123'; next = '/customers' }
$page = (Get-Page $cB "http://127.0.0.1:$PortB/customers").Body
Check 'P-06' '原管理员账号可用(口令随库迁移)' ($page -match '共 1 位' -and $page -match '迁移客户') ""
$detailB = (Get-Page $cB "http://127.0.0.1:$PortB/customers/1").Body
Check 'P-07' '订阅与剩余天数完整' ($detailB -match '剩 46 天') ""
Check 'P-08' '打卡与追加记录完整' ($detailB -match '迁移前打卡' -and $detailB -match '迁移前续费') ""
Check 'P-09' '套餐配置完整' ((Get-Page $cB "http://127.0.0.1:$PortB/packages").Body -match '包月日常') ""
$logs = (Get-Page $cB "http://127.0.0.1:$PortB/logs").Body
Check 'P-10' '操作日志完整' ($logs -match '追加时长' -and $logs -match '打卡') ""
Stop-App $hB
for ($i = 0; $i -lt 10 -and (Test-Path ($dbA + '-wal')); $i++) { Start-Sleep -Milliseconds 200 }
$bSide = @()
foreach ($s in '-wal', '-shm', '-journal') { if (Test-Path ((Join-Path $dirB 'daili.db') + $s)) { $bSide += $s } }
Check 'P-11' 'B 目录运行后仍无旁路文件' ($bSide.Count -eq 0) "sidecars=$($bSide -join ',')"
# ---------- 4. 删除 daili.db = 重置 ----------
Write-Host "`n-- 4. 删除 daili.db 重置数据 --" -ForegroundColor Cyan
foreach ($s in '', '-wal', '-shm', '-journal') { Remove-Item ($dbA + $s) -Force -ErrorAction SilentlyContinue }
$hA2 = Start-App $dirA $PortA
Check 'P-12' '删库后可正常启动' $hA2.Up ""
$cA2 = New-Client
$root = Get-Page $cA2 "http://127.0.0.1:$PortA/"
Check 'P-13' '删库后回到初始化(数据已清空)' ($root.Path -eq '/setup' -and $root.Body -match '创建第一个管理员') "path=$($root.Path)"
Stop-App $hA2
# ---------- 汇总 ----------
$pass = @($script:results | Where-Object { $_ -like 'PASS|*' }).Count
$fail = @($script:results | Where-Object { $_ -like 'FAIL|*' }).Count
Write-Host "`n=== 汇总:通过 $pass 项,失败 $fail 项 ===" -ForegroundColor ($(if ($fail -eq 0) { 'Green' } else { 'Red' }))
New-Item -ItemType Directory -Force -Path $Evidence | Out-Null
$script:results | Set-Content (Join-Path $Evidence 'portability-result.txt') -Encoding UTF8
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
if ($fail -gt 0) { exit 1 }
exit 0