Files
FT546MWS/scripts/shadow-lane-lock/test-shadow-lane-lock.ps1

105 lines
6.6 KiB
PowerShell

param(
[string]$Server = 'lpc:.',
[string]$SqlDirectory = "$PSScriptRoot/../../fortucky-algorithm/src/main/resources/db/sqlserver",
[switch]$TestRepair
)
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
# This runner accepts no database name: fixtures can only enter its own tiny scratch DB.
$testDb = 'FT546WMS_LaneLock_Test_' + [Guid]::NewGuid().ToString('N')
$cs = [Data.SqlClient.SqlConnectionStringBuilder]::new()
$cs['Data Source']=$Server; $cs['Initial Catalog']='master'; $cs['Integrated Security']=$true
$cs['Connect Timeout']=8; $cs['Application Name']='FT546ShadowLaneLockTests'
$cn = [Data.SqlClient.SqlConnection]::new($cs.ConnectionString)
function Exec([string]$sql) {
$cmd=$cn.CreateCommand(); $cmd.CommandTimeout=15; $cmd.CommandText=$sql
try { [void]$cmd.ExecuteNonQuery() } finally { $cmd.Dispose() }
}
function Scalar([string]$sql) {
$cmd=$cn.CreateCommand(); $cmd.CommandTimeout=15; $cmd.CommandText=$sql
try { $cmd.ExecuteScalar() } finally { $cmd.Dispose() }
}
function Assert([bool]$condition,[string]$message) {
if(-not $condition){throw "FAIL: $message"}
Write-Output "PASS: $message"
}
function Hash([string]$value) {
$sha=[Security.Cryptography.SHA256]::Create()
try { ([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($value)))).Replace('-','').ToLowerInvariant() }
finally { $sha.Dispose() }
}
function Definition { [string](Scalar "SELECT OBJECT_DEFINITION(OBJECT_ID('alg.usp_rebuild_lane_state'))") }
function Canonical([string]$value) { [regex]::Replace($value,'(?i)\A\s*(CREATE|ALTER)\s+PROCEDURE','ALTER PROCEDURE') }
$created=$false
$cn.Open()
try {
$dataPath=([string](Scalar "SELECT CONVERT(nvarchar(4000),SERVERPROPERTY('InstanceDefaultDataPath'))")).Replace("'","''")
$logPath=([string](Scalar "SELECT CONVERT(nvarchar(4000),SERVERPROPERTY('InstanceDefaultLogPath'))")).Replace("'","''")
Exec "CREATE DATABASE [$testDb] ON PRIMARY (NAME=N'$testDb',FILENAME=N'${dataPath}$testDb.mdf',SIZE=8MB,MAXSIZE=64MB,FILEGROWTH=8MB) LOG ON (NAME=N'${testDb}_log',FILENAME=N'${logPath}${testDb}_log.ldf',SIZE=8MB,MAXSIZE=64MB,FILEGROWTH=8MB)"
$created=$true; $cn.ChangeDatabase($testDb)
Exec 'SET NOCOUNT ON; SET LOCK_TIMEOUT 1500; SET DEADLOCK_PRIORITY LOW;'
Exec 'CREATE SCHEMA alg'
foreach($batch in ([IO.File]::ReadAllText("$PSScriptRoot/fixture.sql") -split '(?im)^\s*GO\s*\r?$')) {
if($batch.Trim()){ Exec $batch }
}
foreach($source in @('install-shadow.sql','upgrade-shadow-v2-to-v3.sql')) {
$text=[IO.File]::ReadAllText((Join-Path $SqlDirectory $source))
$bodies=@($text -split '(?im)^\s*GO\s*\r?$' | Where-Object {$_ -match '(?im)^\s*(CREATE|ALTER) PROCEDURE alg\.usp_rebuild_lane_state\b'})
Assert ($bodies.Count -eq 1) "$source has exactly one real rebuild body"
$body=$bodies[0]
$start=[regex]::Match($body,'(?im)^\s*(CREATE|ALTER) PROCEDURE alg\.usp_rebuild_lane_state\b').Index
$body=$body.Substring($start).Trim()
Exec "IF OBJECT_ID('alg.usp_rebuild_lane_state') IS NOT NULL DROP PROCEDURE alg.usp_rebuild_lane_state"
Exec ([regex]::Replace($body,'(?i)\A(CREATE|ALTER) PROCEDURE','CREATE PROCEDURE'))
Exec 'DELETE alg.lane_state'
Exec "EXEC alg.usp_rebuild_lane_state @warehouse_id='01'"
Exec ([IO.File]::ReadAllText("$PSScriptRoot/assertions.sql"))
Write-Output "PASS: $source actual T-SQL aggregation, exclusions, repeat and scoping"
if($TestRepair) {
$old="NULLIF(LTRIM(RTRIM(cargo.locked)), '') IS NULL"
$new="(NULLIF(LTRIM(RTRIM(cargo.locked)), '') IS NULL OR LTRIM(RTRIM(cargo.locked)) IN ('0', 'N'))"
$original=(Canonical (Definition)).Replace($new,$old)
Exec $original
$before=Definition
$backup=Join-Path $PSScriptRoot "$testDb-$source.backup.json"
$repair=Join-Path $PSScriptRoot 'repair-shadow-lane-lock.ps1'
try {
$null=& $repair -Server $Server -Database $testDb -Mode Check -ExpectedSha256 (Hash $before)
Assert ((Definition) -ceq $before) 'check mode leaves procedure unchanged'
$null=& $repair -Server $Server -Database $testDb -Mode Apply -ExpectedSha256 (Hash $before) -BackupFile $backup
$fixed=Definition
Assert ((Canonical $fixed) -ceq (Canonical $before).Replace($old,$new)) 'repair changes only the lock predicate'
Exec "EXEC alg.usp_rebuild_lane_state @warehouse_id='01'"
Exec ([IO.File]::ReadAllText("$PSScriptRoot/assertions.sql"))
$null=& $repair -Server $Server -Database $testDb -Mode Apply -ExpectedSha256 (Hash $fixed) -BackupFile $backup
Assert ((Definition) -ceq $fixed) 'repeat apply is a no-op'
$refused=$false
try { $null=& $repair -Server $Server -Database $testDb -Mode Apply -ExpectedSha256 ('0'*64) -BackupFile $backup }
catch { $refused=$_.Exception.Message -like '*hash mismatch*' }
Assert $refused 'unexpected definition is refused'
# Invalid ALTER must roll back atomically, including connection transaction cleanup.
$tx=$cn.BeginTransaction()
$cmd=$cn.CreateCommand(); $cmd.Transaction=$tx; $cmd.CommandText='ALTER PROCEDURE alg.usp_rebuild_lane_state AS INVALID SQL HERE'
$failed=$false
try { [void]$cmd.ExecuteNonQuery() } catch { $failed=$true } finally { $tx.Rollback();$cmd.Dispose();$tx.Dispose() }
Assert ($failed -and (Definition) -ceq $fixed) 'failed ALTER preserves the published procedure'
$null=& $repair -Server $Server -Database $testDb -Mode Rollback -ExpectedSha256 (Hash $fixed) -BackupFile $backup
Assert ((Canonical (Definition)) -ceq (Canonical $before)) 'rollback restores the exact original body'
Exec "EXEC alg.usp_rebuild_lane_state @warehouse_id='01'"
Assert ((Scalar "SELECT available_count FROM alg.lane_state WHERE warehouse_id='01' AND lane_code='UNLOCK_N'") -eq 0) 'rollback reproduces the original N exclusion'
} finally {
if(Test-Path -LiteralPath $backup){ Remove-Item -LiteralPath $backup }
}
}
}
} finally {
if($created) {
$cn.ChangeDatabase('master')
[Data.SqlClient.SqlConnection]::ClearAllPools()
if($testDb -notmatch '^FT546WMS_LaneLock_Test_[a-f0-9]{32}$'){throw 'Invalid scratch database name'}
Exec "DROP DATABASE [$testDb]"
Write-Output "Scratch database removed: $testDb"
}
$cn.Dispose()
}