GameTest part 1 (#43182)
* Ports much of GameTest, minus all the WIP stuff. * remark. * EnsureCVar now adds test properties. * Some cleanup and functionality. * Ignore broken test. Needs fixed eventually. Also explicit context config. * TrackingIssue attribute. * oops * Pair config attribute. * Remove SystemAttribute in favor of using the EntitySystemManager dependency collection. * Ensure idleness. * More tests for tests. * More specific failure catching tests. * Reverse attribute resolution order so suite-wide attributes happen first. * Get rid of AffectedProperties again because I need to refactor PoolSettings for that. * Poke * Final cleanup pass. * Update Content.IntegrationTests/Fixtures/Attributes/RunOnSideAttribute.cs Co-authored-by: Tayrtahn <tayrtahn@gmail.com> * Update Content.IntegrationTests/Fixtures/Attributes/Side.cs Co-authored-by: Tayrtahn <tayrtahn@gmail.com> * Update Content.IntegrationTests/NUnit/Utilities/ITestExtensions.cs Co-authored-by: Tayrtahn <tayrtahn@gmail.com> * Fixes. * shut up nunit. * Make TrackingIssue a bit strict on purpose, so people don't put junk here. * Update Content.IntegrationTests/Tests/GameTestTests/DisconnectedDependencyTest.cs Co-authored-by: Tayrtahn <tayrtahn@gmail.com> * Address. --------- Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
This commit is contained in:
parent
f7cde159c5
commit
3c48c0129f
|
|
@ -0,0 +1,74 @@
|
||||||
|
#nullable enable
|
||||||
|
using System.Reflection;
|
||||||
|
using NUnit.Framework.Interfaces;
|
||||||
|
using NUnit.Framework.Internal;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures the given CVar, on the given side (or both), is the given value.
|
||||||
|
/// Attribute version of <see cref="GameTest.OverrideCVar{T}"/>, and stores the old value the same way.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This only works with <see cref="GameTest"/> fixtures.</remarks>
|
||||||
|
/// <param name="side">The side to set the CVar on, or both.</param>
|
||||||
|
/// <param name="definitionType">The type the CVar is defined on.</param>
|
||||||
|
/// <param name="fieldName">The name of the static field defining the CVar.</param>
|
||||||
|
/// <param name="value">The value to set the CVar to.</param>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// [Test]
|
||||||
|
/// [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.FlavorText), true)]
|
||||||
|
/// public async Task MyTest()
|
||||||
|
/// {
|
||||||
|
/// // CVar is set for you inside the test, and automatically un-set on teardown.
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
/// <seealso cref="GameTest"/>
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||||
|
public sealed class EnsureCVarAttribute(Side side, Type definitionType, string fieldName, object value) : Attribute, IGameTestModifier, IApplyToTest
|
||||||
|
{
|
||||||
|
public const string ClientEnsuredCVarsProperty = "ClientEnsuredCVars";
|
||||||
|
public const string ServerEnsuredCVarsProperty = "ServerEnsuredCVars";
|
||||||
|
|
||||||
|
Task IGameTestModifier.ApplyToTest(GameTest test)
|
||||||
|
{
|
||||||
|
var cvar = LookupCVar();
|
||||||
|
|
||||||
|
test.PreTestAddOverride(side, cvar.Name, value);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CVarDef LookupCVar()
|
||||||
|
{
|
||||||
|
var field = definitionType.GetField(fieldName, BindingFlags.Static | BindingFlags.Public);
|
||||||
|
if (field is null)
|
||||||
|
throw new ArgumentException($"Couldn't find a public, static field named {fieldName} on {definitionType}");
|
||||||
|
|
||||||
|
var obj = field.GetValue(null);
|
||||||
|
|
||||||
|
if (obj is not CVarDef cvar)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"Expected a CVar definition on {definitionType}.{fieldName}, but it was a {obj?.GetType().FullName ?? "null"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.GetType() != cvar.DefaultValue.GetType())
|
||||||
|
throw new NotSupportedException($"Cannot set {cvar.Name} to {value}, it's the wrong type.");
|
||||||
|
|
||||||
|
return cvar;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IApplyToTest.ApplyToTest(Test test)
|
||||||
|
{
|
||||||
|
var cvar = LookupCVar();
|
||||||
|
|
||||||
|
if ((side & Side.Client) != 0)
|
||||||
|
test.Properties.Add(ClientEnsuredCVarsProperty, $"{cvar.Name} = {value}");
|
||||||
|
|
||||||
|
if ((side & Side.Server) != 0)
|
||||||
|
test.Properties.Add(ServerEnsuredCVarsProperty, $"{cvar.Name} = {value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
namespace Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks an attribute as a modifier for <see cref="GameTest"/> fixtures.
|
||||||
|
/// These attributes can be applied to both test methods and fixtures.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// GameTest modifiers are <b>encouraged</b> to also implement IApplyToTest and add properties to the test
|
||||||
|
/// indicating their presence.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IGameTestModifier
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Method called by GameTest on itself when applying <see cref="GameTest"/> modifiers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="test">The test being modified</param>
|
||||||
|
/// <returns>Async task to await.</returns>
|
||||||
|
Task ApplyToTest(GameTest test);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface used for <see cref="GameTest"/> pair configuration attributes.
|
||||||
|
/// This allows such attributes to modify the pair settings, and also describe what parts of pairs they modify
|
||||||
|
/// so odd configuration choices can be spotted.
|
||||||
|
/// </summary>
|
||||||
|
public interface IGameTestPairConfigModifier
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this modifier is exclusive and should conflict with other exclusive modifiers.
|
||||||
|
/// Essentially, fail immediately if other IGameTestPairConfigModifier attributes are present if this is set.
|
||||||
|
/// </summary>
|
||||||
|
bool Exclusive { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when GameTest needs its <see cref="PoolSettings"/> modified by the modifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="test">The test we're applying to.</param>
|
||||||
|
/// <param name="settings">The settings object to modify.</param>
|
||||||
|
void ApplyToPairSettings(GameTest test, ref PoolSettings settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
#nullable enable
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the test pair using settings from the given type (by default the current test) and static property member.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sourceType">The type to look up the member on, if any.</param>
|
||||||
|
/// <param name="sourceMember">The static property to read the settings from.</param>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
|
public sealed class PairConfigAttribute(Type? sourceType, string sourceMember) : Attribute, IGameTestPairConfigModifier
|
||||||
|
{
|
||||||
|
public bool Exclusive => true;
|
||||||
|
|
||||||
|
public readonly Type? SourceType = sourceType;
|
||||||
|
public readonly string SourceMember = sourceMember;
|
||||||
|
|
||||||
|
private const BindingFlags PropertyBindingFlags = BindingFlags.Static
|
||||||
|
| BindingFlags.Public
|
||||||
|
| BindingFlags.NonPublic
|
||||||
|
| BindingFlags.FlattenHierarchy;
|
||||||
|
|
||||||
|
public PairConfigAttribute(string sourceMember) : this(null, sourceMember)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyToPairSettings(GameTest test, ref PoolSettings settings)
|
||||||
|
{
|
||||||
|
var sourceType = SourceType ?? test.GetType();
|
||||||
|
|
||||||
|
var property = sourceType.GetProperty(SourceMember, PropertyBindingFlags);
|
||||||
|
|
||||||
|
if (property is null)
|
||||||
|
{
|
||||||
|
if (sourceType.GetField(SourceMember, PropertyBindingFlags) is not null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"Couldn't find static property {SourceMember} on {sourceType.Name}, but could find a field. Only properties are allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException($"Couldn't find static property {SourceMember} on {sourceType.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!property.PropertyType.IsAssignableTo(typeof(PoolSettings)))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"{sourceType.Name}.{SourceMember} is not assignable to {nameof(PoolSettings)} and cannot be used.");
|
||||||
|
}
|
||||||
|
|
||||||
|
settings = (PoolSettings)property.GetValue(null)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
#nullable enable
|
||||||
|
using Content.IntegrationTests.NUnit.Utilities;
|
||||||
|
using NUnit.Framework.Interfaces;
|
||||||
|
using NUnit.Framework.Internal;
|
||||||
|
using NUnit.Framework.Internal.Commands;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures a test method runs on the given side (client or server, not neither nor both).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This only works for <see cref="GameTest"/> fixtures.
|
||||||
|
/// </remarks>
|
||||||
|
/// <seealso cref="GameTest"/>
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public sealed class RunOnSideAttribute : Attribute, IWrapTestMethod, IImplyFixture, IApplyToTest
|
||||||
|
{
|
||||||
|
public const string RunOnSideProperty = "RanOnSide";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Which side to run the inner test code on, if not the test thread.
|
||||||
|
/// </summary>
|
||||||
|
public Side RunOnSide { get; }
|
||||||
|
|
||||||
|
public RunOnSideAttribute(Side side)
|
||||||
|
{
|
||||||
|
RunOnSide = side;
|
||||||
|
if (side is not Side.Client and not Side.Server)
|
||||||
|
throw new NotSupportedException("Test run-on-side can only be the client or server, not both or neither.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TestCommand ICommandWrapper.Wrap(TestCommand command)
|
||||||
|
{
|
||||||
|
return new SidedTestCommand(command, RunOnSide);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SidedTestCommand : DelegatingTestCommand
|
||||||
|
{
|
||||||
|
private readonly Side _side;
|
||||||
|
|
||||||
|
public SidedTestCommand(TestCommand inner, Side side) : base(inner)
|
||||||
|
{
|
||||||
|
_side = side;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override TestResult Execute(TestExecutionContext context)
|
||||||
|
{
|
||||||
|
innerCommand.Test.EnsureFixtureIsGameTest(typeof(RunOnSideAttribute), out var gt);
|
||||||
|
|
||||||
|
if (_side is not Side.Client and not Side.Server)
|
||||||
|
throw new NotSupportedException($"Sided tests need to specify a specific side. {Test}");
|
||||||
|
|
||||||
|
if (_side is Side.Client)
|
||||||
|
{
|
||||||
|
gt.Client.WaitAssertion(() =>
|
||||||
|
{
|
||||||
|
context.CurrentResult = innerCommand.Execute(context);
|
||||||
|
})
|
||||||
|
.Wait();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gt.Server.WaitAssertion(() =>
|
||||||
|
{
|
||||||
|
context.CurrentResult = innerCommand.Execute(context);
|
||||||
|
})
|
||||||
|
.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.CurrentResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyToTest(Test test)
|
||||||
|
{
|
||||||
|
test.Properties.Add(RunOnSideProperty, RunOnSide.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
#nullable enable
|
||||||
|
namespace Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A flag enum representing a side of a testpair.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum Side : byte
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bitflag representing the client side of a testpair.
|
||||||
|
/// </summary>
|
||||||
|
Client = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// Bitflag representing the server side of a testpair.
|
||||||
|
/// </summary>
|
||||||
|
Server = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A value indicating no side was specified. You shouldn't use this outside of checking for it as an error.
|
||||||
|
/// </summary>
|
||||||
|
Neither = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// A value indicating both sides were specified.
|
||||||
|
/// </summary>
|
||||||
|
Both = Client | Server,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a field on a <see cref="GameTest"/> fixture as needing to be populated with an IoC dependency from the given side.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="GameTest"/>
|
||||||
|
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||||
|
public sealed class SidedDependencyAttribute : Attribute
|
||||||
|
{
|
||||||
|
public SidedDependencyAttribute(Side side)
|
||||||
|
{
|
||||||
|
Side = side;
|
||||||
|
|
||||||
|
if (side is not Side.Client and not Side.Server)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"Expected either the client or the server as a side, got {side}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Side Side { get; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <para>
|
||||||
|
/// An attribute meant to attach an issue (usually related to the test) to a given test or test fixture.
|
||||||
|
/// This sets the <c>TrackingIssue</c> property on the test, and helps developers find why a test exists or why it
|
||||||
|
/// is broken.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// This attribute should be used if a test corresponds directly to a bug in some way, either demonstrating it or
|
||||||
|
/// ensuring it remains fixed. Only URLs should be provided, lone issue numbers are not accepted.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// If the bug was never given an issue, the fix PR containing the test is another acceptable thing to link, and the
|
||||||
|
/// PR should clearly explain the bug it is fixing for future readers.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TrackingIssueAttribute : PropertyAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Domains we allow for tracking issues, to avoid people putting discord or discourse links.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly string[] _validDomains =
|
||||||
|
[
|
||||||
|
"github.com"
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly Regex GithubStyleIssueMatch = new(@"^\/[a-z\-\$\#]*\/[a-z\-\$\#]*\/(issues|pulls)\/\d*$",
|
||||||
|
RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
public TrackingIssueAttribute([StringSyntax(StringSyntaxAttribute.Uri)] string url) : base(url)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||||
|
throw new ArgumentException($"Expected a valid URL for {nameof(TrackingIssueAttribute)}, got {url}");
|
||||||
|
|
||||||
|
// Assert the domain is reasonable.
|
||||||
|
if (!_validDomains.Contains(uri.Host, StringComparer.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"Didn't recognize the domain used for the tracking issue, got {uri.Host}. We support: {string.Join(", ", _validDomains)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the URL is reasonable.
|
||||||
|
if (!GithubStyleIssueMatch.IsMatch(uri.AbsolutePath))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"Didn't recognize the provided github link, it should point to a specific pull request or issue. Got {uri.AbsolutePath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
#nullable enable
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Fixtures;
|
||||||
|
|
||||||
|
// REMARK: You may be wondering why this doesn't bother storing the old CVars.
|
||||||
|
// This is because TestPair actually has some not-well-known functionality to
|
||||||
|
// automatically restore CVars to what they were pre-test for you.
|
||||||
|
//
|
||||||
|
// So instead of rolling that twice, this lets TestPair handle it.
|
||||||
|
public abstract partial class GameTest
|
||||||
|
{
|
||||||
|
[SidedDependency(Side.Server)] private readonly IConfigurationManager _serverCfg = default!;
|
||||||
|
[SidedDependency(Side.Client)] private readonly IConfigurationManager _clientCfg = default!;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, object> _clientCVarOverrides = new();
|
||||||
|
private readonly Dictionary<string, object> _serverCVarOverrides = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a setup-time override for a given cvar, for use by <see cref="IGameTestModifier"/>s.
|
||||||
|
/// </summary>
|
||||||
|
public void PreTestAddOverride(Side side, string cVar, object value)
|
||||||
|
{
|
||||||
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
||||||
|
if (_setupDone)
|
||||||
|
throw new NotSupportedException("Cannot use PreTest functions after test SetUp.");
|
||||||
|
|
||||||
|
if (side is Side.Neither)
|
||||||
|
throw new NotSupportedException($"Must specify a side, or both, for {nameof(PreTestAddOverride)}");
|
||||||
|
|
||||||
|
if ((side & Side.Server) != 0)
|
||||||
|
_serverCVarOverrides.Add(cVar, value);
|
||||||
|
|
||||||
|
if ((side & Side.Client) != 0)
|
||||||
|
_clientCVarOverrides.Add(cVar, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DoPreTestOverrides()
|
||||||
|
{
|
||||||
|
foreach (var (cvar, value) in _clientCVarOverrides)
|
||||||
|
{
|
||||||
|
await OverrideCVarByName(Side.Client, cvar, value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (cvar, value) in _serverCVarOverrides)
|
||||||
|
{
|
||||||
|
await OverrideCVarByName(Side.Server, cvar, value, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Pair.RunUntilSynced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a given CVar for the provided side.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Does its own cleanup, you do not need to set the CVar back yourself.</remarks>
|
||||||
|
public async Task OverrideCVar<T>(Side side, CVarDef<T> cvar, T value, bool sync = true)
|
||||||
|
where T: notnull
|
||||||
|
{
|
||||||
|
await OverrideCVarByName(side, cvar.Name, value, sync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OverrideCVarByName(Side side, string cVar, object value, bool sync)
|
||||||
|
{
|
||||||
|
if (side is Side.Client)
|
||||||
|
{
|
||||||
|
_clientCfg.SetCVar(cVar, value);
|
||||||
|
}
|
||||||
|
else if (side is Side.Server)
|
||||||
|
{
|
||||||
|
_serverCfg.SetCVar(cVar, value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"Expected a specific side, got {side}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sync)
|
||||||
|
await Pair.RunUntilSynced();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace Content.IntegrationTests.Fixtures;
|
||||||
|
|
||||||
|
public abstract partial class GameTest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// All-default-settings PoolSettings, with the client and server disconnected.
|
||||||
|
/// </summary>
|
||||||
|
protected static PoolSettings PsDisconnected => new()
|
||||||
|
{
|
||||||
|
Connected = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
#nullable enable
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Fixtures;
|
||||||
|
|
||||||
|
public abstract partial class GameTest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains all server entities spawned using GameTest proxy methods.
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<EntityUid> _serverEntitiesToClean = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains all client entities spawned using GameTest proxy methods.
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<EntityUid> _clientEntitiesToClean = new();
|
||||||
|
|
||||||
|
private async Task CleanUpEntities()
|
||||||
|
{
|
||||||
|
await Task.WhenAll(
|
||||||
|
Server.WaitAssertion(() =>
|
||||||
|
{
|
||||||
|
foreach (var junk in _serverEntitiesToClean)
|
||||||
|
{
|
||||||
|
if (!SEntMan.Deleted(junk))
|
||||||
|
SEntMan.DeleteEntity(junk);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Client.WaitAssertion(() =>
|
||||||
|
{
|
||||||
|
foreach (var junk in _clientEntitiesToClean)
|
||||||
|
{
|
||||||
|
if (!CEntMan.Deleted(junk))
|
||||||
|
CEntMan.DeleteEntity(junk);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a string representation of an entity for the server.
|
||||||
|
/// </summary>
|
||||||
|
public string SToPrettyString(EntityUid uid)
|
||||||
|
{
|
||||||
|
return Pair.Server.EntMan.ToPrettyString(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a string representation of an entity for the client.
|
||||||
|
/// </summary>
|
||||||
|
public string CToPrettyString(EntityUid uid)
|
||||||
|
{
|
||||||
|
return Pair.Client.EntMan.ToPrettyString(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a server EntityUid into the client-side equivalent entity.
|
||||||
|
/// </summary>
|
||||||
|
public EntityUid ToClientUid(EntityUid serverUid)
|
||||||
|
{
|
||||||
|
return Pair.ToClientUid(serverUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a client EntityUid into the server-side equivalent entity.
|
||||||
|
/// </summary>
|
||||||
|
public EntityUid ToServerUid(EntityUid clientUid)
|
||||||
|
{
|
||||||
|
return Pair.ToServerUid(clientUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the given component from an entity on the server.
|
||||||
|
/// </summary>
|
||||||
|
public T SComp<T>(EntityUid target)
|
||||||
|
where T : IComponent
|
||||||
|
{
|
||||||
|
return SEntMan.GetComponent<T>(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to retrieve the given component from an entity on the server.
|
||||||
|
/// </summary>
|
||||||
|
public bool STryComp<T>(EntityUid? target, [NotNullWhen(true)] out T? component)
|
||||||
|
where T : IComponent
|
||||||
|
{
|
||||||
|
return SEntMan.TryGetComponent(target, out component);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the given component from an entity on the client.
|
||||||
|
/// </summary>
|
||||||
|
public T CComp<T>(EntityUid target)
|
||||||
|
where T : IComponent
|
||||||
|
{
|
||||||
|
return CEntMan.GetComponent<T>(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to retrieve the given component from an entity on the server.
|
||||||
|
/// </summary>
|
||||||
|
public bool CTryComp<T>(EntityUid? target, [NotNullWhen(true)] out T? component)
|
||||||
|
where T : IComponent
|
||||||
|
{
|
||||||
|
return SEntMan.TryGetComponent(target, out component);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pairs an EntityUid with the given component, from the server.
|
||||||
|
/// </summary>
|
||||||
|
public Entity<T> SEntity<T>(EntityUid target)
|
||||||
|
where T : IComponent
|
||||||
|
{
|
||||||
|
return new(target, SEntMan.GetComponent<T>(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pairs an EntityUid with the given component, from the client.
|
||||||
|
/// </summary>
|
||||||
|
public Entity<T> CEntity<T>(EntityUid target)
|
||||||
|
where T : IComponent
|
||||||
|
{
|
||||||
|
return new(target, CEntMan.GetComponent<T>(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns an entity on the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This tracks the entity for post-test cleanup.</remarks>
|
||||||
|
public EntityUid SSpawn(string? id)
|
||||||
|
{
|
||||||
|
var res = SEntMan.Spawn(id);
|
||||||
|
_serverEntitiesToClean.Add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns an entity on the server at a location.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This tracks the entity for post-test cleanup.</remarks>
|
||||||
|
public EntityUid SSpawnAtPosition(string? id, EntityCoordinates coordinates)
|
||||||
|
{
|
||||||
|
var res = SEntMan.SpawnAtPosition(id, coordinates);
|
||||||
|
_serverEntitiesToClean.Add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns an entity on the client.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This tracks the entity for post-test cleanup.</remarks>
|
||||||
|
public EntityUid CSpawn(string? id)
|
||||||
|
{
|
||||||
|
var res = CEntMan.Spawn(id);
|
||||||
|
_clientEntitiesToClean.Add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns an entity on the server at a location.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This tracks the entity for post-test cleanup.</remarks>
|
||||||
|
public EntityUid CSpawnAtPosition(string? id, EntityCoordinates coordinates)
|
||||||
|
{
|
||||||
|
var res = CEntMan.SpawnAtPosition(id, coordinates);
|
||||||
|
_clientEntitiesToClean.Add(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously spawns an entity on the server.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<EntityUid> Spawn(string? id)
|
||||||
|
{
|
||||||
|
var ent = EntityUid.Invalid;
|
||||||
|
|
||||||
|
await Server.WaitPost(() => ent = SSpawn(id));
|
||||||
|
|
||||||
|
return ent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously spawns an entity on the server at the given position.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<EntityUid> SpawnAtPosition(string? id, EntityCoordinates coords)
|
||||||
|
{
|
||||||
|
var ent = EntityUid.Invalid;
|
||||||
|
|
||||||
|
await Server.WaitPost(() => ent = SSpawnAtPosition(id, coords));
|
||||||
|
|
||||||
|
return ent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes an entity on the server immediately.
|
||||||
|
/// </summary>
|
||||||
|
public void SDeleteNow(EntityUid id)
|
||||||
|
{
|
||||||
|
SEntMan.DeleteEntity(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes an entity on the client immediately.
|
||||||
|
/// </summary>
|
||||||
|
public void CDeleteNow(EntityUid id)
|
||||||
|
{
|
||||||
|
CEntMan.DeleteEntity(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queues an entity for deletion at the end of the tick on the server.
|
||||||
|
/// </summary>
|
||||||
|
public void SQueueDel(EntityUid id)
|
||||||
|
{
|
||||||
|
SEntMan.QueueDeleteEntity(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queues an entity for deletion at the end of the tick on the client.
|
||||||
|
/// </summary>
|
||||||
|
public void CQueueDel(EntityUid id)
|
||||||
|
{
|
||||||
|
CEntMan.QueueDeleteEntity(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
namespace Content.IntegrationTests.Fixtures;
|
||||||
|
|
||||||
|
public abstract partial class GameTest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the client and server for the given number of ticks, in lockstep.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Do not use this as a barrier for client-server synchronization, use <see cref="RunUntilSynced"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public Task RunTicksSync(int ticks)
|
||||||
|
{
|
||||||
|
return Pair.RunTicksSync(ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the pairs just long enough for PVS to send entities, ensuring the client's current tick is what the
|
||||||
|
/// server's was at call time.
|
||||||
|
/// </summary>
|
||||||
|
public async Task RunUntilSynced()
|
||||||
|
{
|
||||||
|
await Pair.RunUntilSynced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the test pair for a number of (simulated) seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Does not actually take N seconds to evaluate, the game ticks as fast as possible.
|
||||||
|
/// Do not use this as a barrier for client-server synchronization, use <see cref="RunUntilSynced"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public Task RunSeconds(float seconds)
|
||||||
|
{
|
||||||
|
return Pair.RunSeconds(seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,265 @@
|
||||||
|
#nullable enable
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
using Content.IntegrationTests.NUnit.Constraints;
|
||||||
|
using Content.IntegrationTests.Pair;
|
||||||
|
using Content.IntegrationTests.Utility;
|
||||||
|
using NUnit.Framework.Interfaces;
|
||||||
|
using Robust.Client.Timing;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
using Robust.UnitTesting;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Fixtures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <para>
|
||||||
|
/// A test fixture with an integrated <see cref="GameTest.Pair">test pair</see>,
|
||||||
|
/// proxy methods for efficient test writing, utilities for ensuring tests clean up correctly,
|
||||||
|
/// and dependency injection (<see cref="SidedDependencyAttribute"/>).
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Tests using GameTest support some additional class and method level attributes, namely
|
||||||
|
/// <see cref="RunOnSideAttribute"/>.
|
||||||
|
/// Attributes can be used to control how the test runs.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="CompConstraintExtensions"/>
|
||||||
|
/// <seealso cref="LifeStageConstraintExtensions"/>
|
||||||
|
[TestFixture]
|
||||||
|
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
|
||||||
|
[Property(TestProperties.TestFrameKind, nameof(GameTest))]
|
||||||
|
[SuppressMessage("Structure", "NUnit1028:The non-test method is public")]
|
||||||
|
public abstract partial class GameTest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Set if the test manually marks itself dirty.
|
||||||
|
/// </summary>
|
||||||
|
private bool _pairDestroyed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests-testing-tests assistant to run right before the pair is returned.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? PreFinalizeHook;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The main thread of the game server.
|
||||||
|
/// </summary>
|
||||||
|
public Thread ServerThread { get; private set; } = null!; // NULLABILITY: This is always set during test setup.
|
||||||
|
/// <summary>
|
||||||
|
/// The main thread of the game client.
|
||||||
|
/// </summary>
|
||||||
|
public Thread ClientThread { get; private set; } = null!; // NULLABILITY: This is always set during test setup.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Settings for the client/server pair.
|
||||||
|
/// By default, this gets you a client and server that have connected together.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Always return a new instance whenever this is read. In other words, no backing field please. Arrow syntax only.
|
||||||
|
/// </remarks>
|
||||||
|
public virtual PoolSettings PoolSettings => new() { Connected = true };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client and server pair.
|
||||||
|
/// </summary>
|
||||||
|
public TestPair Pair { get; private set; } = default!; // NULLABILITY: This is always set during test setup.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The game server instance.
|
||||||
|
/// </summary>
|
||||||
|
public RobustIntegrationTest.ServerIntegrationInstance Server => Pair.Server;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The game client instance.
|
||||||
|
/// </summary>
|
||||||
|
public RobustIntegrationTest.ClientIntegrationInstance Client => Pair.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The test player's server session, if any.
|
||||||
|
/// </summary>
|
||||||
|
public ICommonSession? ServerSession => Pair.Player;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The server-side entity manager.
|
||||||
|
/// </summary>
|
||||||
|
[SidedDependency(Side.Server)]
|
||||||
|
public IEntityManager SEntMan = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client-side entity manager.
|
||||||
|
/// </summary>
|
||||||
|
[SidedDependency(Side.Client)]
|
||||||
|
public IEntityManager CEntMan = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The server-side prototype manager.
|
||||||
|
/// </summary>
|
||||||
|
[SidedDependency(Side.Server)]
|
||||||
|
public IPrototypeManager SProtoMan = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client-side prototype manager.
|
||||||
|
/// </summary>
|
||||||
|
[SidedDependency(Side.Client)]
|
||||||
|
public IPrototypeManager CProtoMan = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The server-side game-timing manager.
|
||||||
|
/// </summary>
|
||||||
|
[SidedDependency(Side.Server)]
|
||||||
|
public IGameTiming SGameTiming = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client-side game-timing manager.
|
||||||
|
/// </summary>
|
||||||
|
[SidedDependency(Side.Client)]
|
||||||
|
public IClientGameTiming CGameTiming = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The test map we're using, if any.
|
||||||
|
/// </summary>
|
||||||
|
public TestMapData? TestMap => Pair.TestMap;
|
||||||
|
|
||||||
|
private bool _setupDone = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Primary setup task for the fixture.
|
||||||
|
/// Custom setup must run after this.
|
||||||
|
/// </summary>
|
||||||
|
[SetUp]
|
||||||
|
public virtual async Task DoSetup()
|
||||||
|
{
|
||||||
|
_pairDestroyed = false;
|
||||||
|
var testContext = TestContext.CurrentContext;
|
||||||
|
|
||||||
|
|
||||||
|
var test = testContext.Test;
|
||||||
|
|
||||||
|
var settings = PoolSettings;
|
||||||
|
|
||||||
|
var pairAttribs = test.Method!.GetCustomAttributes<IGameTestPairConfigModifier>(false);
|
||||||
|
var pairSuiteAttribs = test.Method!.TypeInfo.GetCustomAttributes<IGameTestPairConfigModifier>(true);
|
||||||
|
|
||||||
|
if (pairAttribs.Length > 1 && pairAttribs.Any(x => x.Exclusive))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"More than one exclusive pair config attribute is present on the test member.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pairSuiteAttribs.Length > 1 && pairSuiteAttribs.Any(x => x.Exclusive))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"More than one exclusive pair config attribute is present on the test fixture.");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var attribute in pairSuiteAttribs.Concat(pairAttribs))
|
||||||
|
{
|
||||||
|
attribute.ApplyToPairSettings(this, ref settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
Pair = await PoolManager.GetServerClient(settings, new NUnitTestContextWrap(testContext, TestContext.Out));
|
||||||
|
|
||||||
|
Task.WaitAll(
|
||||||
|
Server.WaitPost(() => ServerThread = Thread.CurrentThread),
|
||||||
|
Client.WaitPost(() => ClientThread = Thread.CurrentThread)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Pair.ReallyBeIdle(5); // Arbitrary setup time wait.
|
||||||
|
|
||||||
|
InjectDependencies(this);
|
||||||
|
|
||||||
|
var attribs = test.Method!.GetCustomAttributes<IGameTestModifier>(false);
|
||||||
|
var suiteAttribs = test.Method!.TypeInfo.GetCustomAttributes<IGameTestModifier>(true);
|
||||||
|
|
||||||
|
foreach (var attribute in suiteAttribs.Concat(attribs))
|
||||||
|
{
|
||||||
|
await attribute.ApplyToTest(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupDone = true;
|
||||||
|
|
||||||
|
await DoPreTestOverrides();
|
||||||
|
|
||||||
|
await Pair.RunUntilSynced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Injects <see cref="SidedDependencyAttribute"/> dependencies into the target object.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is called on the GameTest itself automatically. Don't call it twice on the same object.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="target">The object to inject into.</param>
|
||||||
|
public void InjectDependencies(object target)
|
||||||
|
{
|
||||||
|
foreach (var field in target.GetType().GetAllFields())
|
||||||
|
{
|
||||||
|
if (field.GetCustomAttribute<SidedDependencyAttribute>() is { } depAttrib)
|
||||||
|
{
|
||||||
|
// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
|
||||||
|
if (depAttrib.Side is Side.Server)
|
||||||
|
{
|
||||||
|
field.SetValue(target, Server.EntMan.EntitySysManager.DependencyCollection.ResolveType(field.FieldType));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Must be initially connected for this...
|
||||||
|
if (Client.Session is not null)
|
||||||
|
field.SetValue(target, Client.EntMan.EntitySysManager.DependencyCollection.ResolveType(field.FieldType));
|
||||||
|
else
|
||||||
|
field.SetValue(target, Client.InstanceDependencyCollection.ResolveType(field.FieldType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Primary teardown task for the fixture.
|
||||||
|
/// Custom teardown must run before this.
|
||||||
|
/// </summary>
|
||||||
|
[TearDown]
|
||||||
|
public virtual async Task DoTeardown()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// In some cool future we might be able to make this only throw out the pair
|
||||||
|
// if the test threw exceptions. But that'd require fixing all of them to do cleanup properly on failure.
|
||||||
|
//
|
||||||
|
// So not yet.
|
||||||
|
if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed)
|
||||||
|
{
|
||||||
|
_pairDestroyed = true; // Blow it up, we failed and it might be screwed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roll forward til sync for teardown.
|
||||||
|
await Pair.RunUntilSynced();
|
||||||
|
|
||||||
|
await CleanUpEntities();
|
||||||
|
|
||||||
|
// And other teardown logic will go here. Eventually.
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_pairDestroyed = true;
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
PreFinalizeHook?.Invoke();
|
||||||
|
|
||||||
|
if (!_pairDestroyed)
|
||||||
|
await Pair.CleanReturnAsync();
|
||||||
|
else
|
||||||
|
await Pair.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
using NUnit.Framework.Constraints;
|
||||||
|
using NUnit.Framework.Internal;
|
||||||
|
using Robust.UnitTesting;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.NUnit.Constraints;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A prefix constraint like <see cref="PropertyConstraint"/>, for entity components.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="CompConstraintExtensions"/>
|
||||||
|
public sealed class CompConstraint(Type tComp, IIntegrationInstance instance, IConstraint baseConstraint)
|
||||||
|
: PrefixConstraint(baseConstraint, $"component {tComp.Name}")
|
||||||
|
{
|
||||||
|
public override ConstraintResult ApplyTo<TActual>(TActual actual)
|
||||||
|
{
|
||||||
|
if (!ConstraintHelpers.TryActualAsEnt(actual, instance, out var ent, out var error))
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException(
|
||||||
|
$"The input type {typeof(TActual)} to {nameof(CompExistsConstraint)} is not a supported entity id.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ConstraintResult(this, actual, ConstraintStatus.Failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instance.EntMan.TryGetComponent(ent, tComp, out var comp))
|
||||||
|
return new ConstraintResult(this, actual, ConstraintStatus.Failure);
|
||||||
|
|
||||||
|
var baseResult = Reflect.InvokeApplyTo(constraint: baseConstraint, tComp, comp);
|
||||||
|
return new ConstraintResult(this, baseResult.ActualValue, baseResult.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
#nullable enable
|
||||||
|
using Content.IntegrationTests.NUnit.Operators;
|
||||||
|
using NUnit.Framework.Constraints;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.UnitTesting;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.NUnit.Constraints;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides <see cref="M:Content.IntegrationTests.NUnit.Constraints.CompConstraintExtensions.extension(NUnit.Framework.Has).Comp``1(Robust.UnitTesting.IIntegrationInstance)">Has.Comp<T>(side)</see>,
|
||||||
|
/// a constraint that allows you to check for the presence of, or operate on, a component.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// // Assert that the server sided entity myEntity has ItemComponent on the server.
|
||||||
|
/// Assert.That(myEntity, Has.Comp<ItemComponent>(Server));
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
public static class CompConstraintExtensions
|
||||||
|
{
|
||||||
|
extension(Has)
|
||||||
|
{
|
||||||
|
public static ResolvableConstraintExpression Comp<T>(IIntegrationInstance instance)
|
||||||
|
where T : IComponent
|
||||||
|
{
|
||||||
|
return new ConstraintExpression().Comp<T>(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResolvableConstraintExpression Comp(Type t, IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return new ConstraintExpression().Comp(t, instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension(ConstraintExpression expr)
|
||||||
|
{
|
||||||
|
public ResolvableConstraintExpression Comp<T>(IIntegrationInstance instance)
|
||||||
|
where T : IComponent
|
||||||
|
{
|
||||||
|
return expr.Append(new CompOperator(typeof(T), instance));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResolvableConstraintExpression Comp(Type t, IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return expr.Append(new CompOperator(t, instance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
#nullable enable
|
||||||
|
using NUnit.Framework.Constraints;
|
||||||
|
using Robust.UnitTesting;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.NUnit.Constraints;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constraint for whether a component exists.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="CompConstraintExtensions"/>
|
||||||
|
public sealed class CompExistsConstraint(Type component, IIntegrationInstance instance) : Constraint
|
||||||
|
{
|
||||||
|
public override ConstraintResult ApplyTo<TActual>(TActual actual)
|
||||||
|
{
|
||||||
|
if (!ConstraintHelpers.TryActualAsEnt(actual, instance, out var ent, out var error))
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException(
|
||||||
|
$"The input type {typeof(TActual)} to {nameof(CompExistsConstraint)} is not a supported entity id.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ConstraintResult(this, actual, ConstraintStatus.Failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ConstraintResult(this, actual, instance.EntMan.HasComponent(ent, component));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Description => $"has the component {component.Name}";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
#nullable enable
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Content.IntegrationTests.NUnit.Utilities;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Toolshed.TypeParsers;
|
||||||
|
using Robust.UnitTesting;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.NUnit.Constraints;
|
||||||
|
|
||||||
|
public static class ConstraintHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A constraint implementation helper to convert TActual into an entityuid.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="t">The input value to try to get an entity uid from.</param>
|
||||||
|
/// <param name="instance">The integration test instance to resolve the entity from.</param>
|
||||||
|
/// <param name="ent">The resulting entity uid.</param>
|
||||||
|
/// <param name="validType">Whether TActual is recognized to begin with.</param>
|
||||||
|
/// <typeparam name="TActual">The type to cast out of.</typeparam>
|
||||||
|
public static bool TryActualAsEnt<TActual>(TActual t, IIntegrationInstance instance, [NotNullWhen(true)] out EntityUid? ent, out bool validType)
|
||||||
|
{
|
||||||
|
if (t is EntityUid u)
|
||||||
|
{
|
||||||
|
ent = u;
|
||||||
|
validType = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t is IAsType<EntityUid> asTy)
|
||||||
|
{
|
||||||
|
ent = asTy.AsType();
|
||||||
|
validType = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t is IResolvesToEntity resolvable)
|
||||||
|
{
|
||||||
|
if (instance is IServerIntegrationInstance)
|
||||||
|
{
|
||||||
|
ent = resolvable.SEntity;
|
||||||
|
}
|
||||||
|
else if (instance is IClientIntegrationInstance)
|
||||||
|
{
|
||||||
|
ent = resolvable.CEntity;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"{t.GetType()} is not a valid kind of IIntegrationInstance");
|
||||||
|
}
|
||||||
|
|
||||||
|
validType = false;
|
||||||
|
return ent is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t is null)
|
||||||
|
{
|
||||||
|
ent = null;
|
||||||
|
validType = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ent = null;
|
||||||
|
validType = true; // Dunno what this type is!
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
#nullable enable
|
||||||
|
using NUnit.Framework.Constraints;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.UnitTesting;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.NUnit.Constraints;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A constraint for an entity's lifestage.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="LifeStageConstraintExtensions"/>
|
||||||
|
public sealed class LifeStageConstraint(EntityLifeStage stage, IIntegrationInstance instance) : Constraint
|
||||||
|
{
|
||||||
|
public override ConstraintResult ApplyTo<TActual>(TActual actual)
|
||||||
|
{
|
||||||
|
if (!ConstraintHelpers.TryActualAsEnt(actual, instance, out var ent, out var error))
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException(
|
||||||
|
$"The input type {typeof(TActual)} to {nameof(CompExistsConstraint)} is not a supported entity id.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ConstraintResult(this, actual, ConstraintStatus.Failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lifestage = instance.EntMan.GetComponentOrNull<MetaDataComponent>(ent.Value)?.EntityLifeStage;
|
||||||
|
|
||||||
|
return new ConstraintResult(this,
|
||||||
|
lifestage,
|
||||||
|
lifestage == stage || (lifestage is null && stage is EntityLifeStage.Deleted));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Description => stage switch
|
||||||
|
{
|
||||||
|
EntityLifeStage.PreInit => "preinitialized",
|
||||||
|
EntityLifeStage.Initializing => "initializing",
|
||||||
|
EntityLifeStage.Initialized => "initialized",
|
||||||
|
EntityLifeStage.MapInitialized => "map initialized",
|
||||||
|
EntityLifeStage.Terminating => "terminating",
|
||||||
|
EntityLifeStage.Deleted => "deleted",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(stage), stage, null),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides constraints for testing if an entity is in the given lifestage.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// // Assert that the server sided entity myEntity is MapInitialized.
|
||||||
|
/// Assert.That(myEntity, Is.MapInitialized(Server));
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
public static class LifeStageConstraintExtensions
|
||||||
|
{
|
||||||
|
extension(Is)
|
||||||
|
{
|
||||||
|
public static LifeStageConstraint LifeStage(EntityLifeStage stage, IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return new LifeStageConstraint(stage, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LifeStageConstraint PreInit(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return Is.LifeStage(EntityLifeStage.PreInit, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LifeStageConstraint Initializing(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return Is.LifeStage(EntityLifeStage.Initializing, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LifeStageConstraint Initialized(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return Is.LifeStage(EntityLifeStage.Initialized, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LifeStageConstraint MapInitialized(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return Is.LifeStage(EntityLifeStage.MapInitialized, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LifeStageConstraint Terminating(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return Is.LifeStage(EntityLifeStage.Terminating, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LifeStageConstraint Deleted(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return Is.LifeStage(EntityLifeStage.Deleted, instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension(ConstraintExpression expr)
|
||||||
|
{
|
||||||
|
public LifeStageConstraint LifeStage(EntityLifeStage stage, IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
var c = new LifeStageConstraint(stage, instance);
|
||||||
|
|
||||||
|
expr.Append(c);
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LifeStageConstraint PreInit(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return expr.LifeStage(EntityLifeStage.PreInit, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LifeStageConstraint Initializing(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return expr.LifeStage(EntityLifeStage.Initializing, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LifeStageConstraint Initialized(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return expr.LifeStage(EntityLifeStage.Initialized, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LifeStageConstraint MapInitialized(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return expr.LifeStage(EntityLifeStage.MapInitialized, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LifeStageConstraint Terminating(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return expr.LifeStage(EntityLifeStage.Terminating, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LifeStageConstraint Deleted(IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
return expr.LifeStage(EntityLifeStage.Deleted, instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
using Content.IntegrationTests.NUnit.Constraints;
|
||||||
|
using NUnit.Framework.Constraints;
|
||||||
|
using Robust.UnitTesting;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.NUnit.Operators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An operator for use by nunit constraint resolution.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="CompExistsConstraint"/>
|
||||||
|
public sealed class CompOperator : SelfResolvingOperator
|
||||||
|
{
|
||||||
|
private readonly Type _tComp;
|
||||||
|
private readonly IIntegrationInstance _instance;
|
||||||
|
|
||||||
|
public CompOperator(Type tComp, IIntegrationInstance instance)
|
||||||
|
{
|
||||||
|
_tComp = tComp;
|
||||||
|
_instance = instance;
|
||||||
|
|
||||||
|
left_precedence = right_precedence = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Reduce(ConstraintBuilder.ConstraintStack stack)
|
||||||
|
{
|
||||||
|
if (RightContext is null or BinaryOperator)
|
||||||
|
stack.Push(new CompExistsConstraint(_tComp, _instance));
|
||||||
|
else
|
||||||
|
stack.Push(new CompConstraint(_tComp, _instance, stack.Pop()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.NUnit.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An interface for objects that NUnit constraints should treat as a sided entity.
|
||||||
|
/// </summary>
|
||||||
|
public interface IResolvesToEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The server-sided entity, if any.
|
||||||
|
/// </summary>
|
||||||
|
EntityUid? SEntity { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// The client-sided entity, if any.
|
||||||
|
/// </summary>
|
||||||
|
EntityUid? CEntity { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
using Content.IntegrationTests.Fixtures;
|
||||||
|
using NUnit.Framework.Interfaces;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.NUnit.Utilities;
|
||||||
|
|
||||||
|
public static class ITestExtensions
|
||||||
|
{
|
||||||
|
extension<T>(T test)
|
||||||
|
where T : ITest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures the given fixture is a <see cref="GameTest"/>, and if not gives a nice error message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="callingType">The caller's type, usually an attribute.</param>
|
||||||
|
/// <param name="gt">The <see cref="GameTest"/>.</param>
|
||||||
|
/// <exception cref="NotSupportedException">Thrown when the given test isn't a <see cref="GameTest"/></exception>
|
||||||
|
public void EnsureFixtureIsGameTest(Type callingType, out GameTest gt)
|
||||||
|
{
|
||||||
|
if (test.Fixture is not GameTest gameTest)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException(
|
||||||
|
$"The fixture {test.Fixture?.GetType()} needs to be a GameTest for {callingType.Name} to work.");
|
||||||
|
}
|
||||||
|
|
||||||
|
gt = gameTest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
#nullable enable
|
||||||
|
using Content.IntegrationTests.Fixtures;
|
||||||
|
using Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
using Content.IntegrationTests.NUnit.Constraints;
|
||||||
|
using Content.IntegrationTests.NUnit.Operators;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Tests.GameTestTests;
|
||||||
|
|
||||||
|
public sealed class ConstraintsTests : GameTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
[TestOf(typeof(CompExistsConstraint))]
|
||||||
|
[Description("Ensures that a freshly spawned entity matches a constraint stating it has MetaData.")]
|
||||||
|
[RunOnSide(Side.Server)]
|
||||||
|
public void CompPositive()
|
||||||
|
{
|
||||||
|
var ent = SSpawn(null);
|
||||||
|
|
||||||
|
Assert.That(ent, Has.Comp<MetaDataComponent>(Server));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestOf(typeof(CompOperator))]
|
||||||
|
[Description("Ensures that NUnit property access works on Comp constraints.")]
|
||||||
|
[RunOnSide(Side.Server)]
|
||||||
|
public void CompPropertyAccess()
|
||||||
|
{
|
||||||
|
var ent = SSpawn(null);
|
||||||
|
|
||||||
|
Assert.That(ent,
|
||||||
|
Has
|
||||||
|
.Comp<MetaDataComponent>(Server)
|
||||||
|
.Property(nameof(MetaDataComponent.EntityDeleted))
|
||||||
|
.EqualTo(false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestOf(typeof(CompExistsConstraint))]
|
||||||
|
[Description("Ensures that a freshly spawned entity does not match a constraint stating it has some odd component.")]
|
||||||
|
[RunOnSide(Side.Server)]
|
||||||
|
public void CompNegative()
|
||||||
|
{
|
||||||
|
var ent = SSpawn(null);
|
||||||
|
|
||||||
|
// Arbitrary pick.
|
||||||
|
Assert.That(ent, Has.No.Comp<EyeComponent>(Server));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestOf(typeof(LifeStageConstraint))]
|
||||||
|
[Description("Ensures that a freshly deleted entity is deleted to constraints.")]
|
||||||
|
[RunOnSide(Side.Server)]
|
||||||
|
public void DeletedPositive()
|
||||||
|
{
|
||||||
|
var ent = SSpawn(null);
|
||||||
|
|
||||||
|
SDeleteNow(ent);
|
||||||
|
|
||||||
|
Assert.That(ent, Is.Deleted(Server));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestOf(typeof(LifeStageConstraint))]
|
||||||
|
[RunOnSide(Side.Server)]
|
||||||
|
[Description("Entities that never existed are currently considered deleted by constraints.")]
|
||||||
|
public void DeletedNeverExisted()
|
||||||
|
{
|
||||||
|
// We'll never spawn this many ents in tests without it taking all damn day.
|
||||||
|
var ent = new EntityUid(int.MaxValue / 2);
|
||||||
|
|
||||||
|
Assert.That(ent, Is.Deleted(Server), "Entites that never existed still count as deleted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[TestOf(typeof(LifeStageConstraint))]
|
||||||
|
[Description("Entities that are alive do not count as deleted.")]
|
||||||
|
[RunOnSide(Side.Server)]
|
||||||
|
public void DeletedNegative()
|
||||||
|
{
|
||||||
|
var ent = SSpawn(null);
|
||||||
|
|
||||||
|
Assert.That(ent, Is.Not.Deleted(Server));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
#nullable enable
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Content.Client.GameTicking.Managers;
|
||||||
|
using Content.IntegrationTests.Fixtures;
|
||||||
|
using Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
using Content.IntegrationTests.NUnit.Constraints;
|
||||||
|
using Content.Server.GameTicking;
|
||||||
|
using Content.Shared.GameTicking;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Tests.GameTestTests;
|
||||||
|
|
||||||
|
[TestOf(typeof(GameTest))]
|
||||||
|
[TestOf(typeof(SidedDependencyAttribute))]
|
||||||
|
public sealed class DependencyTests : GameTest
|
||||||
|
{
|
||||||
|
[SidedDependency(Side.Server)] private readonly SharedGameTicker _sGameTicker = null!;
|
||||||
|
[SidedDependency(Side.Client)] private readonly SharedGameTicker _cGameTicker = null!;
|
||||||
|
[SidedDependency(Side.Server)] private readonly EntityQuery<TransformComponent> _sXformQuery = default!;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Description("Asserts that sided dependencies actually grab from the right sides.")]
|
||||||
|
public void DependenciesRespectSides()
|
||||||
|
{
|
||||||
|
using (Assert.EnterMultipleScope())
|
||||||
|
{
|
||||||
|
Assert.That(!ReferenceEquals(SEntMan, CEntMan), "Server and client entity managers should be distinct");
|
||||||
|
Assert.That(SEntMan, Is.EqualTo(Server.EntMan).Using<object?>(ReferenceEqualityComparer.Instance));
|
||||||
|
Assert.That(CEntMan, Is.EqualTo(Client.EntMan).Using<object?>(ReferenceEqualityComparer.Instance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Description("Asserts that system dependencies actually grab from the right sides.")]
|
||||||
|
public void SystemDependenciesRespectSides()
|
||||||
|
{
|
||||||
|
using (Assert.EnterMultipleScope())
|
||||||
|
{
|
||||||
|
Assert.That(!ReferenceEquals(_sGameTicker, _cGameTicker),
|
||||||
|
"Server and client gametickers should be distinct");
|
||||||
|
Assert.That(_sGameTicker, Is.TypeOf<GameTicker>());
|
||||||
|
Assert.That(_cGameTicker, Is.TypeOf<ClientGameTicker>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Description("Asserts that query dependencies function")]
|
||||||
|
public async Task QueryDependencies()
|
||||||
|
{
|
||||||
|
var ent = await Spawn(null);
|
||||||
|
|
||||||
|
Assert.That(_sXformQuery.HasComp(ent), Is.True);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
#nullable enable
|
||||||
|
using Content.IntegrationTests.Fixtures;
|
||||||
|
using Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC.Exceptions;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Tests.GameTestTests;
|
||||||
|
|
||||||
|
[TestOf(typeof(GameTest))]
|
||||||
|
[TestOf(typeof(SidedDependencyAttribute))]
|
||||||
|
public sealed class DisconnectedDependencyTest : GameTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
[PairConfig(nameof(PsDisconnected))]
|
||||||
|
[Description($"""
|
||||||
|
Ensures that GameTest can be started up even when the client {nameof(EntitySystemManager)} isn't initialized yet.
|
||||||
|
No body is necessary, if this fails then the bug is firmly in GameTest itself.
|
||||||
|
""")]
|
||||||
|
public void EnsureGameTestSetupWorksDisconnected()
|
||||||
|
{
|
||||||
|
// Nothin'
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[PairConfig(nameof(PsDisconnected))]
|
||||||
|
[Description("""
|
||||||
|
Ensures dependency injection that relies on client side systems fails as expected when the client is detached.
|
||||||
|
""")]
|
||||||
|
public void ClientSystemDependencyFails()
|
||||||
|
{
|
||||||
|
var creature = new Creature();
|
||||||
|
|
||||||
|
Assert.Throws<UnregisteredTypeException>(() =>
|
||||||
|
{
|
||||||
|
InjectDependencies(creature);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Creature
|
||||||
|
{
|
||||||
|
#pragma warning disable CS0414 // Field is assigned but its value is never used
|
||||||
|
[SidedDependency(Side.Client)] private readonly MapSystem _mapSys = null!;
|
||||||
|
#pragma warning restore CS0414 // Field is assigned but its value is never used
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
#nullable enable
|
||||||
|
using System.Linq;
|
||||||
|
using Content.IntegrationTests.Fixtures;
|
||||||
|
using Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Tests.GameTestTests;
|
||||||
|
|
||||||
|
[TestOf(typeof(GameTest))]
|
||||||
|
[TestOf(typeof(EnsureCVarAttribute))]
|
||||||
|
[Description("Ensures EnsureCVar actually sets CVars as expected.")]
|
||||||
|
[EnsureCVar(Side.Server, typeof(EnsureCVarsTestCVars), nameof(EnsureCVarsTestCVars.Foo), true)]
|
||||||
|
public sealed class EnsureCVarTest : GameTest
|
||||||
|
{
|
||||||
|
[SidedDependency(Side.Server)] private readonly IConfigurationManager _sCfg = default!;
|
||||||
|
[SidedDependency(Side.Client)] private readonly IConfigurationManager _cCfg = default!;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Description("Ensure Foo is set and Bar is not.")]
|
||||||
|
public void FooIsSet()
|
||||||
|
{
|
||||||
|
using (Assert.EnterMultipleScope())
|
||||||
|
{
|
||||||
|
Assert.That(_sCfg.GetCVar(EnsureCVarsTestCVars.Foo), Is.True);
|
||||||
|
Assert.That(_cCfg.GetCVar(EnsureCVarsTestCVars.Foo),
|
||||||
|
Is.EqualTo(EnsureCVarsTestCVars.Foo.DefaultValue),
|
||||||
|
"Foo is not replicated and should not be set on the client.");
|
||||||
|
|
||||||
|
Assert.That(_sCfg.GetCVar(EnsureCVarsTestCVars.Bar),
|
||||||
|
Is.EqualTo(EnsureCVarsTestCVars.Bar.DefaultValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[EnsureCVar(Side.Server, typeof(EnsureCVarsTestCVars), nameof(EnsureCVarsTestCVars.Bar), 42)]
|
||||||
|
[Description("Ensure Foo and Bar are set.")]
|
||||||
|
public void BarIsSet()
|
||||||
|
{
|
||||||
|
var props = TestContext.CurrentContext.Test.Properties;
|
||||||
|
using (Assert.EnterMultipleScope())
|
||||||
|
{
|
||||||
|
Assert.That(_sCfg.GetCVar(EnsureCVarsTestCVars.Bar),
|
||||||
|
Is.EqualTo(42));
|
||||||
|
Assert.That(_cCfg.GetCVar(EnsureCVarsTestCVars.Bar),
|
||||||
|
Is.EqualTo(EnsureCVarsTestCVars.Bar.DefaultValue),
|
||||||
|
"Bar is not replicated and should not be set on the client.");
|
||||||
|
|
||||||
|
Assert.That(props[EnsureCVarAttribute.ServerEnsuredCVarsProperty].Count(),
|
||||||
|
Is.EqualTo(1),
|
||||||
|
"Expected EnsureCVar to appropriately mark its target test.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
#nullable enable
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Tests.GameTestTests;
|
||||||
|
|
||||||
|
[CVarDefs]
|
||||||
|
public sealed class EnsureCVarsTestCVars
|
||||||
|
{
|
||||||
|
public static readonly CVarDef<bool> Foo =
|
||||||
|
CVarDef.Create("tests.ensure_cvars.foo", false, CVar.SERVER);
|
||||||
|
|
||||||
|
public static readonly CVarDef<int> Bar =
|
||||||
|
CVarDef.Create("tests.ensure_cvars.bar", 3, CVar.SERVER);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
using Content.IntegrationTests.Fixtures;
|
||||||
|
using Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Tests.GameTestTests;
|
||||||
|
|
||||||
|
[TestOf(typeof(GameTest))]
|
||||||
|
[TestOf(typeof(IGameTestPairConfigModifier))]
|
||||||
|
public sealed class IGameTestPairConfigModifierAttributeTests : GameTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Control()
|
||||||
|
{
|
||||||
|
Assert.That(Pair.Settings.Connected, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[PairConfig(nameof(PsDisconnected))]
|
||||||
|
[Description("Ensures pair settings apply.")]
|
||||||
|
public void PairConfigWorks()
|
||||||
|
{
|
||||||
|
Assert.That(Pair.Settings.Connected, Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
#nullable enable
|
||||||
|
using System.Threading;
|
||||||
|
using Content.IntegrationTests.Fixtures;
|
||||||
|
using Content.IntegrationTests.Fixtures.Attributes;
|
||||||
|
|
||||||
|
namespace Content.IntegrationTests.Tests.GameTestTests;
|
||||||
|
|
||||||
|
[TestOf(typeof(GameTest))]
|
||||||
|
[TestOf(typeof(RunOnSideAttribute))]
|
||||||
|
[Description("Asserts that RunOnSide actually does as expected and runs the test on the given side.")]
|
||||||
|
public sealed class RunOnSideTests : GameTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
[Description("Ensures that the default scenario is the test thread.")]
|
||||||
|
public void Control()
|
||||||
|
{
|
||||||
|
Assert.That(Thread.CurrentThread, Is.Not.EqualTo(ServerThread).And.Not.EqualTo(ClientThread));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[RunOnSide(Side.Server)]
|
||||||
|
public void TestServerSide()
|
||||||
|
{
|
||||||
|
Assert.That(Thread.CurrentThread, Is.EqualTo(ServerThread));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[RunOnSide(Side.Client)]
|
||||||
|
public void TestClientSide()
|
||||||
|
{
|
||||||
|
Assert.That(Thread.CurrentThread, Is.EqualTo(ClientThread));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[RunOnSide(Side.Server)]
|
||||||
|
[Description("Ensures that RunOnSide appropriately adds a property.")]
|
||||||
|
[Ignore("TestContext on the game threads is broken.")]
|
||||||
|
[TrackingIssue("https://github.com/space-wizards/RobustToolbox/issues/6449")]
|
||||||
|
public void TestProperty()
|
||||||
|
{
|
||||||
|
Assert.That(TestContext.CurrentContext.Test.Properties.Get(RunOnSideAttribute.RunOnSideProperty), Is.Not.Null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Content.IntegrationTests.Utility;
|
||||||
|
|
||||||
|
public static class TestProperties
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the property describing what kind of test frame a test is running under.
|
||||||
|
/// </summary>
|
||||||
|
public const string TestFrameKind = "TestFrameKind";
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue