fix: recognize unlocked N lanes in shadow aggregation
This commit is contained in:
104
scripts/shadow-lane-lock/test-shadow-lane-lock.ps1
Normal file
104
scripts/shadow-lane-lock/test-shadow-lane-lock.ps1
Normal file
@@ -0,0 +1,104 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user