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() }