mirror of
https://github.com/wega-team/ss14-wega.git
synced 2026-06-09 10:06:49 +02:00
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:
@@ -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);
|
||||
}
|
||||
+23
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user