test(render): add connected remote-player shadow gate
This commit is contained in:
parent
e56ac57486
commit
a861510a37
4 changed files with 376 additions and 0 deletions
|
|
@ -12,6 +12,8 @@ public sealed class ConnectedRenderPackGateContractTests
|
|||
private const string SoakScript = "run-connected-r6-soak.ps1";
|
||||
private const string PackageLifecycleScript =
|
||||
"run-connected-render-pack-package-lifecycle.ps1";
|
||||
private const string RemotePlayerScript =
|
||||
"run-connected-render-pack-remote-player-gate.ps1";
|
||||
|
||||
[Fact]
|
||||
public void ScreenshotJsonCarriesAuthoritativeCasterClassCounters()
|
||||
|
|
@ -233,6 +235,40 @@ Remove-ConnectedGraphicalSessionConfig -State $state -Path $sessionConfigPath
|
|||
"checkpoint package-lifecycle");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectedRemotePlayerGateUsesDistinctCharactersAndRequiresCasterEvidence()
|
||||
{
|
||||
string source = ReadTool(RemotePlayerScript);
|
||||
Assert.Contains("PrimaryCharacterIndex = 0", source, StringComparison.Ordinal);
|
||||
Assert.Contains("ObserverCharacterIndex = 1", source, StringComparison.Ordinal);
|
||||
Assert.Contains("Primary and observer character indexes must differ", source, StringComparison.Ordinal);
|
||||
Assert.Contains("Get-ConnectedGateBinaryIdentity", source, StringComparison.Ordinal);
|
||||
Assert.Contains("-CharacterIndex $ObserverCharacterIndex", source, StringComparison.Ordinal);
|
||||
Assert.Contains("-CharacterIndex $PrimaryCharacterIndex", source, StringComparison.Ordinal);
|
||||
Assert.Contains("[int]$classes.RemotePlayers -lt 1", source, StringComparison.Ordinal);
|
||||
Assert.Contains("both clients entered world with the same character identity", source, StringComparison.Ordinal);
|
||||
Assert.Contains("Restore-ConnectedRenderPackGateEnvironment $primaryState", source, StringComparison.Ordinal);
|
||||
Assert.Contains("Restore-ConnectedRenderPackGateEnvironment $observerState", source, StringComparison.Ordinal);
|
||||
|
||||
string observer = ReadTool("connected-render-pack-remote-observer.route.txt");
|
||||
AssertAppearsInOrder(
|
||||
observer,
|
||||
"command /teleloc 0x09040008",
|
||||
"wait materialized 1 90000",
|
||||
"input down MovementForward",
|
||||
"input up MovementForward",
|
||||
"checkpoint remote-observer");
|
||||
|
||||
string primary = ReadTool("connected-render-pack-remote-primary.route.txt");
|
||||
AssertAppearsInOrder(
|
||||
primary,
|
||||
"command /teleloc 0x09040008",
|
||||
"wait materialized 1 90000",
|
||||
"screenshot remote-player-before 15000",
|
||||
"screenshot remote-player-after 15000",
|
||||
"checkpoint remote-primary");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EveryConnectedScreenshotMustProveExactFailureFreeActivation()
|
||||
{
|
||||
|
|
|
|||
14
tools/connected-render-pack-remote-observer.route.txt
Normal file
14
tools/connected-render-pack-remote-observer.route.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Secondary character for the connected remote-player shadow gate. It remains
|
||||
# in the same outdoor receiver scene as the primary and crosses several metres
|
||||
# while the primary captures before/after metadata.
|
||||
|
||||
wait world-ready 90000
|
||||
wait world-visible 30000
|
||||
command /teleloc 0x09040008 14.4 188.6 87.705
|
||||
wait materialized 1 90000
|
||||
sleep 6000
|
||||
input down MovementForward
|
||||
sleep 6000
|
||||
input up MovementForward
|
||||
checkpoint remote-observer
|
||||
sleep 30000
|
||||
13
tools/connected-render-pack-remote-primary.route.txt
Normal file
13
tools/connected-render-pack-remote-primary.route.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Primary character for the connected remote-player shadow gate. The observer
|
||||
# is already materialized before this process starts.
|
||||
|
||||
wait world-ready 90000
|
||||
wait world-visible 30000
|
||||
command /teleloc 0x09040008 11.4 188.6 87.705
|
||||
wait materialized 1 90000
|
||||
sleep 4000
|
||||
screenshot remote-player-before 15000
|
||||
sleep 6000
|
||||
screenshot remote-player-after 15000
|
||||
checkpoint remote-primary
|
||||
sleep 1000
|
||||
313
tools/run-connected-render-pack-remote-player-gate.ps1
Normal file
313
tools/run-connected-render-pack-remote-player-gate.ps1
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Repository = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path,
|
||||
[string]$Account = $env:ACDREAM_TEST_USER,
|
||||
[string]$Password = $env:ACDREAM_TEST_PASS,
|
||||
[ValidateRange(0, [int]::MaxValue)][int]$PrimaryCharacterIndex = 0,
|
||||
[ValidateRange(0, [int]::MaxValue)][int]$ObserverCharacterIndex = 1,
|
||||
[switch]$SkipBuild,
|
||||
[int]$LoginTimeoutSeconds = 90
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
. (Join-Path $PSScriptRoot 'connected-render-pack-gate-common.ps1')
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Account)) { $Account = 'testaccount' }
|
||||
if ([string]::IsNullOrWhiteSpace($Password)) { $Password = 'testpassword' }
|
||||
if ($PrimaryCharacterIndex -eq $ObserverCharacterIndex) {
|
||||
throw 'Primary and observer character indexes must differ.'
|
||||
}
|
||||
|
||||
$startedUtc = [DateTime]::UtcNow.ToString('O')
|
||||
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$root = Join-Path $Repository `
|
||||
"artifacts\atmospheric-rendering\connected-remote-player-$stamp"
|
||||
$observerRoot = Join-Path $root 'observer'
|
||||
$primaryRoot = Join-Path $root 'primary'
|
||||
$observerRoute = Join-Path $Repository `
|
||||
'tools\connected-render-pack-remote-observer.route.txt'
|
||||
$primaryRoute = Join-Path $Repository `
|
||||
'tools\connected-render-pack-remote-primary.route.txt'
|
||||
$exe = Join-Path $Repository `
|
||||
'src\AcDream.App\bin\Release\net10.0\AcDream.App.exe'
|
||||
$observerState = $null
|
||||
$primaryState = $null
|
||||
$observerConfig = $null
|
||||
$primaryConfig = $null
|
||||
$observer = $null
|
||||
$primary = $null
|
||||
$binaryIdentity = $null
|
||||
$result = $null
|
||||
$failure = $null
|
||||
|
||||
function Set-ClientEnvironment {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][object]$State,
|
||||
[Parameter(Mandatory = $true)][string]$ArtifactDirectory,
|
||||
[Parameter(Mandatory = $true)][string]$Route
|
||||
)
|
||||
$env:ACDREAM_CONFIG_DIR = $State.ConfigDirectory
|
||||
$env:ACDREAM_DATA_DIR = $State.DataDirectory
|
||||
$env:ACDREAM_CACHE_DIR = $State.CacheDirectory
|
||||
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||
$env:ACDREAM_LIVE = '1'
|
||||
$env:ACDREAM_TEST_HOST = '127.0.0.1'
|
||||
$env:ACDREAM_TEST_PORT = '9000'
|
||||
$env:ACDREAM_TEST_USER = $Account
|
||||
$env:ACDREAM_TEST_PASS = $Password
|
||||
$env:ACDREAM_RETAIL_UI = '1'
|
||||
$env:ACDREAM_DEVTOOLS = '0'
|
||||
$env:ACDREAM_UI_PROBE_DUMP = '0'
|
||||
$env:ACDREAM_UI_PROBE_SCRIPT = $Route
|
||||
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $ArtifactDirectory
|
||||
$env:ACDREAM_NO_AUDIO = '1'
|
||||
}
|
||||
|
||||
function Wait-ForLogPattern {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][Diagnostics.Process]$Client,
|
||||
[Parameter(Mandatory = $true)][string]$Path,
|
||||
[Parameter(Mandatory = $true)][string]$Pattern,
|
||||
[Parameter(Mandatory = $true)][int]$TimeoutSeconds
|
||||
)
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
|
||||
while ([DateTime]::UtcNow -lt $deadline) {
|
||||
$Client.Refresh()
|
||||
if ($Client.HasExited) {
|
||||
throw "client exited with code $($Client.ExitCode) while waiting for '$Pattern'"
|
||||
}
|
||||
if ((Test-Path -LiteralPath $Path) -and
|
||||
(Select-String -LiteralPath $Path -SimpleMatch $Pattern -Quiet)) {
|
||||
return
|
||||
}
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
throw "timed out after $TimeoutSeconds seconds waiting for '$Pattern'"
|
||||
}
|
||||
|
||||
function Close-ClientGracefully {
|
||||
param([Parameter(Mandatory = $true)][Diagnostics.Process]$Client)
|
||||
$Client.Refresh()
|
||||
if ($Client.HasExited) { return }
|
||||
if (-not $Client.CloseMainWindow() -or -not $Client.WaitForExit(30000)) {
|
||||
throw "client $($Client.Id) did not accept graceful close within 30 seconds"
|
||||
}
|
||||
$Client.WaitForExit()
|
||||
}
|
||||
|
||||
function Stop-ExactClient {
|
||||
param([Diagnostics.Process]$Client)
|
||||
if ($null -eq $Client) { return }
|
||||
$Client.Refresh()
|
||||
if (-not $Client.HasExited) {
|
||||
$closed = $Client.CloseMainWindow()
|
||||
if (-not $closed -or -not $Client.WaitForExit(10000)) {
|
||||
Stop-Process -Id $Client.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Read-StatusEvents {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "session status stream is missing: '$Path'"
|
||||
}
|
||||
return @(Get-Content -LiteralPath $Path | ForEach-Object {
|
||||
$_ | ConvertFrom-Json
|
||||
})
|
||||
}
|
||||
|
||||
function Assert-TerminalStatus {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
$events = @(Read-StatusEvents $Path)
|
||||
foreach ($name in @('started', 'connected', 'characterList', 'enteredWorld',
|
||||
'disconnected', 'exited')) {
|
||||
$matching = @($events | Where-Object { $_.e -ceq $name })
|
||||
if ($matching.Count -ne 1) {
|
||||
throw "'$Path' expected one '$name' event, observed $($matching.Count)"
|
||||
}
|
||||
}
|
||||
$entered = @($events | Where-Object { $_.e -ceq 'enteredWorld' })[0]
|
||||
$exited = @($events | Where-Object { $_.e -ceq 'exited' })[0]
|
||||
if ([int]$exited.code -ne 0) {
|
||||
throw "'$Path' terminal status code was $($exited.code)"
|
||||
}
|
||||
return $entered
|
||||
}
|
||||
|
||||
function Read-RemoteMetadata {
|
||||
param([Parameter(Mandatory = $true)][string]$Name)
|
||||
Assert-ConnectedGateSafeLeafName $Name
|
||||
$path = Join-Path $primaryRoot "screenshots\$Name.metadata.json"
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||
throw "remote-player metadata is missing: '$path'"
|
||||
}
|
||||
$metadata = Get-Content -Raw -LiteralPath $path | ConvertFrom-Json
|
||||
$pack = $metadata.RenderPack
|
||||
$classes = $pack.ShadowTransformChurn.CasterClasses
|
||||
if ([int]$metadata.SchemaVersion -ne 1 -or [int]$pack.State -ne 2 -or
|
||||
[string]$pack.PackId -cne 'acdream.atmospheric' -or
|
||||
[string]$pack.PresetId -cne 'medium') {
|
||||
throw "'$Name' did not capture active Atmospheric Medium metadata"
|
||||
}
|
||||
if (-not [bool]$pack.Outdoor -or [double]$pack.DirectionalShadowStrength -le 0) {
|
||||
throw "'$Name' did not capture an active outdoor directional-shadow frame"
|
||||
}
|
||||
if ([int]$classes.LocalPlayers -ne 1 -or [int]$classes.RemotePlayers -lt 1) {
|
||||
throw "'$Name' caster classes were local=$($classes.LocalPlayers), remote=$($classes.RemotePlayers)"
|
||||
}
|
||||
if ([int]$pack.ShadowCasterCount -le 0 -or [int]$pack.CascadeDrawCount -ne 3) {
|
||||
throw "'$Name' did not record complete Medium shadow work"
|
||||
}
|
||||
return [pscustomobject][ordered]@{
|
||||
Name = $Name
|
||||
Path = $path
|
||||
PackId = $pack.PackId
|
||||
PresetId = $pack.PresetId
|
||||
ShadowCasterCount = $pack.ShadowCasterCount
|
||||
CascadeDrawCount = $pack.CascadeDrawCount
|
||||
LocalPlayers = $classes.LocalPlayers
|
||||
RemotePlayers = $classes.RemotePlayers
|
||||
NonPlayerCreatures = $classes.NonPlayerCreatures
|
||||
EquippedChildren = $classes.EquippedChildren
|
||||
DirectionalShadowSourceKind = $pack.DirectionalShadowSourceKind
|
||||
DirectionalShadowStrength = $pack.DirectionalShadowStrength
|
||||
RemoteRootChanges = $pack.ShadowTransformChurn.LiveDynamicRootChanges
|
||||
SharedWorldTransformUsedInstances = $pack.SharedWorldTransformUsedInstances
|
||||
}
|
||||
}
|
||||
|
||||
if (@(Get-Process -Name AcDream.App -ErrorAction SilentlyContinue).Count -gt 0) {
|
||||
throw 'an AcDream.App client is already running; close it gracefully before the two-client gate'
|
||||
}
|
||||
if (@(Get-NetUDPEndpoint -LocalPort 9000 -ErrorAction SilentlyContinue).Count -eq 0) {
|
||||
throw 'local ACE is not listening on UDP port 9000'
|
||||
}
|
||||
foreach ($required in @($observerRoute, $primaryRoute)) {
|
||||
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
|
||||
throw "route file not found: '$required'"
|
||||
}
|
||||
}
|
||||
if (-not $SkipBuild) {
|
||||
Write-Output 'Building Release solution...'
|
||||
& dotnet build (Join-Path $Repository 'AcDream.slnx') -c Release --no-restore
|
||||
if ($LASTEXITCODE -ne 0) { throw "Release build failed with exit code $LASTEXITCODE" }
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) {
|
||||
throw "client executable not found: '$exe'"
|
||||
}
|
||||
|
||||
$null = New-Item -ItemType Directory -Path $observerRoot, $primaryRoot
|
||||
foreach ($path in @($root, $observerRoot, $primaryRoot)) {
|
||||
Assert-ConnectedGateNoReparsePoint $path
|
||||
}
|
||||
$observerState = New-ConnectedRenderPackGateState -Root $observerRoot -Preset medium
|
||||
$primaryState = New-ConnectedRenderPackGateState -Root $primaryRoot -Preset medium
|
||||
try {
|
||||
$binaryIdentity = Get-ConnectedGateBinaryIdentity `
|
||||
-Repository $Repository -Executable $exe -SkipBuild:$SkipBuild
|
||||
$observerStatus = Join-Path $observerState.StateDirectory 'status\observer.jsonl'
|
||||
$primaryStatus = Join-Path $primaryState.StateDirectory 'status\primary.jsonl'
|
||||
$observerConfig = New-ConnectedGraphicalSessionConfig `
|
||||
-State $observerState -Account $Account `
|
||||
-CharacterIndex $ObserverCharacterIndex -Plugins @() `
|
||||
-StatusFile $observerStatus
|
||||
$primaryConfig = New-ConnectedGraphicalSessionConfig `
|
||||
-State $primaryState -Account $Account `
|
||||
-CharacterIndex $PrimaryCharacterIndex -Plugins @() `
|
||||
-StatusFile $primaryStatus
|
||||
|
||||
$observerStdout = Join-Path $observerRoot 'client.stdout.log'
|
||||
$observerStderr = Join-Path $observerRoot 'client.stderr.log'
|
||||
Set-ClientEnvironment $observerState $observerRoot $observerRoute
|
||||
$observer = Start-Process -FilePath $exe -WorkingDirectory $Repository `
|
||||
-ArgumentList @('--session-config', ('"' + $observerConfig + '"')) `
|
||||
-RedirectStandardOutput $observerStdout `
|
||||
-RedirectStandardError $observerStderr -PassThru
|
||||
Wait-ForLogPattern $observer $observerStdout 'live: in world' $LoginTimeoutSeconds
|
||||
Wait-ForLogPattern $observer $observerStdout 'live: teleport materialized' 90
|
||||
|
||||
$primaryStdout = Join-Path $primaryRoot 'client.stdout.log'
|
||||
$primaryStderr = Join-Path $primaryRoot 'client.stderr.log'
|
||||
Set-ClientEnvironment $primaryState $primaryRoot $primaryRoute
|
||||
$primary = Start-Process -FilePath $exe -WorkingDirectory $Repository `
|
||||
-ArgumentList @('--session-config', ('"' + $primaryConfig + '"')) `
|
||||
-RedirectStandardOutput $primaryStdout `
|
||||
-RedirectStandardError $primaryStderr -PassThru
|
||||
Wait-ForLogPattern $primary $primaryStdout 'live: in world' $LoginTimeoutSeconds
|
||||
Wait-ForLogPattern $primary $primaryStdout `
|
||||
'[world-gate] screenshot-complete name=remote-player-after' 120
|
||||
Wait-ForLogPattern $primary $primaryStdout `
|
||||
'[world-gate] checkpoint name=remote-primary' 30
|
||||
|
||||
Close-ClientGracefully $primary
|
||||
Close-ClientGracefully $observer
|
||||
if ($primary.ExitCode -ne 0 -or $observer.ExitCode -ne 0) {
|
||||
throw "client exit codes were primary=$($primary.ExitCode), observer=$($observer.ExitCode)"
|
||||
}
|
||||
$primaryEntered = Assert-TerminalStatus $primaryStatus
|
||||
$observerEntered = Assert-TerminalStatus $observerStatus
|
||||
if ([uint32]$primaryEntered.characterId -eq [uint32]$observerEntered.characterId) {
|
||||
throw 'both clients entered world with the same character identity'
|
||||
}
|
||||
$before = Read-RemoteMetadata 'remote-player-before'
|
||||
$after = Read-RemoteMetadata 'remote-player-after'
|
||||
$result = [pscustomobject][ordered]@{
|
||||
Passed = $true
|
||||
PrimaryProcessId = $primary.Id
|
||||
ObserverProcessId = $observer.Id
|
||||
PrimaryCharacterId = $primaryEntered.characterId
|
||||
ObserverCharacterId = $observerEntered.characterId
|
||||
PrimaryExitCode = $primary.ExitCode
|
||||
ObserverExitCode = $observer.ExitCode
|
||||
Before = $before
|
||||
After = $after
|
||||
}
|
||||
Write-Output (
|
||||
"PASS remote-player casters before=$($before.RemotePlayers) " +
|
||||
"after=$($after.RemotePlayers) primary=0x$('{0:X8}' -f [uint32]$primaryEntered.characterId) " +
|
||||
"observer=0x$('{0:X8}' -f [uint32]$observerEntered.characterId)")
|
||||
}
|
||||
catch {
|
||||
$failure = $_.Exception.ToString()
|
||||
}
|
||||
finally {
|
||||
Stop-ExactClient $primary
|
||||
Stop-ExactClient $observer
|
||||
if ($null -ne $primary) { $primary.Dispose() }
|
||||
if ($null -ne $observer) { $observer.Dispose() }
|
||||
if ($null -ne $primaryConfig -and $null -ne $primaryState) {
|
||||
Remove-ConnectedGraphicalSessionConfig -State $primaryState -Path $primaryConfig
|
||||
}
|
||||
if ($null -ne $observerConfig -and $null -ne $observerState) {
|
||||
Remove-ConnectedGraphicalSessionConfig -State $observerState -Path $observerConfig
|
||||
}
|
||||
if ($null -ne $primaryState) {
|
||||
Restore-ConnectedRenderPackGateEnvironment $primaryState
|
||||
}
|
||||
if ($null -ne $observerState) {
|
||||
Restore-ConnectedRenderPackGateEnvironment $observerState
|
||||
}
|
||||
$report = [pscustomobject][ordered]@{
|
||||
Passed = $null -ne $result -and [string]::IsNullOrWhiteSpace($failure)
|
||||
StartedUtc = $startedUtc
|
||||
FinishedUtc = [DateTime]::UtcNow.ToString('O')
|
||||
SourceCommit = if ($null -eq $binaryIdentity) { $null } else { $binaryIdentity.SourceCommit }
|
||||
BinaryCommit = if ($null -eq $binaryIdentity) { $null } else { $binaryIdentity.BinaryCommit }
|
||||
BinaryMatchesSource = if ($null -eq $binaryIdentity) { $false } else { $binaryIdentity.BinaryMatchesSource }
|
||||
TrackedSourceStatus = if ($null -eq $binaryIdentity) { @() } else { @($binaryIdentity.SourceTrackedStatus) }
|
||||
PrimaryCharacterIndex = $PrimaryCharacterIndex
|
||||
ObserverCharacterIndex = $ObserverCharacterIndex
|
||||
Result = $result
|
||||
Failure = $failure
|
||||
}
|
||||
$reportPath = Join-Path $root 'report.json'
|
||||
$report | ConvertTo-Json -Depth 10 |
|
||||
Set-Content -Encoding utf8 -LiteralPath $reportPath
|
||||
Write-Output "REPORT=$reportPath"
|
||||
Write-Output "RESULT=$(if ($report.Passed) { 'PASS' } else { 'FAIL' })"
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($failure)) { throw $failure }
|
||||
if ($null -eq $result) { throw 'remote-player gate produced no result' }
|
||||
Loading…
Add table
Add a link
Reference in a new issue