Files
Project-M/Assets/_Project/Tests/EditMode/TestAttributeGuardTests.cs
T
kronic be7aa8affd Hygiene B6: test coverage (fixture + missing coverage + guards)
- TestWorld: shared plain-Entities fixture (Make/Make<T>/SetTick reconciling the two SetServerTick variants + Player/Enemy builders). Additive; new tests consume it (40-file migration of existing tests deliberately deferred as pure churn).
- TestAttributeGuardTests: scans *Tests.cs and fails on any parameterless public-void method missing a runner attribute — guards the swallowed-[Test] bug (B0). Confirms the suite has no other dead tests.
- PrepPurchaseSystemTests (6): the previously-untested RPC economy — afford, reject-when-broke, once-per-run, non-Staging reject, unknown-id drop, and DR-014 same-tick atomicity.
- SystemOrderingCycleTests: registers the real run/cycle/combat system set and asserts SortSystems() has no circular dependency (invisible to single-system fixtures; only throws at Play) — also de-risks the B5 splits.

459/459 EditMode tests pass. (BossAISystemTests remains queued — the most complex to author faithfully; the boss stays Play-validated meanwhile.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 23:50:07 -07:00

61 lines
2.9 KiB
C#

using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using NUnit.Framework;
namespace ProjectM.Tests
{
/// <summary>
/// Anti-regression guard for the "an edit swallowed the [Test] attribute" hazard — a real dead test shipped at
/// EnemyAIMathTests.cs:107 (a regression guard for the enemy-stuck-on-cover fix that silently never ran). Scans
/// every *Tests.cs source file in this directory and FAILS if a parameterless public-void method (the suite's
/// test-method shape — every helper is static, so `public void` uniquely selects tests) is not immediately
/// preceded by a runner attribute. File I/O is available at EditMode-test time.
/// </summary>
public class TestAttributeGuardTests
{
static string ThisDir([CallerFilePath] string p = "") => Path.GetDirectoryName(p);
static readonly Regex TestMethod = new Regex(@"^\s*public\s+void\s+[A-Za-z_]\w*\s*\(\s*\)", RegexOptions.Compiled);
static readonly Regex RunnerAttr = new Regex(@"\[\s*(Test|TestCase|TestCaseSource|Theory|SetUp|TearDown|OneTimeSetUp|OneTimeTearDown)\b", RegexOptions.Compiled);
[Test]
public void Every_Public_Void_Test_Method_Has_A_Runner_Attribute()
{
var dir = ThisDir();
Assert.IsTrue(Directory.Exists(dir), $"Test source dir not found: {dir}");
var offenders = new List<string>();
foreach (var file in Directory.GetFiles(dir, "*Tests.cs"))
{
var lines = File.ReadAllLines(file);
for (int i = 0; i < lines.Length; i++)
{
if (!TestMethod.IsMatch(lines[i]))
continue;
// Walk upward past blank / comment lines to the nearest attribute or non-trivial line.
bool attributed = false;
for (int j = i - 1; j >= 0; j--)
{
string t = lines[j].Trim();
if (t.Length == 0 || t.StartsWith("//") || t.StartsWith("/*") || t.StartsWith("*"))
continue;
if (t.StartsWith("["))
{
if (RunnerAttr.IsMatch(t)) { attributed = true; break; }
continue; // a non-runner attribute line — keep scanning the attribute block
}
break; // hit a brace / statement: no attribute block above this method
}
if (!attributed)
offenders.Add($"{Path.GetFileName(file)}:{i + 1} {lines[i].Trim()}");
}
}
Assert.IsEmpty(offenders,
"Test methods missing a runner attribute (swallowed [Test]?):\n" + string.Join("\n", offenders));
}
}
}