mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-06-09 10:06:34 +02:00
Improve test CI and reduce test log spam. (#6447)
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
This commit is contained in:
@@ -28,4 +28,12 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
|
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: dotnet test --no-build -- NUnit.ConsoleOut=0
|
shell: pwsh
|
||||||
|
run: dotnet test --no-build -- NUnit.ConsoleOut=0 NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results"
|
||||||
|
- name: Archive NUnit3 test results.
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: nunit3-results-${{ matrix.os }}
|
||||||
|
path: test_results/*
|
||||||
|
retention-days: 7
|
||||||
|
|||||||
@@ -35,8 +35,19 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build --configuration Tools --no-restore
|
run: dotnet build --configuration DebugOpt --no-restore /m
|
||||||
- name: Content.Tests
|
- name: Content.Tests
|
||||||
run: dotnet test --no-build Content.Tests/Content.Tests.csproj -v n
|
shell: pwsh
|
||||||
|
run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0 NUnit.TestOutputXml="$(pwd)/test_results"
|
||||||
- name: Content.IntegrationTests
|
- name: Content.IntegrationTests
|
||||||
run: COMPlus_gcServer=1 dotnet test --no-build Content.IntegrationTests/Content.IntegrationTests.csproj -v n
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$env:DOTNET_gcServer=1
|
||||||
|
dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed NUnit.TestOutputXml="logs" NUnit.WorkDirectory="$(pwd)/test_results"
|
||||||
|
- name: Archive NUnit3 test results.
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: nunit3-results
|
||||||
|
path: test_results/*
|
||||||
|
retention-days: 7
|
||||||
|
|||||||
+15
-1
@@ -35,11 +35,25 @@ END TEMPLATE-->
|
|||||||
|
|
||||||
### Breaking changes
|
### Breaking changes
|
||||||
|
|
||||||
|
- ITestPair.Init() now requires a TextWriter be provided to write its gravestone to.
|
||||||
|
This gravestone is where TestPair test history is now written to.
|
||||||
|
- ITestPair.AddToHistory() must be used to add tests to the test history.
|
||||||
|
- Test history is now stored in ITestPair.ExtendedTestHistory.
|
||||||
|
- Engine and content tests relying on TestPair using NUnit will now automatically
|
||||||
|
output gravestone files for test history. You should set the working directory for tests if you wish to
|
||||||
|
use these artifacts.
|
||||||
|
- Using test pairs outside of a test environment without providing an `ITestContextLike` implementor will
|
||||||
|
now crash due to the lack of a running NUnit test. Use an `ExternalTestContext` for this use case.
|
||||||
|
- Test logs no longer contain TestPair history.
|
||||||
|
- Test history now includes the GC total memory usage at time of AddToHistory() call. This is typically while the test
|
||||||
|
is obtaining a pair.
|
||||||
|
- ITestPair is now `[NotContentImplementable]` and future additions to the interface will not be considered breaking.
|
||||||
|
- Erroneous logs in tests are now allowed to occur more than once, and assert a failure at the end of the test while doing pair cleanup instead of during it.
|
||||||
- `Prototype<T>`, a precursor to `ProtoId<T>` used by toolshed, has been removed.
|
- `Prototype<T>`, a precursor to `ProtoId<T>` used by toolshed, has been removed.
|
||||||
|
|
||||||
### New features
|
### New features
|
||||||
|
|
||||||
*None yet*
|
- TestPairs now automatically log their test history to a gravestone file.
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Robust.UnitTesting.Pool;
|
||||||
|
|
||||||
|
namespace Robust.UnitTesting.Shared.Testing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A test pool specifically for testing testpool and testpair behavior.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class EngineDummyTestPool : PoolManager<RobustIntegrationTest.TestPair>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using Robust.UnitTesting.Pool;
|
||||||
|
|
||||||
|
namespace Robust.UnitTesting.Shared.Testing;
|
||||||
|
|
||||||
|
|
||||||
|
public sealed class EngineTestErrorLogFails
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
[Description("Asserts that Error level logs in a test pair instance do not throw, but do assert during CleanReturnAsync.")]
|
||||||
|
public async Task AssertErrorLoggingFailsTestCleanReturnAsync()
|
||||||
|
{
|
||||||
|
var pool = new EngineDummyTestPool();
|
||||||
|
|
||||||
|
pool.Startup();
|
||||||
|
|
||||||
|
var pair = await pool.GetPair();
|
||||||
|
|
||||||
|
// Log on both sides.. nothing should happen.
|
||||||
|
Assert.DoesNotThrow(() => pair.Server.Log.Error("Mogus"));
|
||||||
|
Assert.DoesNotThrow(() => pair.Client.Log.Error("Mogus"));
|
||||||
|
|
||||||
|
// But it should get very mad here.
|
||||||
|
Assert.ThrowsAsync<MultipleAssertException>(async () => await pair.CleanReturnAsync());
|
||||||
|
|
||||||
|
Assert.That(pair.State, NUnit.Framework.Is.EqualTo(PairState.Dead), "Expected the pair's return to result in its death.");
|
||||||
|
|
||||||
|
pool.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Description("Asserts that Error level logs in a test pair instance do not throw, but do assert during DirtyDispose.")]
|
||||||
|
public async Task AssertErrorLoggingFailsTestDirtyDispose()
|
||||||
|
{
|
||||||
|
var pool = new EngineDummyTestPool();
|
||||||
|
|
||||||
|
pool.Startup();
|
||||||
|
|
||||||
|
var pair = await pool.GetPair();
|
||||||
|
|
||||||
|
Assert.DoesNotThrow(() => pair.Server.Log.Error("Mogus"));
|
||||||
|
Assert.DoesNotThrow(() => pair.Client.Log.Error("Mogus"));
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<MultipleAssertException>(async () => await pair.DisposeAsync());
|
||||||
|
|
||||||
|
Assert.That(pair.State, NUnit.Framework.Is.EqualTo(PairState.Dead), "Expected the pair's return to result in its death.");
|
||||||
|
|
||||||
|
pool.Shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
namespace Robust.UnitTesting.Pool;
|
namespace Robust.UnitTesting.Pool;
|
||||||
|
|
||||||
|
[NotContentImplementable]
|
||||||
public interface ITestPair
|
public interface ITestPair
|
||||||
{
|
{
|
||||||
int Id { get; }
|
int Id { get; }
|
||||||
@@ -12,7 +15,10 @@ public interface ITestPair
|
|||||||
public PairState State { get; }
|
public PairState State { get; }
|
||||||
public bool Initialized { get; }
|
public bool Initialized { get; }
|
||||||
void Kill();
|
void Kill();
|
||||||
List<string> TestHistory { get; }
|
|
||||||
|
[Obsolete("Constructed on the spot when needed, use ExtendedTestHistory instead.")]
|
||||||
|
List<string> TestHistory => ExtendedTestHistory.Select(x => x.TestName).ToList();
|
||||||
|
IReadOnlyList<TestHistoryEntry> ExtendedTestHistory { get; }
|
||||||
PairSettings Settings { get; set; }
|
PairSettings Settings { get; set; }
|
||||||
|
|
||||||
int ServerSeed { get; }
|
int ServerSeed { get; }
|
||||||
@@ -23,11 +29,12 @@ public interface ITestPair
|
|||||||
void SetupSeed();
|
void SetupSeed();
|
||||||
void ClearModifiedCvars();
|
void ClearModifiedCvars();
|
||||||
void Use();
|
void Use();
|
||||||
Task Init(int id, BasePoolManager manager, PairSettings settings, TextWriter testOut);
|
Task Init(int id, BasePoolManager manager, PairSettings settings, TextWriter testOut, TextWriter? gravestone);
|
||||||
Task RecycleInternal(PairSettings next, TextWriter testOut);
|
Task RecycleInternal(PairSettings next, TextWriter testOut);
|
||||||
Task ApplySettings(PairSettings settings);
|
Task ApplySettings(PairSettings settings);
|
||||||
Task RunTicksSync(int ticks);
|
Task RunTicksSync(int ticks);
|
||||||
Task SyncTicks(int targetDelta = 1);
|
Task SyncTicks(int targetDelta = 1);
|
||||||
|
Task AddToHistory(string testName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PairState : byte
|
public enum PairState : byte
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace Robust.UnitTesting.Pool;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class NUnitTestContextWrap(TestContext context, TextWriter writer) : ITestContextLike
|
public sealed class NUnitTestContextWrap(TestContext context, TextWriter writer) : ITestContextLike
|
||||||
{
|
{
|
||||||
public string FullName => context.Test.FullName;
|
public readonly TestContext Context = context;
|
||||||
|
public string FullName => Context.Test.FullName;
|
||||||
public TextWriter Out => writer;
|
public TextWriter Out => writer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Text;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Internal;
|
||||||
using Robust.Shared;
|
using Robust.Shared;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
@@ -15,6 +16,14 @@ namespace Robust.UnitTesting.Pool;
|
|||||||
|
|
||||||
public abstract class BasePoolManager
|
public abstract class BasePoolManager
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Stores a global count for pair IDs, so they're unique across the entire run regardless of how many pools we
|
||||||
|
/// create and use.
|
||||||
|
///
|
||||||
|
/// This ensures things like gravestone files are truly unique.
|
||||||
|
/// </summary>
|
||||||
|
internal static int PairIdCounter = 0;
|
||||||
|
|
||||||
internal abstract void Return(ITestPair pair);
|
internal abstract void Return(ITestPair pair);
|
||||||
public abstract Assembly[] ClientAssemblies { get; }
|
public abstract Assembly[] ClientAssemblies { get; }
|
||||||
public abstract Assembly[] ServerAssemblies { get; }
|
public abstract Assembly[] ServerAssemblies { get; }
|
||||||
@@ -35,7 +44,6 @@ public abstract class BasePoolManager
|
|||||||
[Virtual]
|
[Virtual]
|
||||||
public class PoolManager<TPair> : BasePoolManager where TPair : class, ITestPair, new()
|
public class PoolManager<TPair> : BasePoolManager where TPair : class, ITestPair, new()
|
||||||
{
|
{
|
||||||
private int _nextPairId;
|
|
||||||
private readonly Lock _pairLock = new();
|
private readonly Lock _pairLock = new();
|
||||||
private bool _initialized;
|
private bool _initialized;
|
||||||
|
|
||||||
@@ -128,10 +136,10 @@ public class PoolManager<TPair> : BasePoolManager where TPair : class, ITestPair
|
|||||||
foreach (var pair in pairs)
|
foreach (var pair in pairs)
|
||||||
{
|
{
|
||||||
var borrowed = Pairs[pair];
|
var borrowed = Pairs[pair];
|
||||||
builder.AppendLine($"Pair {pair.Id}, Tests Run: {pair.TestHistory.Count}, Borrowed: {borrowed}");
|
builder.AppendLine($"Pair {pair.Id}, Tests Run: {pair.ExtendedTestHistory.Count}, Borrowed: {borrowed}");
|
||||||
for (var i = 0; i < pair.TestHistory.Count; i++)
|
for (var i = 0; i < pair.ExtendedTestHistory.Count; i++)
|
||||||
{
|
{
|
||||||
builder.AppendLine($"#{i}: {pair.TestHistory[i]}");
|
builder.AppendLine($"#{i}: {pair.ExtendedTestHistory[i]}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,64 +166,49 @@ public class PoolManager<TPair> : BasePoolManager where TPair : class, ITestPair
|
|||||||
var watch = new Stopwatch();
|
var watch = new Stopwatch();
|
||||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Called by test {currentTestName}");
|
await testOut.WriteLineAsync($"{nameof(GetPair)}: Called by test {currentTestName}");
|
||||||
TPair? pair = null;
|
TPair? pair = null;
|
||||||
try
|
watch.Start();
|
||||||
|
if (settings.MustBeNew)
|
||||||
{
|
{
|
||||||
watch.Start();
|
await testOut.WriteLineAsync(
|
||||||
if (settings.MustBeNew)
|
$"{nameof(GetPair)}: Creating pair, because settings of pool settings");
|
||||||
|
pair = await CreateServerClientPair(settings, testContext, testOut);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await testOut.WriteLineAsync($"{nameof(GetPair)}: Looking in pool for a suitable pair");
|
||||||
|
pair = GrabOptimalPair(settings);
|
||||||
|
if (pair != null)
|
||||||
{
|
{
|
||||||
await testOut.WriteLineAsync(
|
pair.ActivateContext(testOut);
|
||||||
$"{nameof(GetPair)}: Creating pair, because settings of pool settings");
|
await testOut.WriteLineAsync($"{nameof(GetPair)}: Suitable pair found");
|
||||||
pair = await CreateServerClientPair(settings, testOut);
|
|
||||||
}
|
if (pair.Settings.CanFastRecycle(settings))
|
||||||
else
|
|
||||||
{
|
|
||||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Looking in pool for a suitable pair");
|
|
||||||
pair = GrabOptimalPair(settings);
|
|
||||||
if (pair != null)
|
|
||||||
{
|
{
|
||||||
pair.ActivateContext(testOut);
|
await testOut.WriteLineAsync($"{nameof(GetPair)}: Cleanup not needed, Skipping cleanup of pair");
|
||||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Suitable pair found");
|
await pair.ApplySettings(settings);
|
||||||
|
|
||||||
if (pair.Settings.CanFastRecycle(settings))
|
|
||||||
{
|
|
||||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Cleanup not needed, Skipping cleanup of pair");
|
|
||||||
await pair.ApplySettings(settings);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Cleaning existing pair");
|
|
||||||
await pair.RecycleInternal(settings, testOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
await pair.RunTicksSync(5);
|
|
||||||
await pair.SyncTicks(targetDelta: 1);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Creating a new pair, no suitable pair found in pool");
|
await testOut.WriteLineAsync($"{nameof(GetPair)}: Cleaning existing pair");
|
||||||
pair = await CreateServerClientPair(settings, testOut);
|
await pair.RecycleInternal(settings, testOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await pair.RunTicksSync(5);
|
||||||
|
await pair.SyncTicks(targetDelta: 1);
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (pair != null && pair.TestHistory.Count > 0)
|
|
||||||
{
|
{
|
||||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Pair {pair.Id} Test History Start");
|
await testOut.WriteLineAsync($"{nameof(GetPair)}: Creating a new pair, no suitable pair found in pool");
|
||||||
for (var i = 0; i < pair.TestHistory.Count; i++)
|
pair = await CreateServerClientPair(settings, testContext, testOut);
|
||||||
{
|
|
||||||
await testOut.WriteLineAsync($"- Pair {pair.Id} Test #{i}: {pair.TestHistory[i]}");
|
|
||||||
}
|
|
||||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Pair {pair.Id} Test History End");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Retrieving pair {pair.Id} from pool took {watch.Elapsed.TotalMilliseconds} ms");
|
await testOut.WriteLineAsync($"{nameof(GetPair)}: Retrieving pair {pair.Id} from pool took {watch.Elapsed.TotalMilliseconds} ms");
|
||||||
|
|
||||||
|
await pair.AddToHistory(currentTestName);
|
||||||
pair.ValidateSettings(settings);
|
pair.ValidateSettings(settings);
|
||||||
pair.ClearModifiedCvars();
|
pair.ClearModifiedCvars();
|
||||||
pair.Settings = settings;
|
pair.Settings = settings;
|
||||||
pair.TestHistory.Add(currentTestName);
|
|
||||||
pair.SetupSeed();
|
pair.SetupSeed();
|
||||||
|
|
||||||
await testOut.WriteLineAsync($"{nameof(GetPair)}: Returning pair {pair.Id} with client/server seeds: {pair.ClientSeed}/{pair.ServerSeed}");
|
await testOut.WriteLineAsync($"{nameof(GetPair)}: Returning pair {pair.Id} with client/server seeds: {pair.ClientSeed}/{pair.ServerSeed}");
|
||||||
@@ -290,13 +283,18 @@ we are just going to end this here to save a lot of time. This is the exception
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TPair> CreateServerClientPair(PairSettings settings, TextWriter testOut)
|
private async Task<TPair> CreateServerClientPair(PairSettings settings, ITestContextLike context, TextWriter testOut)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var id = Interlocked.Increment(ref _nextPairId);
|
var id = Interlocked.Increment(ref PairIdCounter);
|
||||||
var pair = new TPair();
|
var pair = new TPair();
|
||||||
await pair.Init(id, this, settings, testOut);
|
|
||||||
|
TextWriter? gravestone = null;
|
||||||
|
if (context is NUnitTestContextWrap)
|
||||||
|
gravestone = File.CreateText($"{TestContext.CurrentContext.WorkDirectory}/gravestone-{id}.txt");
|
||||||
|
|
||||||
|
await pair.Init(id, this, settings, testOut, gravestone);
|
||||||
pair.Use();
|
pair.Use();
|
||||||
await pair.RunTicksSync(5);
|
await pair.RunTicksSync(5);
|
||||||
await pair.SyncTicks(targetDelta: 1);
|
await pair.SyncTicks(targetDelta: 1);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using Robust.Shared.Log;
|
using Robust.Shared.Log;
|
||||||
@@ -12,8 +13,7 @@ namespace Robust.UnitTesting.Pool;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// This class logs to two places: an NUnit <see cref="TestContext"/> (so it nicely gets attributed to a test in your IDE),
|
/// This class logs to one place: an NUnit <see cref="TestContext"/> (so it nicely gets attributed to a test in your IDE)
|
||||||
/// and an in-memory ring buffer for diagnostic purposes. If test pooling breaks, the ring buffer can be used to see what the broken instance has gone through.
|
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// The active test context can be swapped out so pooled instances can correctly have their logs attributed.
|
/// The active test context can be swapped out so pooled instances can correctly have their logs attributed.
|
||||||
@@ -29,6 +29,10 @@ public sealed class PoolTestLogHandler : ILogHandler
|
|||||||
|
|
||||||
public LogLevel? FailureLevel { get; set; }
|
public LogLevel? FailureLevel { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<string> FailingLogs => _failingLogs;
|
||||||
|
|
||||||
|
private readonly List<string> _failingLogs = new();
|
||||||
|
|
||||||
public PoolTestLogHandler(string? prefix)
|
public PoolTestLogHandler(string? prefix)
|
||||||
{
|
{
|
||||||
_prefix = prefix != null ? $"{prefix}: " : "";
|
_prefix = prefix != null ? $"{prefix}: " : "";
|
||||||
@@ -36,6 +40,18 @@ public sealed class PoolTestLogHandler : ILogHandler
|
|||||||
|
|
||||||
public bool ShuttingDown;
|
public bool ShuttingDown;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <para>
|
||||||
|
/// Event handler that allows you to override a potential failing log.
|
||||||
|
/// Use this if you want to allow certain error logs to be considered passing.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Has the sawmill name and <see cref="LogEvent"/> passed in, and should return a boolean
|
||||||
|
/// <see langword="true"/> when the log message should not be logged as a failure.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public event Func<string, LogEvent, bool>? JudgeLog;
|
||||||
|
|
||||||
public void Log(string sawmillName, LogEvent message)
|
public void Log(string sawmillName, LogEvent message)
|
||||||
{
|
{
|
||||||
var level = message.Level.ToRobust();
|
var level = message.Level.ToRobust();
|
||||||
@@ -57,16 +73,17 @@ public sealed class PoolTestLogHandler : ILogHandler
|
|||||||
|
|
||||||
testContext.WriteLine(line);
|
testContext.WriteLine(line);
|
||||||
|
|
||||||
if (FailureLevel == null || level < FailureLevel)
|
if (FailureLevel == null || level < FailureLevel || (JudgeLog?.Invoke(sawmillName, message) ?? false))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
testContext.Flush();
|
testContext.Flush();
|
||||||
Assert.Fail($"{line} Exception: {message.Exception}");
|
_failingLogs.Add($"{line} Exception: {message.Exception}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearContext()
|
public void ClearContext()
|
||||||
{
|
{
|
||||||
ActiveContext = null;
|
ActiveContext = null;
|
||||||
|
_failingLogs.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ActivateContext(TextWriter context)
|
public void ActivateContext(TextWriter context)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace Robust.UnitTesting.Pool;
|
||||||
|
|
||||||
|
public sealed class TestHistoryEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the test.
|
||||||
|
/// </summary>
|
||||||
|
public readonly string TestName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The amount of memory the GC claims to be using at the time of adding this entry.
|
||||||
|
/// </summary>
|
||||||
|
public readonly long TimeOfUseMemoryTotal;
|
||||||
|
|
||||||
|
internal TestHistoryEntry(string testName, long timeOfUseMemoryTotal)
|
||||||
|
{
|
||||||
|
TestName = testName;
|
||||||
|
TimeOfUseMemoryTotal = timeOfUseMemoryTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{TestName} (started at {TimeOfUseMemoryTotal} bytes allocated.)";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using Robust.Client.Timing;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
@@ -201,6 +202,31 @@ public partial class TestPair<TServer, TClient>
|
|||||||
await RunTicksSync(SecondsToTicks(seconds));
|
await RunTicksSync(SecondsToTicks(seconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <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()
|
||||||
|
{
|
||||||
|
if (Client.Session is null)
|
||||||
|
{
|
||||||
|
// Already synced, because the client isn't connected so we have nothing *to* sync.
|
||||||
|
// Run a tick on server.
|
||||||
|
await Server.WaitRunTicks(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sGameTiming = Server.Timing;
|
||||||
|
var cGameTiming = (IClientGameTiming)Client.Timing;
|
||||||
|
var startTime = sGameTiming.CurTick;
|
||||||
|
|
||||||
|
while (cGameTiming.LastRealTick < startTime)
|
||||||
|
{
|
||||||
|
await RunTicksSync(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the server-client pair in sync, but also ensures they are both idle each tick.
|
/// Runs the server-client pair in sync, but also ensures they are both idle each tick.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NUnit.Framework.Internal;
|
||||||
using Robust.Client;
|
using Robust.Client;
|
||||||
using Robust.Shared;
|
using Robust.Shared;
|
||||||
using Robust.Shared.Exceptions;
|
using Robust.Shared.Exceptions;
|
||||||
@@ -15,6 +18,22 @@ namespace Robust.UnitTesting.Pool;
|
|||||||
// This partial file contains logic related to recycling & disposing test pairs.
|
// This partial file contains logic related to recycling & disposing test pairs.
|
||||||
public partial class TestPair<TServer, TClient>
|
public partial class TestPair<TServer, TClient>
|
||||||
{
|
{
|
||||||
|
private void ReportErrorLogs()
|
||||||
|
{
|
||||||
|
using (Assert.EnterMultipleScope())
|
||||||
|
{
|
||||||
|
if (ServerLogHandler.FailingLogs.Count == 1)
|
||||||
|
Assert.Fail(ServerLogHandler.FailingLogs[0]);
|
||||||
|
else if (ServerLogHandler.FailingLogs.Count > 1)
|
||||||
|
Assert.Fail("Server had multiple failing logs reported, consult the game log.");
|
||||||
|
|
||||||
|
if (ClientLogHandler.FailingLogs.Count == 1)
|
||||||
|
Assert.Fail(ClientLogHandler.FailingLogs[0]);
|
||||||
|
else if (ClientLogHandler.FailingLogs.Count > 1)
|
||||||
|
Assert.Fail("Client had multiple failing logs reported, consult the game log.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnDirtyDispose()
|
private async Task OnDirtyDispose()
|
||||||
{
|
{
|
||||||
var usageTime = Watch.Elapsed;
|
var usageTime = Watch.Elapsed;
|
||||||
@@ -23,6 +42,7 @@ public partial class TestPair<TServer, TClient>
|
|||||||
Kill();
|
Kill();
|
||||||
var disposeTime = Watch.Elapsed;
|
var disposeTime = Watch.Elapsed;
|
||||||
await TestOut.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Id} in {disposeTime.TotalMilliseconds} ms");
|
await TestOut.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Id} in {disposeTime.TotalMilliseconds} ms");
|
||||||
|
ReportErrorLogs();
|
||||||
// Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
|
// Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
|
||||||
// because someone forgot to clean-return the pair.
|
// because someone forgot to clean-return the pair.
|
||||||
Assert.Warn("Test was dirty-disposed.");
|
Assert.Warn("Test was dirty-disposed.");
|
||||||
@@ -71,18 +91,32 @@ public partial class TestPair<TServer, TClient>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sRuntimeLog = Server.Resolve<IRuntimeLog>();
|
try
|
||||||
if (sRuntimeLog.ExceptionCount > 0)
|
{
|
||||||
throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
|
var sRuntimeLog = Server.Resolve<IRuntimeLog>();
|
||||||
var cRuntimeLog = Client.Resolve<IRuntimeLog>();
|
if (sRuntimeLog.ExceptionCount > 0)
|
||||||
if (cRuntimeLog.ExceptionCount > 0)
|
throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
|
||||||
throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
|
var cRuntimeLog = Client.Resolve<IRuntimeLog>();
|
||||||
|
if (cRuntimeLog.ExceptionCount > 0)
|
||||||
|
throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
|
||||||
|
|
||||||
|
ReportErrorLogs();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
Kill();
|
||||||
|
await ReallyBeIdle();
|
||||||
|
await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Dirty disposed in {Watch.Elapsed.TotalMilliseconds} ms");
|
||||||
|
Assert.Warn("Test was dirty-disposed.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
var returnTime = Watch.Elapsed;
|
var returnTime = Watch.Elapsed;
|
||||||
await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
|
await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
|
||||||
State = PairState.Ready;
|
State = PairState.Ready;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HandlesResourceDisposal]
|
||||||
public async ValueTask CleanReturnAsync()
|
public async ValueTask CleanReturnAsync()
|
||||||
{
|
{
|
||||||
if (State != PairState.InUse)
|
if (State != PairState.InUse)
|
||||||
@@ -90,10 +124,16 @@ public partial class TestPair<TServer, TClient>
|
|||||||
|
|
||||||
await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started");
|
await TestOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started");
|
||||||
State = PairState.CleanDisposed;
|
State = PairState.CleanDisposed;
|
||||||
await OnCleanDispose();
|
try
|
||||||
DebugTools.Assert(State is PairState.Dead or PairState.Ready);
|
{
|
||||||
Manager.Return(this);
|
await OnCleanDispose();
|
||||||
ClearContext();
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DebugTools.Assert(State is PairState.Dead or PairState.Ready);
|
||||||
|
ClearContext();
|
||||||
|
Manager.Return(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
@@ -105,9 +145,15 @@ public partial class TestPair<TServer, TClient>
|
|||||||
break;
|
break;
|
||||||
case PairState.InUse:
|
case PairState.InUse:
|
||||||
await TestOut.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Id} started");
|
await TestOut.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Id} started");
|
||||||
await OnDirtyDispose();
|
try
|
||||||
Manager.Return(this);
|
{
|
||||||
ClearContext();
|
await OnDirtyDispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ClearContext();
|
||||||
|
Manager.Return(this);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Exception($"{nameof(DisposeAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
|
throw new Exception($"{nameof(DisposeAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
@@ -23,8 +25,11 @@ public abstract partial class TestPair<TServer, TClient> : ITestPair, IAsyncDisp
|
|||||||
public PairState State { get; private set; } = PairState.Ready;
|
public PairState State { get; private set; } = PairState.Ready;
|
||||||
public bool Initialized { get; private set; }
|
public bool Initialized { get; private set; }
|
||||||
protected TextWriter TestOut = default!;
|
protected TextWriter TestOut = default!;
|
||||||
|
protected TextWriter? Gravestone = default!;
|
||||||
public Stopwatch Watch { get; } = new();
|
public Stopwatch Watch { get; } = new();
|
||||||
public List<string> TestHistory { get; } = new();
|
|
||||||
|
private List<TestHistoryEntry> _testHistory = new();
|
||||||
|
public IReadOnlyList<TestHistoryEntry> ExtendedTestHistory => _testHistory;
|
||||||
public PairSettings Settings { get; set; } = default!;
|
public PairSettings Settings { get; set; } = default!;
|
||||||
|
|
||||||
public readonly PoolTestLogHandler ServerLogHandler = new("SERVER");
|
public readonly PoolTestLogHandler ServerLogHandler = new("SERVER");
|
||||||
@@ -57,7 +62,8 @@ public abstract partial class TestPair<TServer, TClient> : ITestPair, IAsyncDisp
|
|||||||
int id,
|
int id,
|
||||||
BasePoolManager manager,
|
BasePoolManager manager,
|
||||||
PairSettings settings,
|
PairSettings settings,
|
||||||
TextWriter testOut)
|
TextWriter testOut,
|
||||||
|
TextWriter? gravestone)
|
||||||
{
|
{
|
||||||
if (Initialized)
|
if (Initialized)
|
||||||
throw new InvalidOperationException("Already initialized");
|
throw new InvalidOperationException("Already initialized");
|
||||||
@@ -66,6 +72,10 @@ public abstract partial class TestPair<TServer, TClient> : ITestPair, IAsyncDisp
|
|||||||
Manager = manager;
|
Manager = manager;
|
||||||
Settings = settings;
|
Settings = settings;
|
||||||
Initialized = true;
|
Initialized = true;
|
||||||
|
Gravestone = gravestone;
|
||||||
|
|
||||||
|
if (Gravestone is not null)
|
||||||
|
await Gravestone.WriteLineAsync("Test pair initialized.");
|
||||||
|
|
||||||
ClientLogHandler.ActivateContext(testOut);
|
ClientLogHandler.ActivateContext(testOut);
|
||||||
ServerLogHandler.ActivateContext(testOut);
|
ServerLogHandler.ActivateContext(testOut);
|
||||||
@@ -112,6 +122,10 @@ public abstract partial class TestPair<TServer, TClient> : ITestPair, IAsyncDisp
|
|||||||
ClientLogHandler.ShuttingDown = true;
|
ClientLogHandler.ShuttingDown = true;
|
||||||
Server.Dispose();
|
Server.Dispose();
|
||||||
Client.Dispose();
|
Client.Dispose();
|
||||||
|
|
||||||
|
Gravestone?.WriteLine("Test pair killed.");
|
||||||
|
Gravestone?.WriteLine(Environment.StackTrace);
|
||||||
|
Gravestone?.Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearContext()
|
private void ClearContext()
|
||||||
@@ -123,6 +137,9 @@ public abstract partial class TestPair<TServer, TClient> : ITestPair, IAsyncDisp
|
|||||||
|
|
||||||
public void ActivateContext(TextWriter testOut)
|
public void ActivateContext(TextWriter testOut)
|
||||||
{
|
{
|
||||||
|
// This is the very, very first thing that happens in prepping a pair, so wait a sec
|
||||||
|
// for disposal to get to finish if we got returned right this instant.
|
||||||
|
// Not necessary after some of the disposal changes but defensive is good.
|
||||||
TestOut = testOut;
|
TestOut = testOut;
|
||||||
ServerLogHandler.ActivateContext(testOut);
|
ServerLogHandler.ActivateContext(testOut);
|
||||||
ClientLogHandler.ActivateContext(testOut);
|
ClientLogHandler.ActivateContext(testOut);
|
||||||
@@ -132,9 +149,22 @@ public abstract partial class TestPair<TServer, TClient> : ITestPair, IAsyncDisp
|
|||||||
{
|
{
|
||||||
if (State != PairState.Ready)
|
if (State != PairState.Ready)
|
||||||
throw new InvalidOperationException($"Pair is not ready to use. State: {State}");
|
throw new InvalidOperationException($"Pair is not ready to use. State: {State}");
|
||||||
|
|
||||||
State = PairState.InUse;
|
State = PairState.InUse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task AddToHistory(string testName)
|
||||||
|
{
|
||||||
|
var memUse = GC.GetTotalMemory(false);
|
||||||
|
var entry = new TestHistoryEntry(testName, memUse);
|
||||||
|
_testHistory.Add(entry);
|
||||||
|
if (Gravestone is not null)
|
||||||
|
{
|
||||||
|
await Gravestone.WriteLineAsync($"#{_testHistory.Count}: {entry}");
|
||||||
|
await Gravestone.FlushAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void SetupSeed()
|
public void SetupSeed()
|
||||||
{
|
{
|
||||||
var sRand = Server.Resolve<IRobustRandom>();
|
var sRand = Server.Resolve<IRobustRandom>();
|
||||||
|
|||||||
Reference in New Issue
Block a user