mirror of
https://github.com/roflmuffin/CounterStrikeSharp.git
synced 2025-12-05 23:58:24 -08:00
test: add integration tests xunit plugin/runner (#928)
This commit is contained in:
14
.vscode/tasks.json
vendored
14
.vscode/tasks.json
vendored
@@ -6,10 +6,11 @@
|
||||
{
|
||||
"label": "sync-linux",
|
||||
"type": "shell",
|
||||
"command": "lftp -c \"open -u $LINUX_SERVER_SFTP_USERNAME,$LINUX_SERVER_SFTP_PASSWORD $LINUX_SERVER_SFTP_HOST; mirror -R ${workspaceFolder}/build/addons /game/csgo/addons; mirror -R ${workspaceFolder}/managed/CounterStrikeSharp.API/bin/Release/net8.0/ /game/csgo/addons/counterstrikesharp/api\"",
|
||||
"command": "lftp -c \"open -u $LINUX_SERVER_SFTP_USERNAME,$LINUX_SERVER_SFTP_PASSWORD $LINUX_SERVER_SFTP_HOST; mirror -R ${workspaceFolder}/build/addons /game/csgo/addons; mirror -R ${workspaceFolder}/managed/CounterStrikeSharp.API/bin/Release/net8.0/ /game/csgo/addons/counterstrikesharp/api; mirror -R ${workspaceFolder}/managed/CounterStrikeSharp.Tests.Native/bin/Debug/net8.0/ /game/csgo/addons/counterstrikesharp/plugins/NativeTestsPlugin\"",
|
||||
"dependsOn": [
|
||||
"build",
|
||||
"build-api"
|
||||
"build-api",
|
||||
"build-test-plugin"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
@@ -28,6 +29,15 @@
|
||||
"cwd": "${workspaceFolder}/managed/CounterStrikeSharp.API"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "build-test-plugin",
|
||||
"type": "shell",
|
||||
"group": "build",
|
||||
"command": "dotnet build -c Debug",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/managed/CounterStrikeSharp.Tests.Native"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "generate-schema",
|
||||
"type": "shell",
|
||||
|
||||
46
managed/CounterStrikeSharp.Tests.Native/CommandTests.cs
Normal file
46
managed/CounterStrikeSharp.Tests.Native/CommandTests.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace NativeTestsPlugin;
|
||||
|
||||
public class CommandTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AddCommandHandler()
|
||||
{
|
||||
var mock = new Mock<Action>();
|
||||
var methodCallback = FunctionReference.Create(() =>
|
||||
{
|
||||
mock.Object.Invoke();
|
||||
});
|
||||
|
||||
NativeAPI.AddCommand("test_native", "description", true, (int)ConCommandFlags.FCVAR_LINKED_CONCOMMAND, methodCallback);
|
||||
NativeAPI.IssueServerCommand("test_native");
|
||||
await WaitOneFrame();
|
||||
mock.Verify(s => s(), Times.Once);
|
||||
|
||||
NativeAPI.RemoveCommand("test_native", methodCallback);
|
||||
NativeAPI.IssueServerCommand("test_native");
|
||||
await WaitOneFrame();
|
||||
mock.Verify(s => s(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IssueServerCommand()
|
||||
{
|
||||
bool called = false;
|
||||
NativeAPI.AddCommandListener("say", FunctionReference.Create(() =>
|
||||
{
|
||||
called = true;
|
||||
}), true);
|
||||
|
||||
NativeAPI.IssueServerCommand("say Hello, world!");
|
||||
await WaitOneFrame();
|
||||
|
||||
Assert.True(called, "The 'say' command handler was not called.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Spectre.Console;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace NativeTestsPlugin;
|
||||
|
||||
public class ConsoleTestReporterSink : LongLivedMarshalByRefObject, IMessageSink, IDisposable
|
||||
{
|
||||
public TaskCompletionSource<bool> Finished { get; } = new();
|
||||
|
||||
private int _passed = 0;
|
||||
private int _failed = 0;
|
||||
private int _skipped = 0;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public bool OnMessage(IMessageSinkMessage message)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
// A test has passed
|
||||
case ITestPassed passed:
|
||||
Interlocked.Increment(ref _passed);
|
||||
AnsiConsole.MarkupLineInterpolated($"[underline green][[PASS]][/] [green]{passed.Test.DisplayName}[/]");
|
||||
break;
|
||||
|
||||
// A test has failed
|
||||
case ITestFailed failed:
|
||||
Interlocked.Increment(ref _failed);
|
||||
AnsiConsole.MarkupLineInterpolated($"[underline red][[FAIL]][/] [red]{failed.Test.DisplayName}[/]");
|
||||
AnsiConsole.WriteLine($"\tReason: {failed.ExceptionTypes[0]} - {failed.Messages[0]}");
|
||||
AnsiConsole.WriteLine(IndentStackTrace(failed.StackTraces[0] ?? "No stack trace available."));
|
||||
break;
|
||||
|
||||
// A test was skipped (e.g., using [Fact(Skip = "...")])
|
||||
case ITestSkipped skipped:
|
||||
Interlocked.Increment(ref _skipped);
|
||||
AnsiConsole.MarkupLineInterpolated($"[underline yellow][[SKIP]][/] [yellow]{skipped.Test.DisplayName}[/]");
|
||||
AnsiConsole.MarkupLineInterpolated($"[yellow]\tReason: {skipped.Reason}[/]");
|
||||
break;
|
||||
|
||||
// This message indicates the entire test run for the assembly is complete.
|
||||
case ITestAssemblyFinished:
|
||||
// We signal the main thread that it can stop waiting now.
|
||||
Finished.SetResult(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public string GetSummary()
|
||||
{
|
||||
return $"Summary: {_passed} Passed, {_failed} Failed, {_skipped} Skipped.";
|
||||
}
|
||||
|
||||
private static string IndentStackTrace(string stackTrace)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
var lines = stackTrace.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
builder.AppendLine($" {line}");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Finished.TrySetResult(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
28
managed/CounterStrikeSharp.Tests.Native/EntityTests.cs
Normal file
28
managed/CounterStrikeSharp.Tests.Native/EntityTests.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace NativeTestsPlugin;
|
||||
|
||||
public class EntityTests
|
||||
{
|
||||
[Fact]
|
||||
public void FindEntityAndAccessSchemaMembers()
|
||||
{
|
||||
var world = Utilities.FindAllEntitiesByDesignerName<CWorld>("worldent").FirstOrDefault();
|
||||
|
||||
Assert.NotNull(world);
|
||||
Assert.Equal("worldent", world.DesignerName);
|
||||
Assert.Equal((uint)0, world.Index);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.Equal(0, world.AbsOrigin.X);
|
||||
Assert.Equal(0, world.AbsOrigin.Y);
|
||||
Assert.Equal(0, world.AbsOrigin.Z);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
39
managed/CounterStrikeSharp.Tests.Native/GameEventTests.cs
Normal file
39
managed/CounterStrikeSharp.Tests.Native/GameEventTests.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Threading.Tasks;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace NativeTestsPlugin;
|
||||
|
||||
public class GameEventTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CanRegisterAndDeregisterEventHandlers()
|
||||
{
|
||||
int callCount = 0;
|
||||
var callback = FunctionReference.Create((EventPlayerConnect @event) =>
|
||||
{
|
||||
Assert.NotNull(@event);
|
||||
Assert.NotEmpty(@event.Name);
|
||||
Assert.True(@event.Bot);
|
||||
callCount++;
|
||||
});
|
||||
|
||||
NativeAPI.HookEvent("player_connect", callback, true);
|
||||
|
||||
// Test hooking
|
||||
NativeAPI.IssueServerCommand("bot_kick");
|
||||
NativeAPI.IssueServerCommand("bot_add");
|
||||
await WaitOneFrame();
|
||||
|
||||
Assert.Equal(1, callCount);
|
||||
NativeAPI.UnhookEvent("player_connect", callback, true);
|
||||
|
||||
// Test unhooking
|
||||
NativeAPI.IssueServerCommand("bot_kick");
|
||||
NativeAPI.IssueServerCommand("bot_add");
|
||||
await WaitOneFrame();
|
||||
Assert.Equal(1, callCount);
|
||||
}
|
||||
}
|
||||
|
||||
2
managed/CounterStrikeSharp.Tests.Native/GlobalUsings.cs
Normal file
2
managed/CounterStrikeSharp.Tests.Native/GlobalUsings.cs
Normal file
@@ -0,0 +1,2 @@
|
||||
global using static TestUtils;
|
||||
global using System;
|
||||
36
managed/CounterStrikeSharp.Tests.Native/ListenerTests.cs
Normal file
36
managed/CounterStrikeSharp.Tests.Native/ListenerTests.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Threading.Tasks;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Xunit;
|
||||
|
||||
public class ListenerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CanRegisterAndDeregisterListeners()
|
||||
{
|
||||
int callCount = 0;
|
||||
var callback = FunctionReference.Create((int playerSlot, string name, string ipAddress) =>
|
||||
{
|
||||
|
||||
Assert.NotNull(ipAddress);
|
||||
Assert.NotEmpty(name);
|
||||
Assert.Equal("127.0.0.1", ipAddress);
|
||||
callCount++;
|
||||
});
|
||||
|
||||
NativeAPI.AddListener("OnClientConnect", callback);
|
||||
|
||||
// Test hooking
|
||||
NativeAPI.IssueServerCommand("bot_kick");
|
||||
NativeAPI.IssueServerCommand("bot_add");
|
||||
await WaitOneFrame();
|
||||
|
||||
Assert.Equal(1, callCount);
|
||||
NativeAPI.RemoveListener("OnClientConnect", callback);
|
||||
|
||||
// Test unhooking
|
||||
NativeAPI.IssueServerCommand("bot_kick");
|
||||
NativeAPI.IssueServerCommand("bot_add");
|
||||
await WaitOneFrame();
|
||||
Assert.Equal(1, callCount);
|
||||
}
|
||||
}
|
||||
100
managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs
Normal file
100
managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* This file is part of CounterStrikeSharp.
|
||||
* CounterStrikeSharp is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* CounterStrikeSharp is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with CounterStrikeSharp. If not, see <https://www.gnu.org/licenses/>. *
|
||||
*/
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Xunit;
|
||||
|
||||
|
||||
namespace NativeTestsPlugin
|
||||
{
|
||||
public class NativeTestsPlugin : BasePlugin
|
||||
{
|
||||
public override string ModuleName => "Native Tests";
|
||||
public override string ModuleVersion => "v1.0.0";
|
||||
|
||||
public override string ModuleAuthor => "Roflmuffin";
|
||||
|
||||
public override string ModuleDescription => "A an automated test plugin.";
|
||||
|
||||
private int gameThreadId;
|
||||
|
||||
public override void Load(bool hotReload)
|
||||
{
|
||||
gameThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
// Loading blocks the game thread, so we use NextFrame to run our tests asynchronously.
|
||||
Server.NextFrame(() => RunTests());
|
||||
}
|
||||
|
||||
async Task RunTests()
|
||||
{
|
||||
Console.WriteLine("*****************************************************************");
|
||||
Console.WriteLine($"[{ModuleName}] Starting xUnit test run...");
|
||||
Console.WriteLine("*****************************************************************");
|
||||
|
||||
try
|
||||
{
|
||||
using var reporter = new ConsoleTestReporterSink();
|
||||
|
||||
var project = new XunitProject();
|
||||
using var controller = new XunitFrontController(AppDomainSupport.IfAvailable, this.ModulePath);
|
||||
|
||||
var executionOptions = TestFrameworkOptions.ForExecution();
|
||||
executionOptions.SetDisableParallelization(true);
|
||||
executionOptions.SetMaxParallelThreads(1);
|
||||
executionOptions.SetSynchronousMessageReporting(true);
|
||||
SynchronizationContext.SetSynchronizationContext(new SourceSynchronizationContext(gameThreadId));
|
||||
|
||||
controller.RunAll(reporter, TestFrameworkOptions.ForDiscovery(), executionOptions);
|
||||
|
||||
await reporter.Finished.Task;
|
||||
Console.WriteLine("*****************************************************************");
|
||||
Console.WriteLine($"[{ModuleName}] Test run finished.");
|
||||
Console.WriteLine(reporter.GetSummary());
|
||||
Console.WriteLine("*****************************************************************");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"[{ModuleName}] A critical error occurred during the test run setup: {ex.Message}");
|
||||
Console.WriteLine(ex.StackTrace);
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SourceSynchronizationContext : SynchronizationContext
|
||||
{
|
||||
private readonly int _mainThreadId;
|
||||
public SourceSynchronizationContext(int mainThreadId)
|
||||
{
|
||||
_mainThreadId = mainThreadId;
|
||||
}
|
||||
|
||||
public override void Post(SendOrPostCallback d, object state)
|
||||
{
|
||||
Server.NextWorldUpdate(() => d(state));
|
||||
}
|
||||
|
||||
public override SynchronizationContext CreateCopy()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Platforms>AnyCPU;x86</Platforms>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CounterStrikeSharp.API\CounterStrikeSharp.API.csproj">
|
||||
<Private>false</Private>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="moq" Version="4.20.72" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.50.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.utility" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.extensibility.core" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.assert" Version="2.9.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
2
managed/CounterStrikeSharp.Tests.Native/README.md
Normal file
2
managed/CounterStrikeSharp.Tests.Native/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Native Tests
|
||||
This plugin is intended to be ran inside a running CS2 server running a version of CS# and runs tests against the exposed `NativeAPI` methods.
|
||||
10
managed/CounterStrikeSharp.Tests.Native/TestUtils.cs
Normal file
10
managed/CounterStrikeSharp.Tests.Native/TestUtils.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using CounterStrikeSharp.API;
|
||||
|
||||
public static class TestUtils
|
||||
{
|
||||
public static async Task WaitOneFrame()
|
||||
{
|
||||
await Server.RunOnTickAsync(Server.TickCount + 2, () => { }).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithUserMessages", "..\exam
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithCheckTransmit", "..\examples\WithCheckTransmit\WithCheckTransmit.csproj", "{854B06B5-0E7B-438A-BA51-3F299557F884}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeTestsPlugin", "CounterStrikeSharp.Tests.Native\NativeTestsPlugin.csproj", "{317D3A98-D5C6-40BC-9234-CDAFC033ED0F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -137,6 +139,10 @@ Global
|
||||
{854B06B5-0E7B-438A-BA51-3F299557F884}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{854B06B5-0E7B-438A-BA51-3F299557F884}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{854B06B5-0E7B-438A-BA51-3F299557F884}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{317D3A98-D5C6-40BC-9234-CDAFC033ED0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{317D3A98-D5C6-40BC-9234-CDAFC033ED0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{317D3A98-D5C6-40BC-9234-CDAFC033ED0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{317D3A98-D5C6-40BC-9234-CDAFC033ED0F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user