mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
addcorvaxtests
This commit is contained in:
parent
03e4fc958c
commit
2dcd55d9b8
21 changed files with 2029 additions and 26 deletions
79
.github/workflows/build-test-corvax.yml
vendored
Normal file
79
.github/workflows/build-test-corvax.yml
vendored
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
name: Build & Test Corvax
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, staging, stable ]
|
||||
merge_group:
|
||||
pull_request:
|
||||
types: [ opened, reopened, synchronize, ready_for_review ]
|
||||
branches: [ master, staging, stable ]
|
||||
paths:
|
||||
- 'Corvax/**'
|
||||
- 'Content.Client/**'
|
||||
- 'Content.Server/**'
|
||||
- 'Content.Shared/**'
|
||||
- 'Content.IntegrationTests/**'
|
||||
- 'Content.Tests/**'
|
||||
- 'Resources/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name != 'merge_group' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build & Test Corvax
|
||||
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 50
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Submodule
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Pull engine updates
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
|
||||
- name: Update Engine Submodules
|
||||
run: |
|
||||
cd RobustToolbox/
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build Project
|
||||
run: dotnet build --configuration DebugOpt --no-restore /m
|
||||
|
||||
- name: Run Content.Corvax.Tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:DOTNET_gcServer=1
|
||||
dotnet test --no-build --configuration DebugOpt Corvax/Content.Corvax.Tests/Content.Corvax.Tests.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-corvax-results
|
||||
path: test_results/*
|
||||
retention-days: 7
|
||||
compression-level: 9
|
||||
|
||||
ci-success:
|
||||
name: Build & Test Corvax
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: CI succeeded
|
||||
run: exit 0
|
||||
|
|
@ -29,8 +29,37 @@ using Robust.Shared.Prototypes;
|
|||
using Robust.Shared.Utility;
|
||||
namespace Content.IntegrationTests.Tests
|
||||
{
|
||||
// Corvax-Tests-start
|
||||
[TestFixture]
|
||||
public sealed class PostMapInitTest : GameTest
|
||||
public sealed class PostMapInitTest : PostMapInitTestBase
|
||||
{
|
||||
// ReSharper disable UseCollectionExpression
|
||||
private const string ExcludeCorvax = "/Maps/Corvax";
|
||||
|
||||
[Test, TestCaseSource(nameof(GridsSource), new object[] { false, new string[] { ExcludeCorvax } })]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task GridsLoadableTest(string mapFile) => await RunGridsLoadableTest(mapFile);
|
||||
|
||||
[Test, TestCaseSource(nameof(ShuttleMapFilesSource), new object[] { false, new string[] { ExcludeCorvax } })]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task ShuttlesLoadableTest(ResPath path) => await RunShuttlesLoadableTest(path);
|
||||
|
||||
[Test, TestCaseSource(nameof(AllMapFilesSource), new object[] { false, new string[] { ExcludeCorvax } })]
|
||||
public async Task NoSavedPostMapInitTest(ResPath map) => await RunNoSavedPostMapInitTest(map);
|
||||
|
||||
[Test, TestCaseSource(nameof(GameMapsSource))]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task GameMapsLoadableTest(string mapProto)
|
||||
=> await RunGameMapsLoadableTest(mapProto, customOnly: false, new string[] { ExcludeCorvax });
|
||||
|
||||
[Test, TestCaseSource(nameof(AllMapFilesSource), new object[] { false, new string[] { ExcludeCorvax } })]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task NonGameMapsLoadableTest(ResPath mapPath) => await RunNonGameMapsLoadableTest(mapPath);
|
||||
// ReSharper restore UseCollectionExpression
|
||||
}
|
||||
// Corvax-Tests-end
|
||||
|
||||
public abstract class PostMapInitTestBase : GameTest // Corvax-Tests-Edit
|
||||
{
|
||||
public override PoolSettings PoolSettings => new PoolSettings()
|
||||
{
|
||||
|
|
@ -41,6 +70,63 @@ namespace Content.IntegrationTests.Tests
|
|||
private const bool SkipTestMaps = true;
|
||||
private const string TestMapsPath = "/Maps/Test/";
|
||||
|
||||
// Corvax-Tests-start
|
||||
private static bool MatchesAnyPrefix(string path, string[] prefixes)
|
||||
{
|
||||
foreach (var prefix in prefixes)
|
||||
{
|
||||
if (path.StartsWith(prefix, StringComparison.Ordinal))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool MatchesAnyPrefix(ResPath path, string[] prefixes)
|
||||
=> MatchesAnyPrefix(path.ToString(), prefixes);
|
||||
|
||||
public static IEnumerable<TestCaseData> GridsSource(bool customOnly, string[] prefixes)
|
||||
{
|
||||
foreach (var map in Grids)
|
||||
{
|
||||
if (MatchesAnyPrefix(map, prefixes) != customOnly)
|
||||
continue;
|
||||
|
||||
yield return new TestCaseData(map).SetName(map);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<TestCaseData> AllMapFilesSource(bool customOnly, string[] prefixes)
|
||||
{
|
||||
foreach (var path in AllMapFiles)
|
||||
{
|
||||
if (MatchesAnyPrefix(path, prefixes) != customOnly)
|
||||
continue;
|
||||
|
||||
yield return new TestCaseData(path).SetName(path.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<TestCaseData> ShuttleMapFilesSource(bool customOnly, string[] prefixes)
|
||||
{
|
||||
foreach (var path in ShuttleMapFiles)
|
||||
{
|
||||
if (MatchesAnyPrefix(path, prefixes) != customOnly)
|
||||
continue;
|
||||
|
||||
yield return new TestCaseData(path).SetName(path.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<TestCaseData> GameMapsSource()
|
||||
{
|
||||
foreach (var id in GameMaps)
|
||||
{
|
||||
yield return new TestCaseData(id).SetName(id);
|
||||
}
|
||||
}
|
||||
// Corvax-Tests-end
|
||||
|
||||
private static readonly string[] NoSpawnMaps =
|
||||
{
|
||||
"CentComm",
|
||||
|
|
@ -50,7 +136,7 @@ namespace Content.IntegrationTests.Tests
|
|||
private static readonly string[] Grids =
|
||||
{
|
||||
"/Maps/centcomm.yml",
|
||||
"/Maps/Corvax/corvax_centcomm.yml", // Corvax edit
|
||||
"/Maps/Corvax/corvax_centcomm.yml", // Corvax-Tests
|
||||
AdminTestArenaSystem.ArenaMapPath
|
||||
};
|
||||
|
||||
|
|
@ -83,7 +169,7 @@ namespace Content.IntegrationTests.Tests
|
|||
{
|
||||
"/Maps/centcomm.yml",
|
||||
"/Maps/Shuttles/AdminSpawn/**", // admin gaming
|
||||
"/Maps/Corvax/corvax_centcomm.yml" // Corvax edit
|
||||
"/Maps/Corvax/corvax_centcomm.yml" // Corvax-Tests
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -95,16 +181,19 @@ namespace Content.IntegrationTests.Tests
|
|||
|
||||
private static readonly string[] GameMaps = GameDataScrounger.PrototypesOfKind<GameMapPrototype>().Where(x => x != PoolManager.TestMap).ToArray();
|
||||
private static readonly ResPath[] AllMapFiles = GameDataScrounger.FilesInDirectoryInVfs("/Maps", "*.yml");
|
||||
private static readonly ResPath[] ShuttleMapFiles = GameDataScrounger.FilesInDirectoryInVfs("/Maps/Shuttles", "*.yml");
|
||||
// Corvax-Tests-start
|
||||
private static readonly ResPath[] ShuttleMapFiles =
|
||||
GameDataScrounger.FilesInDirectoryInVfs("/Maps/Shuttles", "*.yml")
|
||||
.Concat(GameDataScrounger.FilesInDirectoryInVfs("/Maps/Corvax/Shuttles", "*.yml"))
|
||||
.ToArray();
|
||||
// Corvax-Tests-end
|
||||
|
||||
private static readonly ProtoId<EntityCategoryPrototype> DoNotMapCategory = "DoNotMap";
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that specific files have been saved as grids and not maps.
|
||||
/// </summary>
|
||||
[Test, TestCaseSource(nameof(Grids))]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task GridsLoadableTest(string mapFile)
|
||||
protected async Task RunGridsLoadableTest(string mapFile) // Corvax-Tests-Edit
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
|
@ -134,10 +223,7 @@ namespace Content.IntegrationTests.Tests
|
|||
/// <summary>
|
||||
/// Asserts that shuttles are loadable and have been saved as grids and not maps.
|
||||
/// </summary>
|
||||
[Test]
|
||||
[TestCaseSource(nameof(ShuttleMapFiles))]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task ShuttlesLoadableTest(ResPath path)
|
||||
protected async Task RunShuttlesLoadableTest(ResPath path) // Corvax-Tests-Edit
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
|
@ -167,9 +253,7 @@ namespace Content.IntegrationTests.Tests
|
|||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(AllMapFiles))]
|
||||
public async Task NoSavedPostMapInitTest(ResPath map)
|
||||
protected async Task RunNoSavedPostMapInitTest(ResPath map) // Corvax-Tests-Edit
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
|
@ -204,7 +288,7 @@ namespace Content.IntegrationTests.Tests
|
|||
|
||||
// TODO MAP TESTS
|
||||
// Move this to some separate test?
|
||||
//CheckDoNotMap(map, root, protoManager); // Corvax-Changes
|
||||
//CheckDoNotMap(map, root, protoManager); // Corvax-Tests
|
||||
|
||||
if (version >= 7)
|
||||
{
|
||||
|
|
@ -233,7 +317,7 @@ namespace Content.IntegrationTests.Tests
|
|||
await server.WaitPost(() => server.EntMan.Spawn(null, new MapCoordinates(0, 0, id)));
|
||||
|
||||
// First check that a pre-init version passes
|
||||
var path = new ResPath($"{nameof(NoSavedPostMapInitTest)}.yml");
|
||||
var path = new ResPath($"{nameof(RunNoSavedPostMapInitTest)}.yml"); // Corvax-Tests-Edit
|
||||
Assert.That(loader.TrySaveMap(id, path));
|
||||
Assert.That(IsPreInit(path, loader, deps, ev.RenamedPrototypes, ev.DeletedPrototypes));
|
||||
|
||||
|
|
@ -324,9 +408,7 @@ namespace Content.IntegrationTests.Tests
|
|||
return true;
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(GameMaps))]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task GameMapsLoadableTest(string mapProto)
|
||||
protected async Task RunGameMapsLoadableTest(string mapProto, bool customOnly, string[] prefixes) // Corvax-Tests-Edit
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
|
@ -339,13 +421,19 @@ namespace Content.IntegrationTests.Tests
|
|||
var shuttleSystem = entManager.EntitySysManager.GetEntitySystem<ShuttleSystem>();
|
||||
var cfg = server.ResolveDependency<IConfigurationManager>();
|
||||
|
||||
// Corvax-Tests-start
|
||||
var gameMapProto = protoManager.Index<GameMapPrototype>(mapProto);
|
||||
if (MatchesAnyPrefix(gameMapProto.MapPath, prefixes) != customOnly)
|
||||
return;
|
||||
// Corvax-Tests-end
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
MapId mapId;
|
||||
try
|
||||
{
|
||||
var opts = DeserializationOptions.Default with { InitializeMaps = true };
|
||||
ticker.LoadGameMap(protoManager.Index<GameMapPrototype>(mapProto), out mapId, opts);
|
||||
ticker.LoadGameMap(gameMapProto, out mapId, opts); // Corvax-Tests-Edit
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -464,10 +552,7 @@ namespace Content.IntegrationTests.Tests
|
|||
return resultCount;
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(AllMapFiles))]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task NonGameMapsLoadableTest(ResPath mapPath)
|
||||
protected async Task RunNonGameMapsLoadableTest(ResPath mapPath) // Corvax-Tests-Edit
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
|
|
|||
|
|
@ -270,8 +270,12 @@ public static class NumberConverter
|
|||
|
||||
private static void AppendWithSpace(StringBuilder stringBuilder, string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str))
|
||||
return;
|
||||
|
||||
if (stringBuilder.Length > 0)
|
||||
stringBuilder.Append(" ");
|
||||
|
||||
stringBuilder.Append(str);
|
||||
}
|
||||
|
||||
|
|
|
|||
1
Corvax/Content.Corvax.Tests/AssemblyInfo.cs
Normal file
1
Corvax/Content.Corvax.Tests/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1 @@
|
|||
[assembly: Parallelizable(ParallelScope.Fixtures)]
|
||||
34
Corvax/Content.Corvax.Tests/Content.Corvax.Tests.csproj
Normal file
34
Corvax/Content.Corvax.Tests/Content.Corvax.Tests.csproj
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputPath>..\..\bin\Content.Corvax.Tests\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<Nullable>disable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AssemblyName>Content.Corvax.Tests</AssemblyName>
|
||||
<RootNamespace>Content.Corvax.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\..\MSBuild\Content.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NUnit" />
|
||||
<PackageReference Include="NUnit3TestAdapter" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Content.Client\Content.Client.csproj" />
|
||||
<ProjectReference Include="..\..\Content.Server\Content.Server.csproj" />
|
||||
<ProjectReference Include="..\..\Content.Shared\Content.Shared.csproj" />
|
||||
<ProjectReference Include="..\..\Content.Tests\Content.Tests.csproj" />
|
||||
<ProjectReference Include="..\..\Content.IntegrationTests\Content.IntegrationTests.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\..\RobustToolbox\MSBuild\Robust.Properties.targets" />
|
||||
<Import Project="..\..\RobustToolbox\MSBuild\Robust.CompNetworkGenerator.targets" />
|
||||
|
||||
<Import Project="..\..\RobustToolbox\Imports\Client.props" />
|
||||
<Import Project="..\..\RobustToolbox\Imports\Server.props" />
|
||||
<Import Project="..\..\RobustToolbox\Imports\Shared.props" />
|
||||
<Import Project="..\..\RobustToolbox\Imports\Testing.props" />
|
||||
</Project>
|
||||
6
Corvax/Content.Corvax.Tests/GlobalUsings.cs
Normal file
6
Corvax/Content.Corvax.Tests/GlobalUsings.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Global usings for Content.Corvax.Tests
|
||||
|
||||
global using NUnit.Framework;
|
||||
global using System;
|
||||
global using System.Threading.Tasks;
|
||||
global using Content.IntegrationTests;
|
||||
31
Corvax/Content.Corvax.Tests/PoolManagerTestEventHandler.cs
Normal file
31
Corvax/Content.Corvax.Tests/PoolManagerTestEventHandler.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
namespace Content.Corvax.Tests;
|
||||
|
||||
[SetUpFixture]
|
||||
public sealed class PoolManagerTestEventHandler
|
||||
{
|
||||
private static TimeSpan MaximumTotalTestingTimeLimit => TimeSpan.FromMinutes(30);
|
||||
private static TimeSpan HardStopTimeLimit => MaximumTotalTestingTimeLimit.Add(TimeSpan.FromMinutes(1));
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void Setup()
|
||||
{
|
||||
PoolManager.Startup();
|
||||
_ = Task.Delay(MaximumTotalTestingTimeLimit).ContinueWith(_ =>
|
||||
{
|
||||
TestContext.Error.WriteLine($"\n\n{nameof(PoolManagerTestEventHandler)}: ERROR: Tests are taking too long. Shutting down.\n\n");
|
||||
PoolManager.Shutdown();
|
||||
});
|
||||
|
||||
_ = Task.Delay(HardStopTimeLimit).ContinueWith(_ =>
|
||||
{
|
||||
var deathReport = PoolManager.DeathReport();
|
||||
Environment.FailFast($"Tests took way too long;\n Death Report:\n{deathReport}");
|
||||
});
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
PoolManager.Shutdown();
|
||||
}
|
||||
}
|
||||
130
Corvax/Content.Corvax.Tests/Syndicate/DocumentPrinterTest.cs
Normal file
130
Corvax/Content.Corvax.Tests/Syndicate/DocumentPrinterTest.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
using Content.Server.Corvax.Documents;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.Shared.Corvax.Documents;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class DocumentPrinterTest : GameTest
|
||||
{
|
||||
[Test]
|
||||
public async Task ReplacesAllPlaceholders()
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
||||
var docSystem = server.System<DocumentPrinterSystem>();
|
||||
var loc = server.ResolveDependency<ILocalizationManager>();
|
||||
|
||||
// Build content using the actual localized variable tokens, so the test
|
||||
// doesn't hardcode the "$date" / "$station" / etc. symbol format.
|
||||
var dateVar = loc.GetString("doc-var-date");
|
||||
var stationVar = loc.GetString("doc-var-station");
|
||||
var nameVar = loc.GetString("doc-var-name");
|
||||
var jobVar = loc.GetString("doc-var-job");
|
||||
|
||||
var content = $"Date: {dateVar}, Station: {stationVar}, Name: {nameVar}, Job: {jobVar}";
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var formatted = docSystem.FormatString(content, "MyStation");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(formatted, Does.Not.Contain(dateVar), "Date placeholder was not replaced.");
|
||||
Assert.That(formatted, Does.Not.Contain(stationVar), "Station placeholder was not replaced.");
|
||||
Assert.That(formatted, Does.Not.Contain(nameVar), "Name placeholder was not replaced.");
|
||||
Assert.That(formatted, Does.Not.Contain(jobVar), "Job placeholder was not replaced.");
|
||||
|
||||
Assert.That(formatted, Does.Contain("MyStation"), "Station name was not inserted.");
|
||||
Assert.That(formatted, Does.Contain(loc.GetString("doc-text-printer-default-name")),
|
||||
"Default name was not inserted when no ID card is provided.");
|
||||
Assert.That(formatted, Does.Contain(loc.GetString("doc-text-printer-default-job")),
|
||||
"Default job was not inserted when no ID card is provided.");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UsesIdCardWhenProvided()
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
||||
var docSystem = server.System<DocumentPrinterSystem>();
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var loc = server.ResolveDependency<ILocalizationManager>();
|
||||
|
||||
var nameVar = loc.GetString("doc-var-name");
|
||||
var jobVar = loc.GetString("doc-var-job");
|
||||
var content = $"Name: {nameVar}, Job: {jobVar}";
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var idCardEntity = entManager.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
var idCard = entManager.EnsureComponent<IdCardComponent>(idCardEntity);
|
||||
idCard.FullName = "John Doe";
|
||||
idCard.LocalizedJobTitle = "Engineer";
|
||||
|
||||
var formatted = docSystem.FormatString(content, "MyStation", idCard);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(formatted, Does.Not.Contain(nameVar), "Name placeholder was not replaced with ID card name.");
|
||||
Assert.That(formatted, Does.Not.Contain(jobVar), "Job placeholder was not replaced with ID card job.");
|
||||
Assert.That(formatted, Does.Contain("John Doe"), "ID card full name was not inserted.");
|
||||
Assert.That(formatted, Does.Contain("Engineer"), "ID card job title was not inserted.");
|
||||
});
|
||||
|
||||
entManager.DeleteEntity(idCardEntity);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UsesDefaultStationWhenNull()
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
||||
var docSystem = server.System<DocumentPrinterSystem>();
|
||||
var loc = server.ResolveDependency<ILocalizationManager>();
|
||||
|
||||
var stationVar = loc.GetString("doc-var-station");
|
||||
var content = $"Station: {stationVar}";
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var formatted = docSystem.FormatString(content, null);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(formatted, Does.Not.Contain(stationVar), "Station placeholder was not replaced when station is null.");
|
||||
Assert.That(formatted, Does.Contain(loc.GetString("doc-text-printer-default-station")),
|
||||
"Default station name was not used when station is null.");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task HasDefaultSlotName()
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var ent = entManager.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
var comp = entManager.EnsureComponent<DocumentPrinterComponent>(ent);
|
||||
|
||||
Assert.That(comp.SlotName, Is.EqualTo("id"), "DocumentPrinterComponent.SlotName should default to \"id\".");
|
||||
|
||||
entManager.DeleteEntity(ent);
|
||||
});
|
||||
}
|
||||
}
|
||||
201
Corvax/Content.Corvax.Tests/Syndicate/GhostGoLobbyTest.cs
Normal file
201
Corvax/Content.Corvax.Tests/Syndicate/GhostGoLobbyTest.cs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.Server.Corvax.Ghost;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class GhostGoLobbyTest : GameTest
|
||||
{
|
||||
private const string TestName = "Oleg";
|
||||
|
||||
[Test]
|
||||
public async Task MarkCharacterTracksUsedCharacter()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var system = server.System<GhostGoLobbySystem>();
|
||||
var profile = MakeProfile(TestName);
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
Assert.That(system.IsCharacterUsed(profile), Is.False, "Character must not be tracked before marking.");
|
||||
|
||||
system.MarkCharacterUsed(profile);
|
||||
|
||||
Assert.That(system.IsCharacterUsed(profile), Is.True, "Character must be tracked after marking.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UntrackedCharacterIsNotReportedAsUsed()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var system = server.System<GhostGoLobbySystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var profile = MakeProfile(TestName);
|
||||
|
||||
Assert.That(system.IsCharacterUsed(profile), Is.False, "Untracked character must not be reported as used.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ProfilesWithSameValuesHashEqually()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var system = server.System<GhostGoLobbySystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var a = MakeProfile(TestName);
|
||||
var b = MakeProfile(TestName);
|
||||
|
||||
system.MarkCharacterUsed(a);
|
||||
|
||||
Assert.That(system.IsCharacterUsed(b), Is.True, "Two profiles with identical Name/Sex/Age/Species must hash equally.");
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(TestName, "Ne" + TestName, TestName = "DifferentNameDoesNotMatch")]
|
||||
[TestCase(TestName, TestName, TestName = "SameNameMatches")]
|
||||
public async Task CharacterMatchingByName(string usedName, string checkName)
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var system = server.System<GhostGoLobbySystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
system.MarkCharacterUsed(MakeProfile(usedName));
|
||||
|
||||
var expected = usedName == checkName;
|
||||
Assert.That(system.IsCharacterUsed(MakeProfile(checkName)), Is.EqualTo(expected),
|
||||
expected
|
||||
? $"Expected '{checkName}' to match the previously used '{usedName}'."
|
||||
: $"Expected '{checkName}' NOT to match the previously used '{usedName}'.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DifferentAgeDoesNotMatch()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var system = server.System<GhostGoLobbySystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var used = MakeProfile(TestName, age: 25);
|
||||
var other = MakeProfile(TestName, age: 40);
|
||||
|
||||
system.MarkCharacterUsed(used);
|
||||
|
||||
Assert.That(system.IsCharacterUsed(other), Is.False, "Profiles with different Age must hash differently.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DifferentSexDoesNotMatch()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var system = server.System<GhostGoLobbySystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var used = MakeProfile(TestName, sex: Sex.Female);
|
||||
var other = MakeProfile(TestName, sex: Sex.Male);
|
||||
|
||||
system.MarkCharacterUsed(used);
|
||||
|
||||
Assert.That(system.IsCharacterUsed(other), Is.False, "Profiles with different Sex must hash differently.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DifferentSpeciesDoesNotMatch()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var system = server.System<GhostGoLobbySystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var used = MakeProfile(TestName, species: "Human");
|
||||
var other = MakeProfile(TestName, species: "Reptilian");
|
||||
|
||||
system.MarkCharacterUsed(used);
|
||||
|
||||
Assert.That(system.IsCharacterUsed(other), Is.False, "Profiles with different Species must hash differently.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PreRoundLobbyClearsUsedCharacters()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var system = server.System<GhostGoLobbySystem>();
|
||||
var profile = MakeProfile(TestName);
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
system.MarkCharacterUsed(profile);
|
||||
Assert.That(system.IsCharacterUsed(profile), Is.True, "Setup sanity: character must be marked before clearing.");
|
||||
});
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
// Simulate a transition into the pre-round lobby.
|
||||
var ev = new GameRunLevelChangedEvent(GameRunLevel.InRound, GameRunLevel.PreRoundLobby);
|
||||
server.EntMan.EventBus.RaiseEvent(EventSource.Local, ev);
|
||||
});
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
Assert.That(system.IsCharacterUsed(profile), Is.False, "Used characters must be cleared when entering the pre-round lobby.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OtherRunLevelsDoNotClearUsedCharacters()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var system = server.System<GhostGoLobbySystem>();
|
||||
var profile = MakeProfile(TestName);
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
system.MarkCharacterUsed(profile);
|
||||
});
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
// Transition into PostRound — must NOT clear the tracked set.
|
||||
var ev = new GameRunLevelChangedEvent(GameRunLevel.InRound, GameRunLevel.PostRound);
|
||||
server.EntMan.EventBus.RaiseEvent(EventSource.Local, ev);
|
||||
});
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
Assert.That(system.IsCharacterUsed(profile), Is.True, "Used characters must only be cleared on PreRoundLobby, not other run levels.");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a minimal profile with a fixed Name/Sex/Age/Species combo.
|
||||
/// Sex and Age are variadic so each test can tweak a single field
|
||||
/// while keeping the rest stable.
|
||||
/// </summary>
|
||||
private static HumanoidCharacterProfile MakeProfile(
|
||||
string name,
|
||||
Sex sex = Sex.Male,
|
||||
int age = 25,
|
||||
string species = "Human")
|
||||
{
|
||||
return new HumanoidCharacterProfile()
|
||||
.WithName(name)
|
||||
.WithSex(sex)
|
||||
.WithAge(age)
|
||||
.WithSpecies(species);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.Server.Corvax.GuideGenerator;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate.Guidebook;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class GuideGeneratorTest : GameTest
|
||||
{
|
||||
private static string RunGenerator(Action<Stream> publish)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
publish(ms);
|
||||
return Encoding.UTF8.GetString(ms.ToArray());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ComponentListJsonIsValid()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var text = RunGenerator(ComponentListGenerator.PublishJson);
|
||||
|
||||
Assert.That(text, Is.Not.Empty);
|
||||
|
||||
using var doc = JsonDocument.Parse(text);
|
||||
Assert.That(doc.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
|
||||
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
Assert.That(prop.Value.ValueKind, Is.EqualTo(JsonValueKind.Array),
|
||||
$"Component '{prop.Name}' must map to an array of entity ids.");
|
||||
Assert.That(prop.Value.GetArrayLength(), Is.GreaterThan(0),
|
||||
$"Component '{prop.Name}' must be attached to at least one entity.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PrototypeListJsonIsValid()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var text = RunGenerator(PrototypeListGenerator.PublishJson);
|
||||
|
||||
Assert.That(text, Is.Not.Empty);
|
||||
|
||||
using var doc = JsonDocument.Parse(text);
|
||||
Assert.That(doc.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
|
||||
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
Assert.That(prop.Value.ValueKind, Is.EqualTo(JsonValueKind.Array),
|
||||
$"Prototype kind '{prop.Name}' must map to an array of ids.");
|
||||
Assert.That(prop.Value.GetArrayLength(), Is.GreaterThan(0),
|
||||
$"Prototype kind '{prop.Name}' must have at least one prototype.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TagJsonIsValid()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var text = RunGenerator(TagJsonGenerator.PublishJson);
|
||||
|
||||
Assert.That(text, Is.Not.Empty);
|
||||
|
||||
using var doc = JsonDocument.Parse(text);
|
||||
Assert.That(doc.RootElement.ValueKind, Is.EqualTo(JsonValueKind.Object));
|
||||
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
Assert.That(prop.Value.ValueKind, Is.EqualTo(JsonValueKind.Array),
|
||||
$"Tag '{prop.Name}' must map to an array of entity ids.");
|
||||
Assert.That(prop.Value.GetArrayLength(), Is.GreaterThan(0),
|
||||
$"Tag '{prop.Name}' must be attached to at least one entity.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.Server.Corvax.GuideGenerator;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Serialization.Markdown.Value;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate.Guidebook;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class TextToolsTest
|
||||
{
|
||||
[TestCase("test", "Test")]
|
||||
[TestCase("a", "A")]
|
||||
[TestCase("hello world", "Hello world")]
|
||||
public void CapitalizeString(string input, string expected)
|
||||
{
|
||||
Assert.That(TextTools.CapitalizeString(input), Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Test", "test")]
|
||||
[TestCase("A", "a")]
|
||||
[TestCase("HELLO WORLD", "hELLO WORLD")]
|
||||
public void DecapitalizeString(string input, string expected)
|
||||
{
|
||||
Assert.That(TextTools.DecapitalizeString(input), Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(" test ", "test")]
|
||||
[TestCase(",test,", "test")]
|
||||
[TestCase(" ,; test ;, ", "test")]
|
||||
public void NormalizeSuffixToken(string input, string expected)
|
||||
{
|
||||
Assert.That(TextTools.NormalizeSuffixToken(input), Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetEditorSuffixFiltersIgnoredTokens()
|
||||
{
|
||||
var ignored = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase) { "do not map" };
|
||||
var result = TextTools.GetEditorSuffix("test, do not map", ignored, TextTools.NormalizeSuffixToken);
|
||||
Assert.That(result, Is.EqualTo("test"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetEditorSuffixLowercasesResult()
|
||||
{
|
||||
var result = TextTools.GetEditorSuffix("Test, OTHER", new HashSet<string>(), TextTools.NormalizeSuffixToken);
|
||||
Assert.That(result, Is.EqualTo("test, other"));
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public sealed class FieldEntryTest
|
||||
{
|
||||
[Test]
|
||||
public void ConvertValueInt()
|
||||
{
|
||||
Assert.That(FieldEntry.ConvertNode(new ValueDataNode("42")), Is.EqualTo(42));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ConvertValueDouble()
|
||||
{
|
||||
Assert.That(FieldEntry.ConvertNode(new ValueDataNode("3.14")), Is.EqualTo(3.14));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ConvertMappingProducesDictionary()
|
||||
{
|
||||
var node = new MappingDataNode
|
||||
{
|
||||
{ "a", new ValueDataNode("1") },
|
||||
{ "b", new ValueDataNode("two") },
|
||||
};
|
||||
|
||||
var result = FieldEntry.ConvertNode(node) as Dictionary<string, object?>;
|
||||
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result!["a"], Is.EqualTo(1));
|
||||
Assert.That(result["b"], Is.EqualTo("two"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ConvertSequenceProducesList()
|
||||
{
|
||||
var node = new SequenceDataNode("a", "b", "c");
|
||||
var result = FieldEntry.ConvertNode(node) as List<object?>;
|
||||
|
||||
Assert.That(result, Is.Not.Null);
|
||||
Assert.That(result!.Count, Is.EqualTo(3));
|
||||
Assert.That(result[0], Is.EqualTo("a"));
|
||||
Assert.That(result[2], Is.EqualTo("c"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeduplicateAgainstDefaultWrapsInDefaultAndIdKeys()
|
||||
{
|
||||
var defaults = new Dictionary<string, object?> { ["a"] = 1, ["b"] = 2 };
|
||||
var values = new Dictionary<string, object?>
|
||||
{
|
||||
["ent1"] = new Dictionary<string, object?> { ["a"] = 1 },
|
||||
};
|
||||
|
||||
var result = FieldEntry.DeduplicateAgainstDefault(defaults, values);
|
||||
|
||||
Assert.That(result, Does.ContainKey(FieldEntry.DefaultField));
|
||||
Assert.That(result[FieldEntry.DefaultField], Is.EqualTo(defaults));
|
||||
Assert.That(result, Does.ContainKey(FieldEntry.PrototypeId));
|
||||
Assert.That(result[FieldEntry.PrototypeId], Is.EqualTo(values));
|
||||
}
|
||||
}
|
||||
113
Corvax/Content.Corvax.Tests/Syndicate/MobIpcTest.cs
Normal file
113
Corvax/Content.Corvax.Tests/Syndicate/MobIpcTest.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.Shared.Corvax.Ipc;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Emp;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Sound.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class MobIpcTest : GameTest
|
||||
{
|
||||
[Test]
|
||||
public async Task EmpPulseDamagesIpc()
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var mapSystem = server.System<SharedMapSystem>();
|
||||
var damageableSystem = server.System<DamageableSystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
mapSystem.CreateMap(out var mapId);
|
||||
var coords = new MapCoordinates(0, 0, mapId);
|
||||
|
||||
var uid = entManager.SpawnEntity("MobIpc", coords);
|
||||
entManager.EnsureComponent<IpcComponent>(uid);
|
||||
|
||||
var before = damageableSystem.GetTotalDamage(uid);
|
||||
|
||||
var ev = new EmpPulseEvent();
|
||||
server.EntMan.EventBus.RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
var after = damageableSystem.GetTotalDamage(uid);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(ev.Affected, Is.True, "IPC must be marked as affected by an EMP pulse.");
|
||||
Assert.That(after, Is.GreaterThan(before), "IPC must take Shock damage from an EMP pulse.");
|
||||
});
|
||||
|
||||
entManager.DeleteEntity(uid);
|
||||
mapSystem.DeleteMap(mapId);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task MobStateCriticalAddsWarningSound()
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var mapSystem = server.System<SharedMapSystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
mapSystem.CreateMap(out var mapId);
|
||||
var coords = new MapCoordinates(0, 0, mapId);
|
||||
|
||||
var uid = entManager.SpawnEntity("MobIpc", coords);
|
||||
entManager.EnsureComponent<IpcComponent>(uid);
|
||||
var mobState = entManager.EnsureComponent<MobStateComponent>(uid);
|
||||
|
||||
Assert.That(entManager.HasComponent<SpamEmitSoundComponent>(uid), Is.False,
|
||||
"Setup sanity: no warning sound before entering Critical.");
|
||||
|
||||
var ev = new MobStateChangedEvent(uid, mobState, MobState.Alive, MobState.Critical);
|
||||
server.EntMan.EventBus.RaiseLocalEvent(uid, ev);
|
||||
|
||||
Assert.That(entManager.HasComponent<SpamEmitSoundComponent>(uid), Is.True,
|
||||
"IPC must emit a warning sound when entering Critical state.");
|
||||
|
||||
entManager.DeleteEntity(uid);
|
||||
mapSystem.DeleteMap(mapId);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task MobStateAliveRemovesWarningSound()
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var mapSystem = server.System<SharedMapSystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
mapSystem.CreateMap(out var mapId);
|
||||
var coords = new MapCoordinates(0, 0, mapId);
|
||||
|
||||
var uid = entManager.SpawnEntity("MobIpc", coords);
|
||||
entManager.EnsureComponent<IpcComponent>(uid);
|
||||
var mobState = entManager.EnsureComponent<MobStateComponent>(uid);
|
||||
entManager.EnsureComponent<SpamEmitSoundComponent>(uid);
|
||||
|
||||
var ev = new MobStateChangedEvent(uid, mobState, MobState.Critical, MobState.Alive);
|
||||
server.EntMan.EventBus.RaiseLocalEvent(uid, ev);
|
||||
|
||||
Assert.That(entManager.HasComponent<SpamEmitSoundComponent>(uid), Is.False,
|
||||
"IPC must stop the warning sound when leaving Critical state.");
|
||||
|
||||
entManager.DeleteEntity(uid);
|
||||
mapSystem.DeleteMap(mapId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// ReSharper disable UseCollectionExpression
|
||||
using Content.IntegrationTests.Fixtures.Attributes;
|
||||
using Content.IntegrationTests.Tests;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate;
|
||||
|
||||
/// <summary>
|
||||
/// Runs PostMapInit tests only for maps from /Maps/Corvax/**.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
public sealed class PostMapInitCorvaxTest : PostMapInitTestBase
|
||||
{
|
||||
private const string IncludedCorvax = "/Maps/Corvax";
|
||||
|
||||
[Test, TestCaseSource(nameof(GridsSource), new object[] { true, new[] { IncludedCorvax } })]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task GridsLoadableTest(string mapFile)
|
||||
{
|
||||
await RunGridsLoadableTest(mapFile);
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(ShuttleMapFilesSource), new object[] { true, new[] { IncludedCorvax } })]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task ShuttlesLoadableTest(ResPath path)
|
||||
{
|
||||
await RunShuttlesLoadableTest(path);
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(AllMapFilesSource), new object[] { true, new[] { IncludedCorvax } })]
|
||||
public async Task NoSavedPostMapInitTest(ResPath map)
|
||||
{
|
||||
await RunNoSavedPostMapInitTest(map);
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(GameMapsSource))]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task GameMapsLoadableTest(string mapProto)
|
||||
{
|
||||
await RunGameMapsLoadableTest(mapProto, customOnly: true, new[] { IncludedCorvax });
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(AllMapFilesSource), new object[] { true, new[] { IncludedCorvax } })]
|
||||
[EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.GridFill), false)]
|
||||
public async Task NonGameMapsLoadableTest(ResPath mapPath)
|
||||
{
|
||||
await RunNonGameMapsLoadableTest(mapPath);
|
||||
}
|
||||
}
|
||||
196
Corvax/Content.Corvax.Tests/Syndicate/SecApartmentDataTest.cs
Normal file
196
Corvax/Content.Corvax.Tests/Syndicate/SecApartmentDataTest.cs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
using System.Linq;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.Server.Corvax.SecApartment;
|
||||
using Content.Shared.SecApartment;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SecApartmentDataTest
|
||||
{
|
||||
[Test]
|
||||
public void SquadDefaultsAreCorrect()
|
||||
{
|
||||
var squad = new Squad("squad_1234", "Alpha Team");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(squad.SquadId, Is.EqualTo("squad_1234"));
|
||||
Assert.That(squad.Name, Is.EqualTo("Alpha Team"));
|
||||
Assert.That(squad.Description, Is.Empty, "Description must start empty.");
|
||||
Assert.That(squad.Members, Is.Empty, "Members must start empty.");
|
||||
Assert.That(squad.Status, Is.EqualTo(SquadStatus.Active), "New squad must be Active.");
|
||||
Assert.That(squad.IconId, Is.EqualTo(SquadIconNum.Alpha), "New squad must use Alpha icon.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CrewMemberInfoPreservesFields()
|
||||
{
|
||||
var owner = new NetEntity(42);
|
||||
var info = new CrewMemberInfo("id1", owner, "John", "Engineer", "JobIconEngineer", null);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(info.MemberId, Is.EqualTo("id1"));
|
||||
Assert.That(info.OwnerUid, Is.EqualTo(owner));
|
||||
Assert.That(info.Name, Is.EqualTo("John"));
|
||||
Assert.That(info.JobTitle, Is.EqualTo("Engineer"));
|
||||
Assert.That(info.JobIcon, Is.EqualTo("JobIconEngineer"));
|
||||
Assert.That(info.SensorStatus, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TimerEntryPreservesFields()
|
||||
{
|
||||
var uid = new NetEntity(7);
|
||||
var entry = new TimerEntry(uid, "Bomb", TimeSpan.FromMinutes(2), TimeSpan.FromMinutes(5));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(entry.TimerUid, Is.EqualTo(uid));
|
||||
Assert.That(entry.Label, Is.EqualTo("Bomb"));
|
||||
Assert.That(entry.RemainingTime, Is.EqualTo(TimeSpan.FromMinutes(2)));
|
||||
Assert.That(entry.TotalTime, Is.EqualTo(TimeSpan.FromMinutes(5)));
|
||||
Assert.That(entry.FinishedAt, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StationDataStartsEmpty()
|
||||
{
|
||||
var data = new StationData();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(data.Squads, Is.Empty);
|
||||
Assert.That(data.TrackedTimers, Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SquadMemberComponentDefaultsToAlphaIcon()
|
||||
{
|
||||
var comp = new SquadMemberComponent();
|
||||
Assert.That(comp.StatusIcon.Id, Is.EqualTo("SecuritySquadIconAlpha"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SecApartmentComponentStationIsNullByDefault()
|
||||
{
|
||||
var comp = new SecApartmentComponent();
|
||||
Assert.That(comp.Station, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SquadIconNumHasTwentyFourEntries()
|
||||
{
|
||||
var values = Enum.GetValues<SquadIconNum>();
|
||||
Assert.That(values.Length, Is.EqualTo(24),
|
||||
"SquadIconNum must contain exactly 24 entries (Alpha..Omega).");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SquadStatusHasActiveAndOnBreak()
|
||||
{
|
||||
var values = Enum.GetValues<SquadStatus>().Cast<SquadStatus>().ToArray();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(values, Does.Contain(SquadStatus.Active));
|
||||
Assert.That(values, Does.Contain(SquadStatus.OnBreak));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SecApartmentPrototypeTest : GameTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Every icon that <c>SecApartmentSystem.GetIconPrototypeId</c> can return
|
||||
/// must exist as a <see cref="StatusIconPrototype"/>. If someone renames
|
||||
/// a squad icon prototype without updating the switch, this test catches it.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task AllSquadIconPrototypesExist()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
var names = new[]
|
||||
{
|
||||
"SecuritySquadIconAlpha",
|
||||
"SecuritySquadIconBeta",
|
||||
"SecuritySquadIconGamma",
|
||||
"SecuritySquadIconDelta",
|
||||
"SecuritySquadIconEpsilon",
|
||||
"SecuritySquadIconZeta",
|
||||
"SecuritySquadIconHeta",
|
||||
"SecuritySquadIconTheta",
|
||||
"SecuritySquadIconIota",
|
||||
"SecuritySquadIconKappa",
|
||||
"SecuritySquadIconLambda",
|
||||
"SecuritySquadIconMu",
|
||||
"SecuritySquadIconNu",
|
||||
"SecuritySquadIconXi",
|
||||
"SecuritySquadIconOmicron",
|
||||
"SecuritySquadIconPi",
|
||||
"SecuritySquadIconRo",
|
||||
"SecuritySquadIconSigma",
|
||||
"SecuritySquadIconTau",
|
||||
"SecuritySquadIconUpsilon",
|
||||
"SecuritySquadIconFi",
|
||||
"SecuritySquadIconHi",
|
||||
"SecuritySquadIconPsi",
|
||||
"SecuritySquadIconOmega",
|
||||
};
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
Assert.That(protoManager.HasIndex<StatusIconPrototype>(name), Is.True,
|
||||
$"Squad icon prototype '{name}' is referenced by GetIconPrototypeId but not registered.");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SecuritySquadIconCountMatchesEnum()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var squadIcons = protoManager.EnumeratePrototypes<StatusIconPrototype>()
|
||||
.Where(p => p.ID.StartsWith("SecuritySquadIcon"))
|
||||
.ToList();
|
||||
|
||||
var enumCount = Enum.GetValues<SquadIconNum>().Length;
|
||||
|
||||
Assert.That(squadIcons.Count, Is.EqualTo(enumCount),
|
||||
$"Expected {enumCount} SecuritySquadIcon* prototypes, found {squadIcons.Count}. " +
|
||||
"If you added a new SquadIconNum entry, add the matching prototype.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SecApartmentSystemIsRegistered()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var sys = server.System<Content.Server.Corvax.SecApartment.SecApartmentSystem>();
|
||||
Assert.That(sys, Is.Not.Null, "SecApartmentSystem must be registered on the server.");
|
||||
});
|
||||
}
|
||||
}
|
||||
114
Corvax/Content.Corvax.Tests/Syndicate/StationGoalTest.cs
Normal file
114
Corvax/Content.Corvax.Tests/Syndicate/StationGoalTest.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.Server.Corvax.StationGoal;
|
||||
using Content.Shared.MassMedia.Components;
|
||||
using Content.Shared.Station.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class StationGoalTest : GameTest
|
||||
{
|
||||
public override PoolSettings PoolSettings => new()
|
||||
{
|
||||
Dirty = true,
|
||||
DummyTicker = false,
|
||||
Connected = true,
|
||||
Map = PoolManager.TestStation,
|
||||
};
|
||||
|
||||
[Test]
|
||||
public async Task ValidateGoals()
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var goals = protoManager.EnumeratePrototypes<StationGoalPrototype>().ToList();
|
||||
Assert.That(goals, Is.Not.Empty, "There must be at least one Station Goal.");
|
||||
|
||||
var seen = new HashSet<string>();
|
||||
foreach (var goal in goals)
|
||||
{
|
||||
Assert.That(goal.Text, Is.Not.Null.And.Not.Empty, $"Station Goal {goal.ID} has no text.");
|
||||
Assert.That(seen.Add(goal.ID), Is.True, $"Duplicate Station Goal ID: {goal.ID}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a station goal to a fax on the pre-loaded station and verifies the result.
|
||||
/// If <paramref name="expectNews"/> is set, also checks whether a news article is published.
|
||||
/// </summary>
|
||||
[TestCase(false, TestName = "SendsGoalToFaxWithoutNews")]
|
||||
[TestCase(true, TestName = "SendsGoalToFaxWithNews")]
|
||||
public async Task SendsGoalToFax(bool expectNews)
|
||||
{
|
||||
var pair = Pair;
|
||||
var server = pair.Server;
|
||||
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var goalSystem = server.System<StationGoalPaperSystem>();
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
EntityUid station = default;
|
||||
int articlesBefore = 0;
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var stationQuery = entManager.EntityQueryEnumerator<StationDataComponent>();
|
||||
Assert.That(stationQuery.MoveNext(out var stationUid, out _), "Pool did not create a station from the map.");
|
||||
station = stationUid;
|
||||
|
||||
if (expectNews)
|
||||
{
|
||||
// Ensure the component exists so news can be published.
|
||||
var stationNews = entManager.EnsureComponent<StationNewsComponent>(station);
|
||||
articlesBefore = stationNews.Articles.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove the component (news writer may have created it at map init)
|
||||
// so that the publish step is silently skipped.
|
||||
entManager.RemoveComponent<StationNewsComponent>(station);
|
||||
articlesBefore = 0;
|
||||
}
|
||||
});
|
||||
|
||||
var goalId = protoManager.EnumeratePrototypes<StationGoalPrototype>().First().ID;
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var result = goalSystem.SendStationGoal(station, goalId);
|
||||
Assert.That(result, Is.True, $"SendStationGoal returned false for {goalId} (fax did not accept?)");
|
||||
});
|
||||
|
||||
server.RunTicks(2);
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var hasNews = entManager.TryGetComponent<StationNewsComponent>(station, out var stationNews);
|
||||
|
||||
if (expectNews)
|
||||
{
|
||||
Assert.That(hasNews, Is.True, "StationNewsComponent is missing after sending the station goal.");
|
||||
Assert.That(stationNews!.Articles.Count, Is.EqualTo(articlesBefore + 1),
|
||||
"Expected exactly one new news article after sending the station goal.");
|
||||
|
||||
var article = stationNews.Articles[^1];
|
||||
Assert.That(article.Title, Is.Not.Null.And.Not.Empty, "News article has an empty title.");
|
||||
Assert.That(article.Content, Is.Not.Null.And.Not.Empty, "News article has empty content.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(hasNews, Is.False, "StationNewsComponent should not have been created when news publishing is disabled.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
169
Corvax/Content.Corvax.Tests/Syndicate/TTS/ClientTtsTests.cs
Normal file
169
Corvax/Content.Corvax.Tests/Syndicate/TTS/ClientTtsTests.cs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.Shared.Corvax.TTS;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
using ClientTTS = Content.Client.Corvax.TTS.TTSSystem;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate.TTS;
|
||||
|
||||
internal static class ClientTtsTestHelpers
|
||||
{
|
||||
public static Dictionary<NetEntity, Queue<PlayTTSEvent>> GetQueues(ClientTTS tts)
|
||||
=> (Dictionary<NetEntity, Queue<PlayTTSEvent>>)TtsReflection.ClientQueuesField.GetValue(tts)!;
|
||||
|
||||
public static void EnableWithFullVolume(ClientTTS tts)
|
||||
{
|
||||
TtsReflection.ClearClientState(tts);
|
||||
TtsReflection.ClientEnabledField.SetValue(tts, true);
|
||||
TtsReflection.ClientRadioVolumeField.SetValue(tts, 1.2f);
|
||||
TtsReflection.ClientVolumeField.SetValue(tts, 1.2f);
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public sealed class ClientTtsGatingTest : GameTest
|
||||
{
|
||||
[Test]
|
||||
public async Task DisabledSkipsEvent()
|
||||
{
|
||||
var client = Pair.Client;
|
||||
var tts = client.System<ClientTTS>();
|
||||
|
||||
await client.WaitPost(() =>
|
||||
{
|
||||
TtsReflection.ClearClientState(tts);
|
||||
TtsReflection.ClientEnabledField.SetValue(tts, false);
|
||||
|
||||
var source = new NetEntity(1);
|
||||
client.EntMan.EventBus.RaiseEvent(EventSource.Local,
|
||||
new PlayTTSEvent(new byte[] { 1 }, source));
|
||||
|
||||
Assert.That(ClientTtsTestHelpers.GetQueues(tts), Is.Empty,
|
||||
"Disabled TTS must not enqueue any playback.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RadioZeroVolumeSkipsEvent()
|
||||
{
|
||||
var client = Pair.Client;
|
||||
var tts = client.System<ClientTTS>();
|
||||
|
||||
await client.WaitPost(() =>
|
||||
{
|
||||
TtsReflection.ClearClientState(tts);
|
||||
TtsReflection.ClientEnabledField.SetValue(tts, true);
|
||||
TtsReflection.ClientRadioVolumeField.SetValue(tts, 0f);
|
||||
|
||||
var source = new NetEntity(1);
|
||||
client.EntMan.EventBus.RaiseEvent(EventSource.Local,
|
||||
new PlayTTSEvent(new byte[] { 1 }, source, isRadio: true));
|
||||
|
||||
Assert.That(ClientTtsTestHelpers.GetQueues(tts), Is.Empty,
|
||||
"Radio TTS with zero volume must not enqueue.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ZeroVolumeSkipsEvent()
|
||||
{
|
||||
var client = Pair.Client;
|
||||
var tts = client.System<ClientTTS>();
|
||||
|
||||
await client.WaitPost(() =>
|
||||
{
|
||||
TtsReflection.ClearClientState(tts);
|
||||
TtsReflection.ClientEnabledField.SetValue(tts, true);
|
||||
TtsReflection.ClientVolumeField.SetValue(tts, 0f);
|
||||
|
||||
var source = new NetEntity(1);
|
||||
client.EntMan.EventBus.RaiseEvent(EventSource.Local,
|
||||
new PlayTTSEvent(new byte[] { 1 }, source));
|
||||
|
||||
Assert.That(ClientTtsTestHelpers.GetQueues(tts), Is.Empty,
|
||||
"TTS with zero volume must not enqueue.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public sealed class ClientTtsQueueTest : GameTest
|
||||
{
|
||||
[Test]
|
||||
public async Task QueueOverflowDropsOldest()
|
||||
{
|
||||
var client = Pair.Client;
|
||||
var tts = client.System<ClientTTS>();
|
||||
|
||||
await client.WaitPost(() =>
|
||||
{
|
||||
ClientTtsTestHelpers.EnableWithFullVolume(tts);
|
||||
|
||||
var source = new NetEntity(1);
|
||||
|
||||
// Force the entity into the "playing" state so incoming events enqueue
|
||||
// instead of trying to play immediately.
|
||||
var playing = (HashSet<NetEntity>)TtsReflection.ClientPlayingField.GetValue(tts)!;
|
||||
playing.Add(source);
|
||||
|
||||
// Enqueue 7 events. The system keeps the queue at 6 max,
|
||||
// dropping the oldest when a new event arrives.
|
||||
for (var i = 0; i < 7; i++)
|
||||
{
|
||||
var payload = new byte[] { (byte)(i + 1) };
|
||||
client.EntMan.EventBus.RaiseEvent(EventSource.Local,
|
||||
new PlayTTSEvent(payload, source));
|
||||
}
|
||||
|
||||
var queues = ClientTtsTestHelpers.GetQueues(tts);
|
||||
Assert.That(queues.ContainsKey(source), Is.True, "Queue for source should exist.");
|
||||
|
||||
var queue = queues[source];
|
||||
Assert.That(queue.Count, Is.EqualTo(6), "Queue must cap at 6 entries.");
|
||||
|
||||
// The first event's payload (1) must have been dropped.
|
||||
Assert.That(queue.All(e => e.Data[0] != 1), Is.True,
|
||||
"Oldest event should have been dropped on overflow.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GlobalEventBypassesQueue()
|
||||
{
|
||||
var client = Pair.Client;
|
||||
var tts = client.System<ClientTTS>();
|
||||
|
||||
await client.WaitPost(() =>
|
||||
{
|
||||
ClientTtsTestHelpers.EnableWithFullVolume(tts);
|
||||
|
||||
// SourceUid == null → routed to PlayTTSInternal directly, bypassing the queue.
|
||||
// PlayTTSInternal will try to load the payload as audio; the fake byte is not
|
||||
// valid audio and may throw on some backends. The routing decision — "not queued" —
|
||||
// is what this test verifies, so audio errors are captured and reported, not
|
||||
// allowed to mask the actual assertion.
|
||||
Exception? playbackError = null;
|
||||
try
|
||||
{
|
||||
client.EntMan.EventBus.RaiseEvent(EventSource.Local,
|
||||
new PlayTTSEvent(new byte[] { 1 }));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
playbackError = ex;
|
||||
}
|
||||
|
||||
// Primary assertion: nothing was queued for any source.
|
||||
Assert.That(ClientTtsTestHelpers.GetQueues(tts), Is.Empty,
|
||||
"Global (null source) must not be queued.");
|
||||
|
||||
if (playbackError is not null)
|
||||
{
|
||||
// Not a failure — just useful diagnostics when audio plumbing changes.
|
||||
TestContext.WriteLine($"Note: PlayTTSInternal threw {playbackError.GetType().Name} " + "(expected for a fake audio payload).");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Corvax.TTS;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
using ServerTTS = Content.Server.Corvax.TTS.TTSSystem;
|
||||
using ServerTTSManager = Content.Server.Corvax.TTS.TTSManager;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate.TTS;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class TtsSanitizeTest : GameTest
|
||||
{
|
||||
[Test]
|
||||
public async Task ReplacesAbbreviations()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var tts = server.System<ServerTTS>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
Assert.That(TtsReflection.CallSanitize(tts, "нт"), Is.EqualTo("Эн Тэ"));
|
||||
Assert.That(TtsReflection.CallSanitize(tts, "гсб"), Is.EqualTo("Гэ Эс Бэ"));
|
||||
Assert.That(TtsReflection.CallSanitize(tts, "id"), Is.EqualTo("Ай Ди"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task StripsInvalidCharacters()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var tts = server.System<ServerTTS>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var result = TtsReflection.CallSanitize(tts, "Hello * World #");
|
||||
Assert.That(result, Does.Not.Contain("*"));
|
||||
Assert.That(result, Does.Not.Contain("#"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task EmptyInputReturnsEmpty()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var tts = server.System<ServerTTS>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
Assert.That(TtsReflection.CallSanitize(tts, ""), Is.Empty);
|
||||
Assert.That(TtsReflection.CallSanitize(tts, " "), Is.Empty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public sealed class TtsSsmlTest : GameTest
|
||||
{
|
||||
[Test]
|
||||
public async Task WrapsInSpeakTags()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var tts = server.System<ServerTTS>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
// Traits value here is irrelevant for the wrapping check; use 0.
|
||||
var ssml = TtsReflection.CallToSsml(tts, "Hello world", 0);
|
||||
Assert.That(ssml, Does.StartWith("<speak>"));
|
||||
Assert.That(ssml, Does.EndWith("</speak>"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task EscapesXml()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var tts = server.System<ServerTTS>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var ssml = TtsReflection.CallToSsml(tts, "<test> & \"q\"", 0);
|
||||
Assert.That(ssml, Does.Contain("<test>"));
|
||||
Assert.That(ssml, Does.Contain("&"));
|
||||
Assert.That(ssml, Does.Contain("""));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public sealed class TtsEntitySpokeTest : GameTest
|
||||
{
|
||||
private static EntityUid SpawnSpeaker(
|
||||
IEntityManager entMan, SharedMapSystem mapSystem, MapId mapId, string? voiceId)
|
||||
{
|
||||
var mob = entMan.SpawnEntity("MobHuman", new MapCoordinates(0, 0, mapId));
|
||||
var tts = entMan.EnsureComponent<TTSComponent>(mob);
|
||||
tts.VoicePrototypeId = voiceId;
|
||||
return mob;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RunsWithCachedAudio()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var mapSystem = server.System<SharedMapSystem>();
|
||||
var tts = server.System<ServerTTS>();
|
||||
var manager = server.ResolveDependency<ServerTTSManager>();
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
mapSystem.CreateMap(out var mapId);
|
||||
|
||||
var voiceProto = protoManager.Index<TTSVoicePrototype>("Taskmaster");
|
||||
var mob = SpawnSpeaker(entMan, mapSystem, mapId, "Taskmaster");
|
||||
|
||||
// Pre-compute the exact SSML GenerateTTS will produce and seed the cache.
|
||||
TtsReflection.ClearServerCache(manager);
|
||||
var ssml = TtsReflection.BuildSsmlFor(tts, "test", isWhisper: false);
|
||||
TtsReflection.PrepopulateServerCache(
|
||||
manager, voiceProto.Speaker, ssml, new byte[] { 1, 2, 3 });
|
||||
|
||||
// Sanity: fail here, not on a silent network call if the SSML pipeline drifts.
|
||||
Assert.That(TtsReflection.HasServerCacheEntry(manager, voiceProto.Speaker, ssml),
|
||||
Is.True, "Sanity: cache must contain the seeded SSML entry.");
|
||||
|
||||
// Raise EntitySpokeEvent on the mob; the system must hit the cache.
|
||||
var ev = new EntitySpokeEvent(mob, "test", "test", null, null);
|
||||
server.EntMan.EventBus.RaiseLocalEvent(mob, ev);
|
||||
|
||||
entMan.DeleteEntity(mob);
|
||||
mapSystem.DeleteMap(mapId);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IgnoresVeryLongMessage()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var mapSystem = server.System<SharedMapSystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
mapSystem.CreateMap(out var mapId);
|
||||
var mob = SpawnSpeaker(entMan, mapSystem, mapId, "Taskmaster");
|
||||
|
||||
// 201 chars — over MaxMessageChars (200). The handler must return early
|
||||
// and never touch TTSManager (so no HTTP even without cache).
|
||||
var longMsg = new string('a', 201);
|
||||
var ev = new EntitySpokeEvent(mob, longMsg, longMsg, null, null);
|
||||
server.EntMan.EventBus.RaiseLocalEvent(mob, ev);
|
||||
|
||||
entMan.DeleteEntity(mob);
|
||||
mapSystem.DeleteMap(mapId);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IgnoresEntityWithoutVoice()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var mapSystem = server.System<SharedMapSystem>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
mapSystem.CreateMap(out var mapId);
|
||||
var mob = SpawnSpeaker(entMan, mapSystem, mapId, voiceId: null);
|
||||
|
||||
var ev = new EntitySpokeEvent(mob, "test", "test", null, null);
|
||||
server.EntMan.EventBus.RaiseLocalEvent(mob, ev);
|
||||
|
||||
entMan.DeleteEntity(mob);
|
||||
mapSystem.DeleteMap(mapId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.IntegrationTests.Fixtures;
|
||||
using Content.Shared.Corvax.TTS;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate.TTS;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class TtsVoicePrototypeTest : GameTest
|
||||
{
|
||||
[Test]
|
||||
public async Task PrototypesAreValid()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var voices = protoManager.EnumeratePrototypes<TTSVoicePrototype>().ToList();
|
||||
Assert.That(voices, Is.Not.Empty, "There must be at least one TTS voice.");
|
||||
|
||||
var seen = new HashSet<string>();
|
||||
foreach (var voice in voices)
|
||||
{
|
||||
Assert.That(voice.ID, Is.Not.Null.And.Not.Empty);
|
||||
Assert.That(seen.Add(voice.ID), Is.True, $"Duplicate voice ID: {voice.ID}");
|
||||
Assert.That(voice.Speaker, Is.Not.Null.And.Not.Empty, $"Voice {voice.ID} has empty Speaker.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DefaultVoiceExists()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
Assert.That(protoManager.HasIndex<TTSVoicePrototype>("Taskmaster"), Is.True,
|
||||
"Default voice 'Taskmaster' must exist.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AnnouncementSpeakerExists()
|
||||
{
|
||||
var server = Pair.Server;
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
Assert.That(protoManager.HasIndex<TTSVoicePrototype>("Glados"), Is.True,
|
||||
"Announcement speaker 'Glados' must exist.");
|
||||
});
|
||||
}
|
||||
}
|
||||
178
Corvax/Content.Corvax.Tests/Syndicate/TTS/TtsReflection.cs
Normal file
178
Corvax/Content.Corvax.Tests/Syndicate/TTS/TtsReflection.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Content.Shared.Corvax.TTS;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
using ClientTTS = Content.Client.Corvax.TTS.TTSSystem;
|
||||
using ServerTTS = Content.Server.Corvax.TTS.TTSSystem;
|
||||
using ServerTTSManager = Content.Server.Corvax.TTS.TTSManager;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate.TTS;
|
||||
|
||||
/// <summary>
|
||||
/// Reflection bridge into the TTS implementation.
|
||||
/// All lookups are validated on first touch and throw a descriptive
|
||||
/// <see cref="InvalidOperationException"/> when the production surface
|
||||
/// changes — otherwise a silent <c>NullReferenceException</c> inside a
|
||||
/// single test would hide the actual contract break.
|
||||
/// </summary>
|
||||
internal static class TtsReflection
|
||||
{
|
||||
private static FieldInfo RequireField(Type type, string name)
|
||||
{
|
||||
var field = type.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (field is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"TTS reflection contract broken: {type.FullName}.{name} field not found. " +
|
||||
"Update TtsReflection to match the current implementation.");
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
private static MethodInfo RequireMethod(Type type, string name)
|
||||
{
|
||||
var method = type.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (method is null)
|
||||
throw new InvalidOperationException(
|
||||
$"TTS reflection contract broken: {type.FullName}.{name}() not found. " +
|
||||
"Update TtsReflection to match the current implementation.");
|
||||
return method;
|
||||
}
|
||||
|
||||
private static Type RequireNestedType(Type type, string name)
|
||||
{
|
||||
var nested = type.GetNestedType(name, BindingFlags.NonPublic);
|
||||
if (nested is null)
|
||||
throw new InvalidOperationException(
|
||||
$"TTS reflection contract broken: nested type {type.FullName}+{name} not found. " +
|
||||
"Update TtsReflection to match the current implementation.");
|
||||
return nested;
|
||||
}
|
||||
|
||||
private static readonly FieldInfo ServerCacheField =
|
||||
RequireField(typeof(ServerTTSManager), "_cache");
|
||||
|
||||
private static readonly FieldInfo ServerCacheKeysField =
|
||||
RequireField(typeof(ServerTTSManager), "_cacheKeysSeq");
|
||||
|
||||
private static readonly MethodInfo ServerSanitizeMethod =
|
||||
RequireMethod(typeof(ServerTTS), "Sanitize");
|
||||
|
||||
private static readonly MethodInfo ServerToSsmlMethod =
|
||||
RequireMethod(typeof(ServerTTS), "ToSsmlText");
|
||||
|
||||
private static readonly Type ServerSoundTraitsType =
|
||||
RequireNestedType(typeof(ServerTTS), "SoundTraits");
|
||||
|
||||
public static readonly FieldInfo ClientPlayingField =
|
||||
RequireField(typeof(ClientTTS), "_playingEntities");
|
||||
|
||||
public static readonly FieldInfo ClientQueuesField =
|
||||
RequireField(typeof(ClientTTS), "_entityQueues");
|
||||
|
||||
public static readonly FieldInfo ClientEnabledField =
|
||||
RequireField(typeof(ClientTTS), "_ttsEnabled");
|
||||
|
||||
public static readonly FieldInfo ClientRadioVolumeField =
|
||||
RequireField(typeof(ClientTTS), "_radioVolume");
|
||||
|
||||
public static readonly FieldInfo ClientVolumeField =
|
||||
RequireField(typeof(ClientTTS), "_volume");
|
||||
|
||||
private static int TraitValue(string name)
|
||||
{
|
||||
if (!Enum.IsDefined(ServerSoundTraitsType, name))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"TTS reflection contract broken: SoundTraits.{name} is not defined. " +
|
||||
"Update TtsReflection to match the current enum.");
|
||||
}
|
||||
|
||||
return Convert.ToInt32(Enum.Parse(ServerSoundTraitsType, name));
|
||||
}
|
||||
|
||||
private static readonly int NormalTraits =
|
||||
TraitValue("RateFast") | TraitValue("PitchMedium");
|
||||
|
||||
private static readonly int WhisperTraits =
|
||||
TraitValue("RateSlow") | TraitValue("PitchVerylow") | TraitValue("VolumeXSoft");
|
||||
|
||||
public static string CallSanitize(ServerTTS system, string text)
|
||||
=> (string)ServerSanitizeMethod.Invoke(system, new object[] { text })!;
|
||||
|
||||
public static string CallToSsml(ServerTTS system, string sanitized, int traitsValue)
|
||||
{
|
||||
var traits = Enum.ToObject(ServerSoundTraitsType, traitsValue);
|
||||
return (string)ServerToSsmlMethod.Invoke(system, new[] { sanitized, traits })!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors the SSML pipeline inside <c>ServerTTS.GenerateTTS</c>:
|
||||
/// sanitize → append trailing period if the text ends in a letter → wrap via <c>ToSsmlText</c>.
|
||||
/// The period step is duplicated because it lives inside <c>GenerateTTS</c> itself,
|
||||
/// which would otherwise trigger the HTTP-backed <see cref="ServerTTSManager"/>.
|
||||
/// If the production pipeline changes, the cache key will not match and
|
||||
/// <c>RunsWithCachedAudio</c> will fall through to the network — a real failure,
|
||||
/// not a silent pass.
|
||||
/// </summary>
|
||||
public static string BuildSsmlFor(ServerTTS system, string text, bool isWhisper)
|
||||
{
|
||||
var sanitized = CallSanitize(system, text);
|
||||
if (string.IsNullOrEmpty(sanitized))
|
||||
return string.Empty;
|
||||
|
||||
if (char.IsLetter(sanitized[^1]))
|
||||
sanitized += ".";
|
||||
|
||||
var traits = isWhisper ? WhisperTraits : NormalTraits;
|
||||
return CallToSsml(system, sanitized, traits);
|
||||
}
|
||||
|
||||
public static void ClearServerCache(ServerTTSManager manager)
|
||||
{
|
||||
var cache = (Dictionary<string, byte[]>)ServerCacheField.GetValue(manager)!;
|
||||
var keys = (List<string>)ServerCacheKeysField.GetValue(manager)!;
|
||||
cache.Clear();
|
||||
keys.Clear();
|
||||
}
|
||||
|
||||
public static void PrepopulateServerCache(
|
||||
ServerTTSManager manager, string speaker, string ssml, byte[] data)
|
||||
{
|
||||
var cache = (Dictionary<string, byte[]>)ServerCacheField.GetValue(manager)!;
|
||||
var keys = (List<string>)ServerCacheKeysField.GetValue(manager)!;
|
||||
|
||||
var key = BuildCacheKey(speaker, ssml);
|
||||
cache[key] = data;
|
||||
keys.Add(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanity helper: verify that a seeded cache entry is actually present,
|
||||
/// so <c>RunsWithCachedAudio</c> fails at the setup step rather than
|
||||
/// silently making a real network call.
|
||||
/// </summary>
|
||||
public static bool HasServerCacheEntry(ServerTTSManager manager, string speaker, string ssml)
|
||||
{
|
||||
var cache = (Dictionary<string, byte[]>)ServerCacheField.GetValue(manager)!;
|
||||
return cache.ContainsKey(BuildCacheKey(speaker, ssml));
|
||||
}
|
||||
|
||||
private static string BuildCacheKey(string speaker, string ssml)
|
||||
{
|
||||
var rawKey = $"{speaker}/{ssml.ToLowerInvariant()}";
|
||||
return rawKey.Length <= 32
|
||||
? rawKey
|
||||
: Convert.ToHexString(
|
||||
System.Security.Cryptography.MD5.HashData(Encoding.UTF8.GetBytes(rawKey)));
|
||||
}
|
||||
|
||||
public static void ClearClientState(ClientTTS tts)
|
||||
{
|
||||
((Dictionary<NetEntity, Queue<PlayTTSEvent>>)ClientQueuesField.GetValue(tts)!).Clear();
|
||||
((HashSet<NetEntity>)ClientPlayingField.GetValue(tts)!).Clear();
|
||||
}
|
||||
}
|
||||
166
Corvax/Content.Corvax.Tests/Syndicate/TTS/TtsUnitTests.cs
Normal file
166
Corvax/Content.Corvax.Tests/Syndicate/TTS/TtsUnitTests.cs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.Server.Corvax.TTS;
|
||||
using Content.Shared.Corvax.TTS;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Corvax.Tests.Syndicate.TTS;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class TtsComponentTest
|
||||
{
|
||||
[Test]
|
||||
public void DefaultVoiceIsTaskmaster()
|
||||
{
|
||||
var comp = new TTSComponent();
|
||||
Assert.That(comp.VoicePrototypeId, Is.Not.Null);
|
||||
Assert.That(comp.VoicePrototypeId!.Value.Id, Is.EqualTo("Taskmaster"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void VoiceCanBeNulled()
|
||||
{
|
||||
var comp = new TTSComponent { VoicePrototypeId = null };
|
||||
Assert.That(comp.VoicePrototypeId, Is.Null);
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public sealed class TtsEventTest
|
||||
{
|
||||
[Test]
|
||||
public void PlayEventStoresAllParameters()
|
||||
{
|
||||
var data = new byte[] { 1, 2, 3 };
|
||||
var source = new NetEntity(42);
|
||||
var ev = new PlayTTSEvent(data, source, isWhisper: true, isRadio: true);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(ev.Data, Is.SameAs(data));
|
||||
Assert.That(ev.SourceUid, Is.EqualTo(source));
|
||||
Assert.That(ev.IsWhisper, Is.True);
|
||||
Assert.That(ev.IsRadio, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PlayEventDefaultsAreFalse()
|
||||
{
|
||||
var ev = new PlayTTSEvent(Array.Empty<byte>());
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(ev.SourceUid, Is.Null);
|
||||
Assert.That(ev.IsWhisper, Is.False);
|
||||
Assert.That(ev.IsRadio, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RequestPreviewEventStoresVoiceId()
|
||||
{
|
||||
var ev = new RequestPreviewTTSEvent("TestVoice");
|
||||
Assert.That(ev.VoiceId, Is.EqualTo("TestVoice"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TransformSpeakerEventStoresVoiceId()
|
||||
{
|
||||
var sender = new EntityUid(1);
|
||||
var ev = new TransformSpeakerVoiceEvent(sender, "TestVoice");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(ev.Sender, Is.EqualTo(sender));
|
||||
Assert.That(ev.VoiceId, Is.EqualTo("TestVoice"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public sealed class RadioChannelFlagTest
|
||||
{
|
||||
private static readonly RadioChannelFlag[] AllChannels =
|
||||
{
|
||||
RadioChannelFlag.Common, RadioChannelFlag.Command, RadioChannelFlag.Engineering,
|
||||
RadioChannelFlag.Medical, RadioChannelFlag.Science, RadioChannelFlag.Security,
|
||||
RadioChannelFlag.Service, RadioChannelFlag.Supply, RadioChannelFlag.Legal,
|
||||
RadioChannelFlag.Syndicate, RadioChannelFlag.Binary, RadioChannelFlag.Handheld,
|
||||
RadioChannelFlag.Freelance, RadioChannelFlag.CentCom, RadioChannelFlag.Xenoborg,
|
||||
RadioChannelFlag.Mothership,
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void EachChannelIsDistinctBit()
|
||||
{
|
||||
var seen = new HashSet<int>();
|
||||
foreach (var flag in AllChannels)
|
||||
{
|
||||
var value = (int)flag;
|
||||
Assert.That(value, Is.GreaterThan(0));
|
||||
Assert.That(value & (value - 1), Is.EqualTo(0), $"{flag} must be a single bit.");
|
||||
Assert.That(seen.Add(value), Is.True, $"Bit collision at {flag}.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllExceptCommonExcludesCommon()
|
||||
{
|
||||
Assert.That(RadioChannelFlag.AllExceptCommon.HasFlag(RadioChannelFlag.Common), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AllIncludesEveryChannel()
|
||||
{
|
||||
foreach (var flag in AllChannels)
|
||||
{
|
||||
Assert.That(RadioChannelFlag.All.HasFlag(flag), Is.True, $"All must include {flag}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public sealed class TtsVoiceEffectPresetTest
|
||||
{
|
||||
[Test]
|
||||
public void EnumValuesAreStable()
|
||||
{
|
||||
// These values are networked; changing them breaks clients.
|
||||
Assert.That((int)TTSVoiceEffectPreset.None, Is.EqualTo(0));
|
||||
Assert.That((int)TTSVoiceEffectPreset.Room, Is.EqualTo(1));
|
||||
Assert.That((int)TTSVoiceEffectPreset.Hall, Is.EqualTo(2));
|
||||
Assert.That((int)TTSVoiceEffectPreset.Void, Is.EqualTo(3));
|
||||
Assert.That((int)TTSVoiceEffectPreset.Airlock, Is.EqualTo(4));
|
||||
Assert.That((int)TTSVoiceEffectPreset.Warm, Is.EqualTo(5));
|
||||
Assert.That((int)TTSVoiceEffectPreset.Subspace, Is.EqualTo(6));
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
public sealed class NumberConverterTest
|
||||
{
|
||||
[TestCase(0L, "ноль")]
|
||||
[TestCase(1L, "один")]
|
||||
[TestCase(10L, "десять")]
|
||||
[TestCase(21L, "двадцать один")]
|
||||
[TestCase(1000L, "одна тысяча")]
|
||||
[TestCase(2000L, "две тысячи")]
|
||||
[TestCase(5000L, "пять тысяч")]
|
||||
[TestCase(1_000_000L, "один миллион")]
|
||||
public void ConvertsNumber(long input, string expected)
|
||||
{
|
||||
Assert.That(NumberConverter.NumberToText(input), Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NegativePrefixedWithMinus()
|
||||
{
|
||||
Assert.That(NumberConverter.NumberToText(-5), Does.StartWith("минус"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OutOfRangeReturnsEmpty()
|
||||
{
|
||||
Assert.That(NumberConverter.NumberToText(1_000_000_000_000_000), Is.Empty);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<Folder Name="/Build/">
|
||||
<File Path="Tools/generate_hashes.ps1" />
|
||||
</Folder>
|
||||
<Folder Name="/Corvax Interfaces/">
|
||||
<Folder Name="/Corvax/">
|
||||
<Project Path="Corvax/Content.Corvax.Interfaces.Client/Content.Corvax.Interfaces.Client.csproj">
|
||||
<BuildType Solution="DebugOpt|*" Project="Debug" />
|
||||
<BuildType Solution="Tools|*" Project="Debug" />
|
||||
|
|
@ -26,6 +26,10 @@
|
|||
<BuildType Solution="DebugOpt|*" Project="Debug" />
|
||||
<BuildType Solution="Tools|*" Project="Debug" />
|
||||
</Project>
|
||||
<Project Path="Corvax/Content.Corvax.Tests/Content.Corvax.Tests.csproj">
|
||||
<BuildType Solution="DebugOpt|*" Project="Debug" />
|
||||
<BuildType Solution="Tools|*" Project="Debug" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/MSBuild/">
|
||||
<File Path="MSBuild/Content.props" />
|
||||
|
|
@ -181,4 +185,4 @@
|
|||
<Project Path="Content.Tools/Content.Tools.csproj" />
|
||||
<Project Path="Content.YAMLLinter/Content.YAMLLinter.csproj" />
|
||||
<Project Path="Pow3r/Pow3r.csproj" />
|
||||
</Solution>
|
||||
</Solution>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue