2023-08-05 16:16:48 +12:00
#nullable enable
2022-06-19 20:22:28 -07:00
using System.Collections.Generic ;
2023-03-11 05:09:41 +01:00
using System.IO ;
2022-06-19 20:22:28 -07:00
using System.Linq ;
2023-08-05 16:16:48 +12:00
using System.Reflection ;
2022-09-15 20:17:02 -07:00
using System.Text ;
2022-06-19 20:22:28 -07:00
using System.Threading ;
using Content.Client.IoC ;
using Content.Client.Parallax.Managers ;
using Content.IntegrationTests.Tests ;
2022-07-05 08:02:24 -07:00
using Content.IntegrationTests.Tests.Destructible ;
2022-06-19 20:22:28 -07:00
using Content.IntegrationTests.Tests.DeviceNetwork ;
using Content.IntegrationTests.Tests.Interaction.Click ;
using Content.Server.GameTicking ;
using Content.Shared.CCVar ;
2023-08-02 03:09:25 +12:00
using Content.Shared.GameTicking ;
2022-06-19 20:22:28 -07:00
using Robust.Client ;
using Robust.Server ;
2023-08-02 03:09:25 +12:00
using Robust.Server.Player ;
2022-12-20 23:25:03 +01:00
using Robust.Shared ;
2022-06-19 20:22:28 -07:00
using Robust.Shared.Configuration ;
using Robust.Shared.ContentPack ;
using Robust.Shared.Exceptions ;
using Robust.Shared.GameObjects ;
using Robust.Shared.IoC ;
using Robust.Shared.Log ;
using Robust.Shared.Map ;
2022-11-22 13:12:04 +11:00
using Robust.Shared.Map.Components ;
2022-06-19 20:22:28 -07:00
using Robust.Shared.Network ;
using Robust.Shared.Prototypes ;
using Robust.Shared.Timing ;
2023-08-05 16:16:48 +12:00
using Robust.Shared.Utility ;
2022-06-19 20:22:28 -07:00
using Robust.UnitTesting ;
[assembly: LevelOfParallelism(3)]
namespace Content.IntegrationTests ;
2022-08-15 20:32:15 -07:00
/// <summary>
/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
/// </summary>
2023-08-05 16:16:48 +12:00
public static partial class PoolManager
2022-06-19 20:22:28 -07:00
{
2023-08-02 03:09:25 +12:00
public const string TestMap = "Empty" ;
2023-08-06 14:30:28 +12:00
private static readonly ( string cvar , string value ) [ ] TestCvars =
2022-06-19 20:22:28 -07:00
{
2023-03-11 05:09:41 +01:00
// @formatter:off
( CCVars . DatabaseSynchronous . Name , "true" ) ,
( CCVars . DatabaseSqliteDelay . Name , "0" ) ,
( CCVars . HolidaysEnabled . Name , "false" ) ,
2023-08-02 03:09:25 +12:00
( CCVars . GameMap . Name , TestMap ) ,
2023-03-11 05:09:41 +01:00
( CCVars . AdminLogsQueueSendDelay . Name , "0" ) ,
( CVars . NetPVS . Name , "false" ) ,
( CCVars . NPCMaxUpdates . Name , "999999" ) ,
( CVars . ThreadParallelCount . Name , "1" ) ,
( CCVars . GameRoleTimers . Name , "false" ) ,
2023-05-31 11:13:02 +10:00
( CCVars . GridFill . Name , "false" ) ,
2023-03-22 20:29:55 +11:00
( CCVars . ArrivalsShuttles . Name , "false" ) ,
2023-03-11 05:09:41 +01:00
( CCVars . EmergencyShuttleEnabled . Name , "false" ) ,
( CCVars . ProcgenPreload . Name , "false" ) ,
2023-05-16 06:36:45 -05:00
( CCVars . WorldgenEnabled . Name , "false" ) ,
2023-08-06 14:30:28 +12:00
( CVars . ReplayClientRecordingEnabled . Name , "false" ) ,
( CVars . ReplayServerRecordingEnabled . Name , "false" ) ,
( CCVars . GameDummyTicker . Name , "true" ) ,
( CCVars . GameLobbyEnabled . Name , "false" ) ,
( CCVars . ConfigPresetDevelopment . Name , "false" ) ,
( CCVars . AdminLogsEnabled . Name , "false" ) ,
// This breaks some tests.
// TODO: Figure out which tests this breaks.
( CVars . NetBufferSize . Name , "0" )
2023-03-11 05:09:41 +01:00
// @formatter:on
2022-06-19 20:22:28 -07:00
} ;
2023-07-05 21:54:25 -07:00
private static int _pairId ;
private static readonly object PairLock = new ( ) ;
2023-08-05 16:16:48 +12:00
private static bool _initialized ;
2022-09-15 20:17:02 -07:00
// Pair, IsBorrowed
2023-07-05 21:54:25 -07:00
private static readonly Dictionary < Pair , bool > Pairs = new ( ) ;
private static bool _dead ;
2023-08-05 16:16:48 +12:00
private static Exception ? _poolFailureReason ;
2022-06-19 20:22:28 -07:00
2023-03-11 05:09:41 +01:00
private static async Task < ( RobustIntegrationTest . ServerIntegrationInstance , PoolTestLogHandler ) > GenerateServer (
PoolSettings poolSettings ,
TextWriter testOut )
2022-06-19 20:22:28 -07:00
{
var options = new RobustIntegrationTest . ServerIntegrationOptions
{
ContentStart = true ,
Options = new ServerOptions ( )
{
LoadConfigAndUserData = false ,
LoadContentResources = ! poolSettings . NoLoadContent ,
} ,
ContentAssemblies = new [ ]
{
typeof ( Shared . Entry . EntryPoint ) . Assembly ,
typeof ( Server . Entry . EntryPoint ) . Assembly ,
typeof ( PoolManager ) . Assembly
}
} ;
2023-04-24 18:34:12 +12:00
var logHandler = new PoolTestLogHandler ( "SERVER" ) ;
logHandler . ActivateContext ( testOut ) ;
options . OverrideLogHandler = ( ) = > logHandler ;
2022-06-19 20:22:28 -07:00
options . BeforeStart + = ( ) = >
{
2023-07-05 21:54:25 -07:00
var entSysMan = IoCManager . Resolve < IEntitySystemManager > ( ) ;
var compFactory = IoCManager . Resolve < IComponentFactory > ( ) ;
entSysMan . LoadExtraSystemType < ResettingEntitySystemTests . TestRoundRestartCleanupEvent > ( ) ;
entSysMan . LoadExtraSystemType < InteractionSystemTests . TestInteractionSystem > ( ) ;
entSysMan . LoadExtraSystemType < DeviceNetworkTestSystem > ( ) ;
entSysMan . LoadExtraSystemType < TestDestructibleListenerSystem > ( ) ;
2022-06-19 20:22:28 -07:00
IoCManager . Resolve < ILogManager > ( ) . GetSawmill ( "loc" ) . Level = LogLevel . Error ;
2023-04-24 18:34:12 +12:00
IoCManager . Resolve < IConfigurationManager > ( )
. OnValueChanged ( RTCVars . FailureLogLevel , value = > logHandler . FailureLevel = value , true ) ;
2022-06-19 20:22:28 -07:00
} ;
2023-08-06 14:30:28 +12:00
SetDefaultCVars ( options ) ;
2022-06-19 20:22:28 -07:00
var server = new RobustIntegrationTest . ServerIntegrationInstance ( options ) ;
await server . WaitIdleAsync ( ) ;
2023-08-06 14:30:28 +12:00
await SetupCVars ( server , poolSettings ) ;
2023-03-11 05:09:41 +01:00
return ( server , logHandler ) ;
2022-06-19 20:22:28 -07:00
}
2022-08-15 20:32:15 -07:00
/// <summary>
/// This shuts down the pool, and disposes all the server/client pairs.
/// This is a one time operation to be used when the testing program is exiting.
/// </summary>
2022-06-19 20:22:28 -07:00
public static void Shutdown ( )
{
2022-09-15 20:17:02 -07:00
List < Pair > localPairs ;
2022-06-19 20:22:28 -07:00
lock ( PairLock )
{
2023-07-05 21:54:25 -07:00
if ( _dead )
2022-09-15 20:17:02 -07:00
return ;
2023-07-05 21:54:25 -07:00
_dead = true ;
2022-09-15 20:17:02 -07:00
localPairs = Pairs . Keys . ToList ( ) ;
}
foreach ( var pair in localPairs )
{
pair . Kill ( ) ;
}
2023-08-05 16:16:48 +12:00
_initialized = false ;
2022-09-15 20:17:02 -07:00
}
public static string DeathReport ( )
{
lock ( PairLock )
{
var builder = new StringBuilder ( ) ;
var pairs = Pairs . Keys . OrderBy ( pair = > pair . PairId ) ;
2022-06-19 20:22:28 -07:00
foreach ( var pair in pairs )
{
2022-09-15 20:17:02 -07:00
var borrowed = Pairs [ pair ] ;
builder . AppendLine ( $"Pair {pair.PairId}, Tests Run: {pair.TestHistory.Count}, Borrowed: {borrowed}" ) ;
2023-07-05 21:54:25 -07:00
for ( var i = 0 ; i < pair . TestHistory . Count ; i + + )
2022-09-15 20:17:02 -07:00
{
builder . AppendLine ( $"#{i}: {pair.TestHistory[i]}" ) ;
}
2022-06-19 20:22:28 -07:00
}
2022-09-15 20:17:02 -07:00
return builder . ToString ( ) ;
2022-06-19 20:22:28 -07:00
}
}
2023-03-11 05:09:41 +01:00
private static async Task < ( RobustIntegrationTest . ClientIntegrationInstance , PoolTestLogHandler ) > GenerateClient (
PoolSettings poolSettings ,
TextWriter testOut )
2022-06-19 20:22:28 -07:00
{
var options = new RobustIntegrationTest . ClientIntegrationOptions
{
FailureLogLevel = LogLevel . Warning ,
ContentStart = true ,
ContentAssemblies = new [ ]
{
typeof ( Shared . Entry . EntryPoint ) . Assembly ,
typeof ( Client . Entry . EntryPoint ) . Assembly ,
typeof ( PoolManager ) . Assembly
}
} ;
if ( poolSettings . NoLoadContent )
{
Assert . Warn ( "NoLoadContent does not work on the client, ignoring" ) ;
}
options . Options = new GameControllerOptions ( )
{
LoadConfigAndUserData = false ,
// LoadContentResources = !poolSettings.NoLoadContent
} ;
2023-04-24 18:34:12 +12:00
var logHandler = new PoolTestLogHandler ( "CLIENT" ) ;
logHandler . ActivateContext ( testOut ) ;
options . OverrideLogHandler = ( ) = > logHandler ;
2022-06-19 20:22:28 -07:00
options . BeforeStart + = ( ) = >
{
IoCManager . Resolve < IModLoader > ( ) . SetModuleBaseCallbacks ( new ClientModuleTestingCallbacks
{
ClientBeforeIoC = ( ) = >
{
2023-08-02 03:09:25 +12:00
// do not register extra systems or components here -- they will get cleared when the client is
// disconnected. just use reflection.
2022-06-19 20:22:28 -07:00
IoCManager . Register < IParallaxManager , DummyParallaxManager > ( true ) ;
IoCManager . Resolve < ILogManager > ( ) . GetSawmill ( "loc" ) . Level = LogLevel . Error ;
2023-04-24 18:34:12 +12:00
IoCManager . Resolve < IConfigurationManager > ( )
. OnValueChanged ( RTCVars . FailureLogLevel , value = > logHandler . FailureLevel = value , true ) ;
2022-06-19 20:22:28 -07:00
}
} ) ;
} ;
2023-08-06 14:30:28 +12:00
SetDefaultCVars ( options ) ;
2022-06-19 20:22:28 -07:00
var client = new RobustIntegrationTest . ClientIntegrationInstance ( options ) ;
await client . WaitIdleAsync ( ) ;
2023-08-06 14:30:28 +12:00
await SetupCVars ( client , poolSettings ) ;
2023-03-11 05:09:41 +01:00
return ( client , logHandler ) ;
2022-06-19 20:22:28 -07:00
}
2023-08-06 14:30:28 +12:00
private static async Task SetupCVars ( RobustIntegrationTest . IntegrationInstance instance , PoolSettings settings )
2022-06-19 20:22:28 -07:00
{
2023-08-06 14:30:28 +12:00
var cfg = instance . ResolveDependency < IConfigurationManager > ( ) ;
await instance . WaitPost ( ( ) = >
2022-06-19 20:22:28 -07:00
{
2023-08-06 14:30:28 +12:00
if ( cfg . IsCVarRegistered ( CCVars . GameDummyTicker . Name ) )
cfg . SetCVar ( CCVars . GameDummyTicker , settings . UseDummyTicker ) ;
2022-06-19 20:22:28 -07:00
2023-08-06 14:30:28 +12:00
if ( cfg . IsCVarRegistered ( CCVars . GameLobbyEnabled . Name ) )
cfg . SetCVar ( CCVars . GameLobbyEnabled , settings . InLobby ) ;
2022-06-19 20:22:28 -07:00
2023-08-06 14:30:28 +12:00
if ( cfg . IsCVarRegistered ( CVars . NetInterp . Name ) )
cfg . SetCVar ( CVars . NetInterp , settings . DisableInterpolate ) ;
2022-06-19 20:22:28 -07:00
2023-08-06 14:30:28 +12:00
if ( cfg . IsCVarRegistered ( CCVars . GameMap . Name ) )
cfg . SetCVar ( CCVars . GameMap , settings . Map ) ;
2022-06-19 20:22:28 -07:00
2023-08-06 14:30:28 +12:00
if ( cfg . IsCVarRegistered ( CCVars . AdminLogsEnabled . Name ) )
cfg . SetCVar ( CCVars . AdminLogsEnabled , settings . AdminLogsEnabled ) ;
2022-12-20 23:25:03 +01:00
2023-08-06 14:30:28 +12:00
if ( cfg . IsCVarRegistered ( CVars . NetInterp . Name ) )
cfg . SetCVar ( CVars . NetInterp , ! settings . DisableInterpolate ) ;
} ) ;
}
2023-03-06 20:38:07 +01:00
2023-08-06 14:30:28 +12:00
private static void SetDefaultCVars ( RobustIntegrationTest . IntegrationOptions options )
{
foreach ( var ( cvar , value ) in TestCvars )
{
options . CVarOverrides [ cvar ] = value ;
}
2022-06-19 20:22:28 -07:00
}
2022-08-15 20:32:15 -07:00
/// <summary>
/// Gets a <see cref="PairTracker"/>, which can be used to get access to a server, and client <see cref="Pair"/>
/// </summary>
/// <param name="poolSettings">See <see cref="PoolSettings"/></param>
/// <returns></returns>
2023-08-05 16:16:48 +12:00
public static async Task < PairTracker > GetServerClient ( PoolSettings ? poolSettings = null )
2023-07-05 21:54:25 -07:00
{
return await GetServerClientPair ( poolSettings ? ? new PoolSettings ( ) ) ;
}
2022-06-19 20:22:28 -07:00
2023-03-11 05:09:41 +01:00
private static string GetDefaultTestName ( TestContext testContext )
2022-08-27 20:18:42 -07:00
{
2023-03-11 05:09:41 +01:00
return testContext . Test . FullName . Replace ( "Content.IntegrationTests.Tests." , "" ) ;
2022-08-27 20:18:42 -07:00
}
2022-08-27 19:55:31 -07:00
private static async Task < PairTracker > GetServerClientPair ( PoolSettings poolSettings )
2022-06-19 20:22:28 -07:00
{
2023-08-05 16:16:48 +12:00
if ( ! _initialized )
throw new InvalidOperationException ( $"Pool manager has not been initialized" ) ;
2023-03-11 05:09:41 +01:00
// Trust issues with the AsyncLocal that backs this.
var testContext = TestContext . CurrentContext ;
var testOut = TestContext . Out ;
2022-08-27 19:55:31 -07:00
DieIfPoolFailure ( ) ;
2023-03-11 05:09:41 +01:00
var currentTestName = poolSettings . TestName ? ? GetDefaultTestName ( testContext ) ;
2022-08-27 19:55:31 -07:00
var poolRetrieveTimeWatch = new Stopwatch ( ) ;
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync ( $"{nameof(GetServerClientPair)}: Called by test {currentTestName}" ) ;
2023-08-05 16:16:48 +12:00
Pair ? pair = null ;
2022-07-01 23:39:16 -07:00
try
2022-06-19 20:22:28 -07:00
{
2022-07-01 23:39:16 -07:00
poolRetrieveTimeWatch . Start ( ) ;
if ( poolSettings . MustBeNew )
2022-06-19 20:22:28 -07:00
{
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync (
2022-08-27 19:55:31 -07:00
$"{nameof(GetServerClientPair)}: Creating pair, because settings of pool settings" ) ;
2023-03-11 05:09:41 +01:00
pair = await CreateServerClientPair ( poolSettings , testOut ) ;
2023-08-06 14:30:28 +12:00
// Newly created pairs should always be in a valid state.
await RunTicksSync ( pair , 5 ) ;
await SyncTicks ( pair , targetDelta : 1 ) ;
ValidateFastRecycle ( pair , poolSettings ) ;
2022-07-01 23:39:16 -07:00
}
else
{
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync ( $"{nameof(GetServerClientPair)}: Looking in pool for a suitable pair" ) ;
2022-07-01 23:39:16 -07:00
pair = GrabOptimalPair ( poolSettings ) ;
if ( pair ! = null )
2022-06-19 20:22:28 -07:00
{
2023-03-11 05:09:41 +01:00
pair . ActivateContext ( testOut ) ;
await testOut . WriteLineAsync ( $"{nameof(GetServerClientPair)}: Suitable pair found" ) ;
2022-07-01 23:39:16 -07:00
var canSkip = pair . Settings . CanFastRecycle ( poolSettings ) ;
2022-08-27 19:55:31 -07:00
if ( canSkip )
2022-07-01 23:39:16 -07:00
{
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync ( $"{nameof(GetServerClientPair)}: Cleanup not needed, Skipping cleanup of pair" ) ;
2023-08-06 14:30:28 +12:00
await SetupCVars ( pair . Client , poolSettings ) ;
await SetupCVars ( pair . Server , poolSettings ) ;
await RunTicksSync ( pair , 1 ) ;
2022-07-01 23:39:16 -07:00
}
else
{
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync ( $"{nameof(GetServerClientPair)}: Cleaning existing pair" ) ;
await CleanPooledPair ( poolSettings , pair , testOut ) ;
2022-07-01 23:39:16 -07:00
}
2023-08-02 03:09:25 +12:00
2023-08-06 14:30:28 +12:00
await RunTicksSync ( pair , 5 ) ;
await SyncTicks ( pair , targetDelta : 1 ) ;
ValidateFastRecycle ( pair , poolSettings ) ;
2022-06-19 20:22:28 -07:00
}
else
{
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync ( $"{nameof(GetServerClientPair)}: Creating a new pair, no suitable pair found in pool" ) ;
pair = await CreateServerClientPair ( poolSettings , testOut ) ;
2022-06-19 20:22:28 -07:00
}
}
2022-07-01 23:39:16 -07:00
2022-06-19 20:22:28 -07:00
}
2022-07-01 23:39:16 -07:00
finally
2022-06-19 20:22:28 -07:00
{
2022-08-27 19:55:31 -07:00
if ( pair ! = null & & pair . TestHistory . Count > 1 )
2022-07-01 23:39:16 -07:00
{
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync ( $"{nameof(GetServerClientPair)}: Pair {pair.PairId} Test History Start" ) ;
2023-07-05 21:54:25 -07:00
for ( var i = 0 ; i < pair . TestHistory . Count ; i + + )
2022-08-27 19:55:31 -07:00
{
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync ( $"- Pair {pair.PairId} Test #{i}: {pair.TestHistory[i]}" ) ;
2022-08-27 19:55:31 -07:00
}
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync ( $"{nameof(GetServerClientPair)}: Pair {pair.PairId} Test History End" ) ;
2022-07-01 23:39:16 -07:00
}
}
2022-08-27 19:55:31 -07:00
var poolRetrieveTime = poolRetrieveTimeWatch . Elapsed ;
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync (
2022-08-27 19:55:31 -07:00
$"{nameof(GetServerClientPair)}: Retrieving pair {pair.PairId} from pool took {poolRetrieveTime.TotalMilliseconds} ms" ) ;
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync (
2022-08-27 19:55:31 -07:00
$"{nameof(GetServerClientPair)}: Returning pair {pair.PairId}" ) ;
pair . Settings = poolSettings ;
pair . TestHistory . Add ( currentTestName ) ;
var usageWatch = new Stopwatch ( ) ;
usageWatch . Start ( ) ;
2023-03-11 05:09:41 +01:00
return new PairTracker ( testOut )
2022-08-27 19:55:31 -07:00
{
Pair = pair ,
UsageWatch = usageWatch
} ;
2022-06-19 20:22:28 -07:00
}
2023-08-06 14:30:28 +12:00
private static void ValidateFastRecycle ( Pair pair , PoolSettings settings )
2023-08-02 03:09:25 +12:00
{
2023-08-06 14:30:28 +12:00
var cfg = pair . Server . ResolveDependency < IConfigurationManager > ( ) ;
Assert . That ( cfg . GetCVar ( CCVars . AdminLogsEnabled ) , Is . EqualTo ( settings . AdminLogsEnabled ) ) ;
Assert . That ( cfg . GetCVar ( CCVars . GameLobbyEnabled ) , Is . EqualTo ( settings . InLobby ) ) ;
Assert . That ( cfg . GetCVar ( CCVars . GameDummyTicker ) , Is . EqualTo ( settings . UseDummyTicker ) ) ;
var ticker = pair . Server . ResolveDependency < EntityManager > ( ) . System < GameTicker > ( ) ;
Assert . That ( ticker . DummyTicker , Is . EqualTo ( settings . UseDummyTicker ) ) ;
var expectPreRound = settings . InLobby | settings . DummyTicker ;
var expectedLevel = expectPreRound ? GameRunLevel . PreRoundLobby : GameRunLevel . InRound ;
Assert . That ( ticker . RunLevel , Is . EqualTo ( expectedLevel ) ) ;
2023-08-02 03:09:25 +12:00
var baseClient = pair . Client . ResolveDependency < IBaseClient > ( ) ;
var netMan = pair . Client . ResolveDependency < INetManager > ( ) ;
2023-08-06 14:30:28 +12:00
Assert . That ( netMan . IsConnected , Is . Not . EqualTo ( ! settings . ShouldBeConnected ) ) ;
2023-08-02 03:09:25 +12:00
2023-08-06 14:30:28 +12:00
if ( ! settings . ShouldBeConnected )
2023-08-02 03:09:25 +12:00
return ;
Assert . That ( baseClient . RunLevel , Is . EqualTo ( ClientRunLevel . InGame ) ) ;
var cPlayer = pair . Client . ResolveDependency < Robust . Client . Player . IPlayerManager > ( ) ;
var sPlayer = pair . Server . ResolveDependency < IPlayerManager > ( ) ;
Assert . That ( sPlayer . Sessions . Count ( ) , Is . EqualTo ( 1 ) ) ;
2023-08-06 14:30:28 +12:00
Assert . That ( cPlayer . LocalPlayer ? . Session . UserId , Is . EqualTo ( sPlayer . Sessions . Single ( ) . UserId ) ) ;
2023-08-02 03:09:25 +12:00
2023-08-06 14:30:28 +12:00
if ( ticker . DummyTicker )
return ;
2023-08-02 03:09:25 +12:00
var status = ticker . PlayerGameStatuses [ sPlayer . Sessions . Single ( ) . UserId ] ;
2023-08-06 14:30:28 +12:00
var expected = settings . InLobby
2023-08-02 03:09:25 +12:00
? PlayerGameStatus . NotReadyToPlay
: PlayerGameStatus . JoinedGame ;
Assert . That ( status , Is . EqualTo ( expected ) ) ;
}
2023-08-05 16:16:48 +12:00
private static Pair ? GrabOptimalPair ( PoolSettings poolSettings )
2022-06-19 20:22:28 -07:00
{
lock ( PairLock )
{
2023-08-05 16:16:48 +12:00
Pair ? fallback = null ;
2022-09-15 20:17:02 -07:00
foreach ( var pair in Pairs . Keys )
2022-06-19 20:22:28 -07:00
{
2022-09-15 20:17:02 -07:00
if ( Pairs [ pair ] )
continue ;
if ( ! pair . Settings . CanFastRecycle ( poolSettings ) )
{
fallback = pair ;
continue ;
}
Pairs [ pair ] = true ;
2022-06-19 20:22:28 -07:00
return pair ;
}
2022-09-15 20:17:02 -07:00
if ( fallback ! = null )
{
Pairs [ fallback ! ] = true ;
}
return fallback ;
2022-06-19 20:22:28 -07:00
}
}
/// <summary>
2022-08-15 20:32:15 -07:00
/// Used by PairTracker after checking the server/client pair, Don't use this.
2022-06-19 20:22:28 -07:00
/// </summary>
/// <param name="pair"></param>
public static void NoCheckReturn ( Pair pair )
{
lock ( PairLock )
{
2022-09-15 20:17:02 -07:00
if ( pair . Dead )
{
Pairs . Remove ( pair ) ;
}
else
{
Pairs [ pair ] = false ;
}
2022-06-19 20:22:28 -07:00
}
}
2023-08-06 14:30:28 +12:00
private static async Task CleanPooledPair ( PoolSettings settings , Pair pair , TextWriter testOut )
2022-06-19 20:22:28 -07:00
{
2023-08-06 14:30:28 +12:00
pair . Settings = default ! ;
2022-06-19 20:22:28 -07:00
var methodWatch = new Stopwatch ( ) ;
methodWatch . Start ( ) ;
2023-08-06 14:30:28 +12:00
await testOut . WriteLineAsync ( $"Recycling..." ) ;
2022-06-19 20:22:28 -07:00
var configManager = pair . Server . ResolveDependency < IConfigurationManager > ( ) ;
2023-07-05 21:54:25 -07:00
var entityManager = pair . Server . ResolveDependency < IEntityManager > ( ) ;
var gameTicker = entityManager . System < GameTicker > ( ) ;
2023-08-06 14:30:28 +12:00
var cNetMgr = pair . Client . ResolveDependency < IClientNetManager > ( ) ;
2023-08-02 03:09:25 +12:00
2023-08-06 14:30:28 +12:00
await RunTicksSync ( pair , 1 ) ;
2023-08-02 03:09:25 +12:00
2023-08-06 14:30:28 +12:00
// Disconnect the client if they are connected.
if ( cNetMgr . IsConnected )
2022-06-19 20:22:28 -07:00
{
2023-08-06 14:30:28 +12:00
await testOut . WriteLineAsync ( $"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Disconnecting client." ) ;
await pair . Client . WaitPost ( ( ) = > cNetMgr . ClientDisconnect ( "Test pooling cleanup disconnect" ) ) ;
await RunTicksSync ( pair , 1 ) ;
2022-06-19 20:22:28 -07:00
}
2023-08-06 14:30:28 +12:00
Assert . That ( cNetMgr . IsConnected , Is . False ) ;
2022-06-19 20:22:28 -07:00
2023-08-06 14:30:28 +12:00
// Move to pre-round lobby. Required to toggle dummy ticker on and off
if ( gameTicker . RunLevel ! = GameRunLevel . PreRoundLobby )
2022-06-19 20:22:28 -07:00
{
2023-08-06 14:30:28 +12:00
await testOut . WriteLineAsync ( $"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Restarting server." ) ;
Assert . That ( gameTicker . DummyTicker , Is . False ) ;
configManager . SetCVar ( CCVars . GameLobbyEnabled , true ) ;
await pair . Server . WaitPost ( ( ) = > gameTicker . RestartRound ( ) ) ;
await RunTicksSync ( pair , 1 ) ;
}
2022-07-01 23:39:16 -07:00
2023-08-06 14:30:28 +12:00
//Apply Cvars
await testOut . WriteLineAsync ( $"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Setting CVar " ) ;
await SetupCVars ( pair . Client , settings ) ;
await SetupCVars ( pair . Server , settings ) ;
await RunTicksSync ( pair , 1 ) ;
2022-06-19 20:22:28 -07:00
2023-08-06 14:30:28 +12:00
// Restart server.
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync ( $"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Restarting server again" ) ;
2023-08-02 03:09:25 +12:00
await pair . Server . WaitPost ( ( ) = > gameTicker . RestartRound ( ) ) ;
2023-08-06 14:30:28 +12:00
await RunTicksSync ( pair , 1 ) ;
2022-06-19 20:22:28 -07:00
2023-08-06 14:30:28 +12:00
// Connect client
if ( settings . ShouldBeConnected )
2022-06-19 20:22:28 -07:00
{
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync ( $"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Connecting client" ) ;
2022-06-19 20:22:28 -07:00
pair . Client . SetConnectTarget ( pair . Server ) ;
2023-08-06 14:30:28 +12:00
await pair . Client . WaitPost ( ( ) = > cNetMgr . ClientConnect ( null ! , 0 , null ! ) ) ;
2022-06-19 20:22:28 -07:00
}
2023-08-06 14:30:28 +12:00
await testOut . WriteLineAsync ( $"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Idling" ) ;
2022-06-19 20:22:28 -07:00
await ReallyBeIdle ( pair ) ;
2023-03-11 05:09:41 +01:00
await testOut . WriteLineAsync ( $"Recycling: {methodWatch.Elapsed.TotalMilliseconds} ms: Done recycling" ) ;
2022-06-19 20:22:28 -07:00
}
2022-08-27 19:55:31 -07:00
private static void DieIfPoolFailure ( )
2022-06-19 20:22:28 -07:00
{
2023-07-05 21:54:25 -07:00
if ( _poolFailureReason ! = null )
2022-08-24 20:55:30 -07:00
{
2023-07-05 21:54:25 -07:00
// If the _poolFailureReason is not null, we can assume at least one test failed.
2022-09-15 20:17:02 -07:00
// So we say inconclusive so we don't add more failed tests to search through.
2022-08-24 20:55:30 -07:00
Assert . Inconclusive ( @ "
In a different test , the pool manager had an exception when trying to create a server / client pair .
Instead of risking that the pool manager will fail at creating a server / client pairs for every single test ,
2023-07-05 21:54:25 -07:00
we are just going to end this here to save a lot of time . This is the exception that started this : \ n { 0 } ", _poolFailureReason);
2022-08-24 20:55:30 -07:00
}
2022-09-15 20:17:02 -07:00
2023-07-05 21:54:25 -07:00
if ( _dead )
2022-09-15 20:17:02 -07:00
{
// If Pairs is null, we ran out of time, we can't assume a test failed.
// So we are going to tell it all future tests are a failure.
Assert . Fail ( "The pool was shut down" ) ;
}
2022-08-27 19:55:31 -07:00
}
2023-08-05 16:16:48 +12:00
2023-03-11 05:09:41 +01:00
private static async Task < Pair > CreateServerClientPair ( PoolSettings poolSettings , TextWriter testOut )
2022-08-27 19:55:31 -07:00
{
Pair pair ;
2022-08-24 20:55:30 -07:00
try
{
2023-03-11 05:09:41 +01:00
var ( client , clientLog ) = await GenerateClient ( poolSettings , testOut ) ;
var ( server , serverLog ) = await GenerateServer ( poolSettings , testOut ) ;
pair = new Pair
{
Server = server ,
ServerLogHandler = serverLog ,
Client = client ,
ClientLogHandler = clientLog ,
2023-07-05 21:54:25 -07:00
PairId = Interlocked . Increment ( ref _pairId )
2023-03-11 05:09:41 +01:00
} ;
2023-08-05 16:16:48 +12:00
if ( ! poolSettings . NoLoadTestPrototypes )
await pair . LoadPrototypes ( _testPrototypes ! ) ;
2022-08-24 20:55:30 -07:00
}
catch ( Exception ex )
{
2023-07-05 21:54:25 -07:00
_poolFailureReason = ex ;
2022-08-24 20:55:30 -07:00
throw ;
}
2022-06-19 20:22:28 -07:00
2023-08-06 14:30:28 +12:00
if ( ! poolSettings . UseDummyTicker )
{
var gameTicker = pair . Server . ResolveDependency < IEntityManager > ( ) . System < GameTicker > ( ) ;
await pair . Server . WaitPost ( ( ) = > gameTicker . RestartRound ( ) ) ;
}
if ( poolSettings . ShouldBeConnected )
2022-06-19 20:22:28 -07:00
{
pair . Client . SetConnectTarget ( pair . Server ) ;
await pair . Client . WaitPost ( ( ) = >
{
var netMgr = IoCManager . Resolve < IClientNetManager > ( ) ;
if ( ! netMgr . IsConnected )
{
netMgr . ClientConnect ( null ! , 0 , null ! ) ;
}
} ) ;
await ReallyBeIdle ( pair , 10 ) ;
2022-08-24 20:55:30 -07:00
await pair . Client . WaitRunTicks ( 1 ) ;
2022-06-19 20:22:28 -07:00
}
return pair ;
}
2022-08-15 20:32:15 -07:00
/// <summary>
/// Creates a map, a grid, and a tile, and gives back references to them.
/// </summary>
/// <param name="pairTracker">A pairTracker</param>
/// <returns>A TestMapData</returns>
2022-06-21 07:44:19 -07:00
public static async Task < TestMapData > CreateTestMap ( PairTracker pairTracker )
2022-06-19 20:22:28 -07:00
{
2022-06-21 07:44:19 -07:00
var server = pairTracker . Pair . Server ;
2023-06-01 00:09:14 +10:00
await server . WaitIdleAsync ( ) ;
2022-06-21 07:44:19 -07:00
var settings = pairTracker . Pair . Settings ;
2023-06-01 00:09:14 +10:00
var mapManager = server . ResolveDependency < IMapManager > ( ) ;
var tileDefinitionManager = server . ResolveDependency < ITileDefinitionManager > ( ) ;
2022-07-01 23:39:16 -07:00
var mapData = new TestMapData ( ) ;
2022-06-21 07:44:19 -07:00
await server . WaitPost ( ( ) = >
{
mapData . MapId = mapManager . CreateMap ( ) ;
2023-04-15 07:41:25 +12:00
mapData . MapUid = mapManager . GetMapEntityId ( mapData . MapId ) ;
2022-06-21 07:44:19 -07:00
mapData . MapGrid = mapManager . CreateGrid ( mapData . MapId ) ;
2023-07-05 21:54:25 -07:00
mapData . GridUid = mapData . MapGrid . Owner ; // Fixing this requires an engine PR.
2023-04-17 18:07:03 +12:00
mapData . GridCoords = new EntityCoordinates ( mapData . GridUid , 0 , 0 ) ;
2022-08-10 17:05:40 +10:00
var plating = tileDefinitionManager [ "Plating" ] ;
2022-06-21 07:44:19 -07:00
var platingTile = new Tile ( plating . TileId ) ;
mapData . MapGrid . SetTile ( mapData . GridCoords , platingTile ) ;
mapData . MapCoords = new MapCoordinates ( 0 , 0 , mapData . MapId ) ;
mapData . Tile = mapData . MapGrid . GetAllTiles ( ) . First ( ) ;
} ) ;
2023-08-06 14:30:28 +12:00
if ( settings . ShouldBeConnected )
2022-06-19 20:22:28 -07:00
{
2022-06-21 07:44:19 -07:00
await RunTicksSync ( pairTracker . Pair , 10 ) ;
2022-06-19 20:22:28 -07:00
}
2022-06-21 07:44:19 -07:00
return mapData ;
2022-06-19 20:22:28 -07:00
}
2022-08-15 20:32:15 -07:00
/// <summary>
/// Runs a server/client pair in sync
/// </summary>
/// <param name="pair">A server/client pair</param>
/// <param name="ticks">How many ticks to run them for</param>
2022-06-19 20:22:28 -07:00
public static async Task RunTicksSync ( Pair pair , int ticks )
{
for ( var i = 0 ; i < ticks ; i + + )
{
await pair . Server . WaitRunTicks ( 1 ) ;
await pair . Client . WaitRunTicks ( 1 ) ;
}
}
2022-08-15 20:32:15 -07:00
/// <summary>
/// Runs the server/client in sync, but also ensures they are both idle each tick.
/// </summary>
/// <param name="pair">The server/client pair</param>
/// <param name="runTicks">How many ticks to run</param>
2022-06-19 20:22:28 -07:00
public static async Task ReallyBeIdle ( Pair pair , int runTicks = 25 )
{
2023-07-05 21:54:25 -07:00
for ( var i = 0 ; i < runTicks ; i + + )
2022-06-19 20:22:28 -07:00
{
await pair . Client . WaitRunTicks ( 1 ) ;
await pair . Server . WaitRunTicks ( 1 ) ;
2023-07-05 21:54:25 -07:00
for ( var idleCycles = 0 ; idleCycles < 4 ; idleCycles + + )
2022-06-19 20:22:28 -07:00
{
await pair . Client . WaitIdleAsync ( ) ;
await pair . Server . WaitIdleAsync ( ) ;
}
}
}
2023-08-02 03:09:25 +12:00
/// <summary>
/// Run the server/clients until the ticks are synchronized.
/// By default the client will be one tick ahead of the server.
/// </summary>
public static async Task SyncTicks ( Pair pair , int targetDelta = 1 )
{
var sTiming = pair . Server . ResolveDependency < IGameTiming > ( ) ;
var cTiming = pair . Client . ResolveDependency < IGameTiming > ( ) ;
var sTick = ( int ) sTiming . CurTick . Value ;
var cTick = ( int ) cTiming . CurTick . Value ;
var delta = cTick - sTick ;
if ( delta = = targetDelta )
return ;
if ( delta > targetDelta )
await pair . Server . WaitRunTicks ( delta - targetDelta ) ;
else
await pair . Client . WaitRunTicks ( targetDelta - delta ) ;
sTick = ( int ) sTiming . CurTick . Value ;
cTick = ( int ) cTiming . CurTick . Value ;
delta = cTick - sTick ;
Assert . That ( delta , Is . EqualTo ( targetDelta ) ) ;
}
2022-08-15 20:32:15 -07:00
/// <summary>
/// Runs a server, or a client until a condition is true
/// </summary>
/// <param name="instance">The server or client</param>
/// <param name="func">The condition to check</param>
/// <param name="maxTicks">How many ticks to try before giving up</param>
/// <param name="tickStep">How many ticks to wait between checks</param>
public static async Task WaitUntil ( RobustIntegrationTest . IntegrationInstance instance , Func < bool > func ,
int maxTicks = 600 ,
int tickStep = 1 )
{
await WaitUntil ( instance , async ( ) = > await Task . FromResult ( func ( ) ) , maxTicks , tickStep ) ;
}
/// <summary>
/// Runs a server, or a client until a condition is true
/// </summary>
/// <param name="instance">The server or client</param>
/// <param name="func">The async condition to check</param>
/// <param name="maxTicks">How many ticks to try before giving up</param>
/// <param name="tickStep">How many ticks to wait between checks</param>
2022-06-19 20:22:28 -07:00
public static async Task WaitUntil ( RobustIntegrationTest . IntegrationInstance instance , Func < Task < bool > > func ,
int maxTicks = 600 ,
int tickStep = 1 )
{
var ticksAwaited = 0 ;
bool passed ;
await instance . WaitIdleAsync ( ) ;
while ( ! ( passed = await func ( ) ) & & ticksAwaited < maxTicks )
{
var ticksToRun = tickStep ;
if ( ticksAwaited + tickStep > maxTicks )
{
ticksToRun = maxTicks - ticksAwaited ;
}
await instance . WaitRunTicks ( ticksToRun ) ;
ticksAwaited + = ticksToRun ;
}
if ( ! passed )
{
Assert . Fail ( $"Condition did not pass after {maxTicks} ticks.\n" +
$"Tests ran ({instance.TestsRan.Count}):\n" +
$"{string.Join('\n', instance.TestsRan)}" ) ;
}
Assert . That ( passed ) ;
}
2023-04-25 12:30:35 +12:00
/// <summary>
/// Helper method that retrieves all entity prototypes that have some component.
/// </summary>
public static List < EntityPrototype > GetEntityPrototypes < T > ( RobustIntegrationTest . IntegrationInstance instance ) where T : Component
{
var protoMan = instance . ResolveDependency < IPrototypeManager > ( ) ;
var compFact = instance . ResolveDependency < IComponentFactory > ( ) ;
var id = compFact . GetComponentName ( typeof ( T ) ) ;
var list = new List < EntityPrototype > ( ) ;
foreach ( var ent in protoMan . EnumeratePrototypes < EntityPrototype > ( ) )
{
if ( ent . Components . ContainsKey ( id ) )
list . Add ( ent ) ;
}
return list ;
}
2023-08-05 16:16:48 +12:00
/// <summary>
/// Initialize the pool manager.
/// </summary>
/// <param name="assembly">Assembly to search for to discover extra test prototypes.</param>
public static void Startup ( Assembly ? assembly )
{
if ( _initialized )
throw new InvalidOperationException ( "Already initialized" ) ;
_initialized = true ;
DiscoverTestPrototypes ( assembly ) ;
}
2022-06-19 20:22:28 -07:00
}
2022-08-15 20:32:15 -07:00
/// <summary>
/// Settings for the pooled server, and client pair.
/// Some options are for changing the pair, and others are
/// so the pool can properly clean up what you borrowed.
/// </summary>
2022-06-19 20:22:28 -07:00
public sealed class PoolSettings
{
2022-08-15 20:32:15 -07:00
/// <summary>
/// If the returned pair must not be reused
/// </summary>
2023-08-05 16:16:48 +12:00
public bool MustNotBeReused = > Destructive | | NoLoadContent | | NoLoadTestPrototypes ;
2022-08-15 20:32:15 -07:00
/// <summary>
/// If the given pair must be brand new
/// </summary>
2023-08-05 16:16:48 +12:00
public bool MustBeNew = > Fresh | | NoLoadContent | | NoLoadTestPrototypes ;
2022-06-19 20:22:28 -07:00
/// <summary>
2022-08-15 20:32:15 -07:00
/// Set to true if the test will ruin the server/client pair.
2022-06-19 20:22:28 -07:00
/// </summary>
public bool Destructive { get ; init ; }
/// <summary>
2022-08-15 20:32:15 -07:00
/// Set to true if the given server/client pair should be created fresh.
2022-06-19 20:22:28 -07:00
/// </summary>
public bool Fresh { get ; init ; }
/// <summary>
2023-08-06 14:30:28 +12:00
/// Set to true if the given server should be using a dummy ticker. Ignored if <see cref="InLobby"/> is true.
/// </summary>
public bool DummyTicker { get ; init ; } = true ;
public bool UseDummyTicker = > ! InLobby & & DummyTicker ;
/// <summary>
/// If true, this enables the creation of admin logs during the test.
2022-06-19 20:22:28 -07:00
/// </summary>
2023-08-06 14:30:28 +12:00
public bool AdminLogsEnabled { get ; init ; }
2022-06-19 20:22:28 -07:00
/// <summary>
2023-08-06 14:30:28 +12:00
/// Set to true if the given server/client pair should be connected from each other.
/// Defaults to disconnected as it makes dirty recycling slightly faster.
/// If <see cref="InLobby"/> is true, this option is ignored.
2022-06-19 20:22:28 -07:00
/// </summary>
2023-08-06 14:30:28 +12:00
public bool Connected { get ; init ; }
public bool ShouldBeConnected = > InLobby | | Connected ;
2022-06-19 20:22:28 -07:00
/// <summary>
2022-08-15 20:32:15 -07:00
/// Set to true if the given server/client pair should be in the lobby.
2023-08-02 03:09:25 +12:00
/// If the pair is not in the lobby at the end of the test, this test must be marked as dirty.
2022-06-19 20:22:28 -07:00
/// </summary>
2023-08-06 14:30:28 +12:00
/// <remarks>
/// If this is enabled, the value of <see cref="DummyTicker"/> is ignored.
/// </remarks>
2022-06-19 20:22:28 -07:00
public bool InLobby { get ; init ; }
/// <summary>
2022-08-15 20:32:15 -07:00
/// Set this to true to skip loading the content files.
/// Note: This setting won't work with a client.
2022-06-19 20:22:28 -07:00
/// </summary>
public bool NoLoadContent { get ; init ; }
/// <summary>
2023-08-05 16:16:48 +12:00
/// This will return a server-client pair that has not loaded test prototypes.
/// Try avoiding this whenever possible, as this will always create & destroy a new pair.
/// Use <see cref="Pair.IsTestPrototype(EntityPrototype)"/> if you need to exclude test prototypees.
2022-06-19 20:22:28 -07:00
/// </summary>
2023-08-05 16:16:48 +12:00
public bool NoLoadTestPrototypes { get ; init ; }
2022-06-19 20:22:28 -07:00
/// <summary>
2022-08-15 20:32:15 -07:00
/// Set this to true to disable the NetInterp CVar on the given server/client pair
2022-06-19 20:22:28 -07:00
/// </summary>
public bool DisableInterpolate { get ; init ; }
/// <summary>
2022-08-15 20:32:15 -07:00
/// Set this to true to always clean up the server/client pair before giving it to another borrower
2022-06-19 20:22:28 -07:00
/// </summary>
public bool Dirty { get ; init ; }
/// <summary>
2022-08-15 20:32:15 -07:00
/// Set this to the path of a map to have the given server/client pair load the map.
2022-06-19 20:22:28 -07:00
/// </summary>
2023-08-02 03:09:25 +12:00
public string Map { get ; init ; } = PoolManager . TestMap ;
2022-06-19 20:22:28 -07:00
2022-08-27 19:55:31 -07:00
/// <summary>
/// Overrides the test name detection, and uses this in the test history instead
/// </summary>
2023-08-05 16:16:48 +12:00
public string? TestName { get ; set ; }
2022-08-27 19:55:31 -07:00
2022-06-19 20:22:28 -07:00
/// <summary>
2022-08-15 20:32:15 -07:00
/// Tries to guess if we can skip recycling the server/client pair.
2022-06-19 20:22:28 -07:00
/// </summary>
/// <param name="nextSettings">The next set of settings the old pair will be set to</param>
2022-08-15 20:32:15 -07:00
/// <returns>If we can skip cleaning it up</returns>
2022-06-19 20:22:28 -07:00
public bool CanFastRecycle ( PoolSettings nextSettings )
{
2023-08-02 03:09:25 +12:00
if ( MustNotBeReused )
throw new InvalidOperationException ( "Attempting to recycle a non-reusable test." ) ;
if ( nextSettings . MustBeNew )
throw new InvalidOperationException ( "Attempting to recycle a test while requesting a fresh test." ) ;
if ( Dirty )
return false ;
// Check that certain settings match.
2023-08-06 14:30:28 +12:00
return ! ShouldBeConnected = = ! nextSettings . ShouldBeConnected
& & UseDummyTicker = = nextSettings . UseDummyTicker
2023-08-02 03:09:25 +12:00
& & Map = = nextSettings . Map
2023-08-05 16:16:48 +12:00
& & InLobby = = nextSettings . InLobby ;
}
2022-06-19 20:22:28 -07:00
}
2022-08-15 20:32:15 -07:00
/// <summary>
/// Holds a reference to things commonly needed when testing on a map
/// </summary>
2022-06-21 07:44:19 -07:00
public sealed class TestMapData
{
2023-04-15 07:41:25 +12:00
public EntityUid MapUid { get ; set ; }
2023-04-17 18:07:03 +12:00
public EntityUid GridUid { get ; set ; }
2022-06-21 07:44:19 -07:00
public MapId MapId { get ; set ; }
2023-08-05 16:16:48 +12:00
public MapGridComponent MapGrid { get ; set ; } = default ! ;
2022-06-21 07:44:19 -07:00
public EntityCoordinates GridCoords { get ; set ; }
public MapCoordinates MapCoords { get ; set ; }
public TileRef Tile { get ; set ; }
}
2022-08-15 20:32:15 -07:00
/// <summary>
/// A server/client pair
/// </summary>
2022-06-19 20:22:28 -07:00
public sealed class Pair
{
2022-09-15 20:17:02 -07:00
public bool Dead { get ; private set ; }
2022-06-19 20:22:28 -07:00
public int PairId { get ; init ; }
public List < string > TestHistory { get ; set ; } = new ( ) ;
2023-08-05 16:16:48 +12:00
public PoolSettings Settings { get ; set ; } = default ! ;
public RobustIntegrationTest . ServerIntegrationInstance Server { get ; init ; } = default ! ;
public RobustIntegrationTest . ClientIntegrationInstance Client { get ; init ; } = default ! ;
2022-09-15 20:17:02 -07:00
2023-08-05 16:16:48 +12:00
public PoolTestLogHandler ServerLogHandler { get ; init ; } = default ! ;
public PoolTestLogHandler ClientLogHandler { get ; init ; } = default ! ;
private Dictionary < Type , HashSet < string > > _loadedPrototypes = new ( ) ;
private HashSet < string > _loadedEntityPrototypes = new ( ) ;
2023-03-11 05:09:41 +01:00
2022-09-15 20:17:02 -07:00
public void Kill ( )
{
Dead = true ;
Server . Dispose ( ) ;
Client . Dispose ( ) ;
}
2023-03-11 05:09:41 +01:00
public void ClearContext ( )
{
ServerLogHandler . ClearContext ( ) ;
ClientLogHandler . ClearContext ( ) ;
}
public void ActivateContext ( TextWriter testOut )
{
ServerLogHandler . ActivateContext ( testOut ) ;
ClientLogHandler . ActivateContext ( testOut ) ;
}
2023-08-05 16:16:48 +12:00
public async Task LoadPrototypes ( List < string > prototypes )
{
await LoadPrototypes ( Server , prototypes ) ;
await LoadPrototypes ( Client , prototypes ) ;
}
private async Task LoadPrototypes ( RobustIntegrationTest . IntegrationInstance instance , List < string > prototypes )
{
var changed = new Dictionary < Type , HashSet < string > > ( ) ;
var protoMan = instance . ResolveDependency < IPrototypeManager > ( ) ;
foreach ( var file in prototypes )
{
protoMan . LoadString ( file , changed : changed ) ;
}
await instance . WaitPost ( ( ) = > protoMan . ReloadPrototypes ( changed ) ) ;
foreach ( var ( kind , ids ) in changed )
{
_loadedPrototypes . GetOrNew ( kind ) . UnionWith ( ids ) ;
}
if ( _loadedPrototypes . TryGetValue ( typeof ( EntityPrototype ) , out var entIds ) )
_loadedEntityPrototypes . UnionWith ( entIds ) ;
}
public bool IsTestPrototype ( EntityPrototype proto )
{
return _loadedEntityPrototypes . Contains ( proto . ID ) ;
}
public bool IsTestEntityPrototype ( string id )
{
return _loadedEntityPrototypes . Contains ( id ) ;
}
public bool IsTestPrototype < TPrototype > ( string id ) where TPrototype : IPrototype
{
return IsTestPrototype ( typeof ( TPrototype ) , id ) ;
}
public bool IsTestPrototype < TPrototype > ( TPrototype proto ) where TPrototype : IPrototype
{
return IsTestPrototype ( typeof ( TPrototype ) , proto . ID ) ;
}
public bool IsTestPrototype ( Type kind , string id )
{
return _loadedPrototypes . TryGetValue ( kind , out var ids ) & & ids . Contains ( id ) ;
}
2022-06-19 20:22:28 -07:00
}
2022-08-15 20:32:15 -07:00
/// <summary>
/// Used by the pool to keep track of a borrowed server/client pair.
/// </summary>
2022-06-19 20:22:28 -07:00
public sealed class PairTracker : IAsyncDisposable
{
2023-03-11 05:09:41 +01:00
private readonly TextWriter _testOut ;
2022-06-19 20:22:28 -07:00
private int _disposed ;
2023-08-05 16:16:48 +12:00
public Stopwatch UsageWatch { get ; set ; } = default ! ;
public Pair Pair { get ; init ; } = default ! ;
2023-03-11 05:09:41 +01:00
public PairTracker ( TextWriter testOut )
{
_testOut = testOut ;
}
2022-06-19 20:22:28 -07:00
2023-08-03 15:07:21 +12:00
// Convenience properties.
public RobustIntegrationTest . ServerIntegrationInstance Server = > Pair . Server ;
public RobustIntegrationTest . ClientIntegrationInstance Client = > Pair . Client ;
2022-08-15 20:32:15 -07:00
private async Task OnDirtyDispose ( )
2022-06-19 20:22:28 -07:00
{
var usageTime = UsageWatch . Elapsed ;
2023-03-11 05:09:41 +01:00
await _testOut . WriteLineAsync ( $"{nameof(DisposeAsync)}: Test gave back pair {Pair.PairId} in {usageTime.TotalMilliseconds} ms" ) ;
2022-06-19 20:22:28 -07:00
var dirtyWatch = new Stopwatch ( ) ;
dirtyWatch . Start ( ) ;
2022-09-15 20:17:02 -07:00
Pair . Kill ( ) ;
PoolManager . NoCheckReturn ( Pair ) ;
2022-06-19 20:22:28 -07:00
var disposeTime = dirtyWatch . Elapsed ;
2023-03-11 05:09:41 +01:00
await _testOut . WriteLineAsync ( $"{nameof(DisposeAsync)}: Disposed pair {Pair.PairId} in {disposeTime.TotalMilliseconds} ms" ) ;
2023-08-03 15:07:21 +12:00
// Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
// because someone forgot to clean-return the pair.
Assert . Warn ( "Test was dirty-disposed." ) ;
2022-06-19 20:22:28 -07:00
}
2022-08-15 20:32:15 -07:00
private async Task OnCleanDispose ( )
2022-06-19 20:22:28 -07:00
{
var usageTime = UsageWatch . Elapsed ;
2023-03-11 05:09:41 +01:00
await _testOut . WriteLineAsync ( $"{nameof(CleanReturnAsync)}: Test borrowed pair {Pair.PairId} for {usageTime.TotalMilliseconds} ms" ) ;
2022-06-19 20:22:28 -07:00
var cleanWatch = new Stopwatch ( ) ;
cleanWatch . Start ( ) ;
// Let any last minute failures the test cause happen.
await PoolManager . ReallyBeIdle ( Pair ) ;
if ( ! Pair . Settings . Destructive )
{
if ( Pair . Client . IsAlive = = false )
{
2022-08-27 19:55:31 -07:00
throw new Exception ( $"{nameof(CleanReturnAsync)}: Test killed the client in pair {Pair.PairId}:" , Pair . Client . UnhandledException ) ;
2022-06-19 20:22:28 -07:00
}
if ( Pair . Server . IsAlive = = false )
{
2022-08-27 19:55:31 -07:00
throw new Exception ( $"{nameof(CleanReturnAsync)}: Test killed the server in pair {Pair.PairId}:" , Pair . Server . UnhandledException ) ;
2022-06-19 20:22:28 -07:00
}
}
if ( Pair . Settings . MustNotBeReused )
{
2022-09-15 20:17:02 -07:00
Pair . Kill ( ) ;
PoolManager . NoCheckReturn ( Pair ) ;
2022-08-28 15:13:59 -07:00
await PoolManager . ReallyBeIdle ( Pair ) ;
2022-06-19 20:22:28 -07:00
var returnTime2 = cleanWatch . Elapsed ;
2023-03-11 05:09:41 +01:00
await _testOut . WriteLineAsync ( $"{nameof(CleanReturnAsync)}: Clean disposed in {returnTime2.TotalMilliseconds} ms" ) ;
2022-06-19 20:22:28 -07:00
return ;
}
var sRuntimeLog = Pair . Server . ResolveDependency < IRuntimeLog > ( ) ;
2023-03-11 05:09:41 +01:00
if ( sRuntimeLog . ExceptionCount > 0 )
throw new Exception ( $"{nameof(CleanReturnAsync)}: Server logged exceptions" ) ;
2022-06-19 20:22:28 -07:00
var cRuntimeLog = Pair . Client . ResolveDependency < IRuntimeLog > ( ) ;
2023-03-11 05:09:41 +01:00
if ( cRuntimeLog . ExceptionCount > 0 )
throw new Exception ( $"{nameof(CleanReturnAsync)}: Client logged exceptions" ) ;
Pair . ClearContext ( ) ;
2022-06-19 20:22:28 -07:00
PoolManager . NoCheckReturn ( Pair ) ;
var returnTime = cleanWatch . Elapsed ;
2023-03-11 05:09:41 +01:00
await _testOut . WriteLineAsync ( $"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Pair.PairId} back into the pool" ) ;
2022-06-19 20:22:28 -07:00
}
public async ValueTask CleanReturnAsync ( )
{
var disposed = Interlocked . Exchange ( ref _disposed , 1 ) ;
switch ( disposed )
{
case 0 :
2023-03-11 05:09:41 +01:00
await _testOut . WriteLineAsync ( $"{nameof(CleanReturnAsync)}: Return of pair {Pair.PairId} started" ) ;
2022-06-19 20:22:28 -07:00
break ;
case 1 :
2022-08-27 19:55:31 -07:00
throw new Exception ( $"{nameof(CleanReturnAsync)}: Already clean returned" ) ;
2022-06-19 20:22:28 -07:00
case 2 :
2022-08-27 19:55:31 -07:00
throw new Exception ( $"{nameof(CleanReturnAsync)}: Already dirty disposed" ) ;
2022-06-19 20:22:28 -07:00
default :
2022-08-27 19:55:31 -07:00
throw new Exception ( $"{nameof(CleanReturnAsync)}: Unexpected disposed value" ) ;
2022-06-19 20:22:28 -07:00
}
await OnCleanDispose ( ) ;
}
public async ValueTask DisposeAsync ( )
{
var disposed = Interlocked . Exchange ( ref _disposed , 2 ) ;
switch ( disposed )
{
case 0 :
2023-03-11 05:09:41 +01:00
await _testOut . WriteLineAsync ( $"{nameof(DisposeAsync)}: Dirty return of pair {Pair.PairId} started" ) ;
2022-06-19 20:22:28 -07:00
break ;
case 1 :
2023-03-11 05:09:41 +01:00
await _testOut . WriteLineAsync ( $"{nameof(DisposeAsync)}: Pair {Pair.PairId} was properly clean disposed" ) ;
2022-06-19 20:22:28 -07:00
return ;
case 2 :
2022-08-27 19:55:31 -07:00
throw new Exception ( $"{nameof(DisposeAsync)}: Already dirty disposed pair {Pair.PairId}" ) ;
2022-06-19 20:22:28 -07:00
default :
2022-08-27 19:55:31 -07:00
throw new Exception ( $"{nameof(DisposeAsync)}: Unexpected disposed value" ) ;
2022-06-19 20:22:28 -07:00
}
await OnDirtyDispose ( ) ;
}
}