Improve Do Not Map test to whitelist specific prototypes per map and whitelist entire directories (#36117)

* Enable whitelisting specific DNM prototypes per map

* Enable whitelisting directories

* Rename fields

* Use a HashSet instead of an array

* Add check for unused whitelist entries

* Remove whitelisting for meta (warden's rubber stamp was removed)

* Add glob support courtesy of @IProduceWidgets

* Update xmldoc
This commit is contained in:
Tayrtahn
2025-09-10 16:26:45 -04:00
committed by GitHub
parent 7b9aee3977
commit 3e2152a59e

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking;
using Content.Server.Maps;
@@ -44,17 +45,43 @@ namespace Content.IntegrationTests.Tests
AdminTestArenaSystem.ArenaMapPath
};
/// <summary>
/// A dictionary linking maps to collections of entity prototype ids that should be exempt from "DoNotMap" restrictions.
/// </summary>
/// <remarks>
/// This declares that the listed entity prototypes are allowed to be present on the map
/// despite being categorized as "DoNotMap", while any unlisted prototypes will still
/// cause the test to fail.
/// </remarks>
private static readonly Dictionary<string, HashSet<EntProtoId>> DoNotMapWhitelistSpecific = new()
{
{"/Maps/bagel.yml", ["RubberStampMime"]},
{"/Maps/reach.yml", ["HandheldCrewMonitor"]},
{"/Maps/Shuttles/ShuttleEvent/honki.yml", ["GoldenBikeHorn", "RubberStampClown"]},
{"/Maps/Shuttles/ShuttleEvent/syndie_evacpod.yml", ["RubberStampSyndicate"]},
{"/Maps/Shuttles/ShuttleEvent/cruiser.yml", ["ShuttleGunPerforator"]},
{"/Maps/Shuttles/ShuttleEvent/instigator.yml", ["ShuttleGunFriendship"]},
};
/// <summary>
/// Maps listed here are given blanket freedom to contain "DoNotMap" entities. Use sparingly.
/// </summary>
/// <remarks>
/// It is also possible to whitelist entire directories here. For example, adding
/// "/Maps/Shuttles/**" will whitelist all shuttle maps.
/// </remarks>
private static readonly string[] DoNotMapWhitelist =
{
"/Maps/centcomm.yml",
"/Maps/bagel.yml", // Contains mime's rubber stamp --> Either fix this, remove the category, or remove this comment if intentional.
"/Maps/reach.yml", // Contains handheld crew monitor
"/Maps/Shuttles/ShuttleEvent/cruiser.yml", // Contains LSE-1200c "Perforator"
"/Maps/Shuttles/ShuttleEvent/honki.yml", // Contains golden honker, clown's rubber stamp
"/Maps/Shuttles/ShuttleEvent/instigator.yml", // Contains EXP-320g "Friendship"
"/Maps/Shuttles/ShuttleEvent/syndie_evacpod.yml", // Contains syndicate rubber stamp
};
/// <summary>
/// Converts the above globs into regex so your eyes dont bleed trying to add filepaths.
/// </summary>
private static readonly Regex[] DoNotMapWhiteListRegexes = DoNotMapWhitelist
.Select(glob => new Regex(GlobToRegex(glob), RegexOptions.IgnoreCase | RegexOptions.Compiled))
.ToArray();
private static readonly string[] GameMaps =
{
"Dev",
@@ -247,17 +274,30 @@ namespace Content.IntegrationTests.Tests
await pair.CleanReturnAsync();
}
private bool IsWhitelistedForMap(EntProtoId protoId, ResPath map)
{
if (!DoNotMapWhitelistSpecific.TryGetValue(map.ToString(), out var allowedProtos))
return false;
return allowedProtos.Contains(protoId);
}
/// <summary>
/// Check that maps do not have any entities that belong to the DoNotMap entity category
/// </summary>
private void CheckDoNotMap(ResPath map, YamlNode node, IPrototypeManager protoManager)
{
if (DoNotMapWhitelist.Contains(map.ToString()))
return;
foreach (var regex in DoNotMapWhiteListRegexes)
{
if (regex.IsMatch(map.ToString()))
return;
}
var yamlEntities = node["entities"];
var dnmCategory = protoManager.Index(DoNotMapCategory);
// Make a set containing all the specific whitelisted proto ids for this map
HashSet<EntProtoId> unusedExemptions = DoNotMapWhitelistSpecific.TryGetValue(map.ToString(), out var exemptions) ? new(exemptions) : [];
Assert.Multiple(() =>
{
foreach (var yamlEntity in (YamlSequenceNode)yamlEntities)
@@ -268,10 +308,17 @@ namespace Content.IntegrationTests.Tests
if (!protoManager.TryIndex(protoId, out var proto))
continue;
Assert.That(!proto.Categories.Contains(dnmCategory),
Assert.That(!proto.Categories.Contains(dnmCategory) || IsWhitelistedForMap(protoId, map),
$"\nMap {map} contains entities in the DO NOT MAP category ({proto.Name})");
// The proto id is used on this map, so remove it from the set
unusedExemptions.Remove(protoId);
}
});
// If there are any proto ids left, they must not have been used in the map!
Assert.That(unusedExemptions, Is.Empty,
$"Map {map} has DO NOT MAP entities whitelisted that are not present in the map: {string.Join(", ", unusedExemptions)}");
}
private bool IsPreInit(ResPath map,
@@ -332,7 +379,7 @@ namespace Content.IntegrationTests.Tests
MapId mapId;
try
{
var opts = DeserializationOptions.Default with {InitializeMaps = true};
var opts = DeserializationOptions.Default with { InitializeMaps = true };
ticker.LoadGameMap(protoManager.Index<GameMapPrototype>(mapProto), out mapId, opts);
}
catch (Exception ex)
@@ -439,7 +486,7 @@ namespace Content.IntegrationTests.Tests
#nullable enable
while (queryPoint.MoveNext(out T? comp, out var xform))
{
var spawner = (ISpawnPoint) comp;
var spawner = (ISpawnPoint)comp;
if (spawner.SpawnType is not SpawnPointType.LateJoin
|| xform.GridUid == null
@@ -553,5 +600,20 @@ namespace Content.IntegrationTests.Tests
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
/// <summary>
/// Lets us the convert the filepaths to regex without eyeglaze trying to add new paths.
/// </summary>
private static string GlobToRegex(string glob)
{
var regex = Regex.Escape(glob)
.Replace(@"\*\*", "**") // replace **
.Replace(@"\*", "*") // replace *
.Replace("**", ".*") // ** → match across folders
.Replace("*", @"[^/]*") // * → match within a single folder
.Replace(@"\?", "."); // ? → any single character
return $"^{regex}$";
}
}
}