175 lines
6.5 KiB
C#
175 lines
6.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using NUnit.Framework;
|
|
using NightclubArcadia.Skills;
|
|
using UnityEditor;
|
|
|
|
namespace NightclubArcadia.Skills.Tests
|
|
{
|
|
/// <summary>
|
|
/// Source lint over Assets/Dialogue/**/*.yarn. This is a tripwire, not a proof —
|
|
/// it catches the easy-and-wrong wirings, not every possible one.
|
|
/// </summary>
|
|
public class CandidateYarnLintTests
|
|
{
|
|
const string DialogueRoot = "Assets/Dialogue";
|
|
const string DatabasePath = "Assets/Candidates/CandidateDatabase.asset";
|
|
|
|
static readonly Regex LeanWrite = new Regex(
|
|
@"\$self_lean_([a-z][a-z0-9_]*)",
|
|
RegexOptions.Compiled);
|
|
static readonly Regex FoundWrite = new Regex(
|
|
@"\$found_([a-z][a-z0-9_]*)",
|
|
RegexOptions.Compiled);
|
|
static readonly Regex LeanVsLean = new Regex(
|
|
@"\$self_lean_[a-z][a-z0-9_]*\s*(>=|<=|==|!=|>|<)\s*\$self_lean_[a-z][a-z0-9_]*",
|
|
RegexOptions.Compiled);
|
|
static readonly Regex NodeSplit = new Regex(
|
|
@"^title:\s*(.+)\s*$",
|
|
RegexOptions.Compiled | RegexOptions.Multiline);
|
|
|
|
CandidateDatabase LoadDatabase()
|
|
{
|
|
// Assets are checked in (generated by CandidateSystemSetup). Same pattern as
|
|
// SkillRosterTests — the test assembly cannot reference the Editor setup type.
|
|
var db = AssetDatabase.LoadAssetAtPath<CandidateDatabase>(DatabasePath);
|
|
Assert.IsNotNull(db, $"Expected CandidateDatabase at {DatabasePath}");
|
|
return db;
|
|
}
|
|
|
|
static IEnumerable<string> AllYarnFiles()
|
|
{
|
|
return Directory.GetFiles(DialogueRoot, "*.yarn", SearchOption.AllDirectories);
|
|
}
|
|
|
|
static string Read(string path) => File.ReadAllText(path);
|
|
|
|
[Test]
|
|
public void Lint_LeanAndFoundWrites_MatchRegistryIds()
|
|
{
|
|
var db = LoadDatabase();
|
|
var candidateIds = new HashSet<string>(
|
|
db.All.Select(c => c.Id),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
var clueIds = new HashSet<string>(
|
|
db.All.SelectMany(c => c.Clues).Select(c => c.Id),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var path in AllYarnFiles())
|
|
{
|
|
var text = Read(path);
|
|
foreach (Match match in LeanWrite.Matches(text))
|
|
{
|
|
var id = match.Groups[1].Value;
|
|
Assert.IsTrue(
|
|
candidateIds.Contains(id),
|
|
$"{path}: $self_lean_{id} has no matching candidate id in the registry.");
|
|
}
|
|
|
|
foreach (Match match in FoundWrite.Matches(text))
|
|
{
|
|
var id = match.Groups[1].Value;
|
|
Assert.IsTrue(
|
|
clueIds.Contains(id),
|
|
$"{path}: $found_{id} has no matching clue id in the registry.");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Lint_AuthoredClues_HaveFoundWrites()
|
|
{
|
|
var db = LoadDatabase();
|
|
var allYarn = string.Join("\n", AllYarnFiles().Select(Read));
|
|
|
|
foreach (var candidate in db.All)
|
|
{
|
|
foreach (var clue in candidate.Clues)
|
|
{
|
|
if (clue.Status != CandidateClueStatus.Authored)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var needle = $"<<set $found_{clue.Id} = true>>";
|
|
Assert.IsTrue(
|
|
allYarn.Contains(needle),
|
|
$"Authored clue '{clue.Id}' has no `{needle}` anywhere under {DialogueRoot}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Lint_NoLeanVsLeanComparison()
|
|
{
|
|
foreach (var path in AllYarnFiles())
|
|
{
|
|
var text = Read(path);
|
|
Assert.IsFalse(
|
|
LeanVsLean.IsMatch(text),
|
|
$"{path}: comparing two $self_lean_* values against each other is forbidden " +
|
|
"(no derived winner). Comparing one lean against a constant is fine.");
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Lint_ClueAndLeanWrites_NotSkillGated()
|
|
{
|
|
foreach (var path in AllYarnFiles())
|
|
{
|
|
var text = Read(path);
|
|
var matches = NodeSplit.Matches(text);
|
|
for (var i = 0; i < matches.Count; i++)
|
|
{
|
|
var start = matches[i].Index;
|
|
var end = i + 1 < matches.Count ? matches[i + 1].Index : text.Length;
|
|
var nodeBody = text.Substring(start, end - start);
|
|
var title = matches[i].Groups[1].Value.Trim();
|
|
|
|
var writesLeanOrFound =
|
|
nodeBody.Contains("$self_lean_") || nodeBody.Contains("$found_");
|
|
if (!writesLeanOrFound)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Assert.IsFalse(
|
|
nodeBody.Contains("skill_rank("),
|
|
$"{path} node '{title}': a node that writes $self_lean_* / $found_* " +
|
|
"must not sit behind a skill_rank(...) guard.");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Lint_CommentaryNodes_NeverWriteLeanOrFound()
|
|
{
|
|
foreach (var path in AllYarnFiles())
|
|
{
|
|
var text = Read(path);
|
|
var matches = NodeSplit.Matches(text);
|
|
for (var i = 0; i < matches.Count; i++)
|
|
{
|
|
var title = matches[i].Groups[1].Value.Trim();
|
|
if (!title.StartsWith("Commentary_", StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var start = matches[i].Index;
|
|
var end = i + 1 < matches.Count ? matches[i + 1].Index : text.Length;
|
|
var nodeBody = text.Substring(start, end - start);
|
|
|
|
Assert.IsFalse(
|
|
nodeBody.Contains("$self_lean_") || nodeBody.Contains("$found_"),
|
|
$"{path} node '{title}': Commentary_* nodes must not write lean or found flags " +
|
|
"(ambient advocacy only; clue lines are scene-authored).");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|