be7aa8affd
- 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>
61 lines
2.9 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|