Skip to content

Commit 7a7d93c

Browse files
committed
[Test] 完善测试,实现基于section的实质比较
1 parent 116e185 commit 7a7d93c

1 file changed

Lines changed: 251 additions & 1 deletion

File tree

tests/ogk/Ogkr测试.cs

Lines changed: 251 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,256 @@ public void 解析Ogkr再生成回去(OgkrTestInput c)
2424
_output.WriteLine(string.Join('\n', parseAlerts));
2525
_output.WriteLine(string.Join('\n', generateAlerts));
2626

27-
Assert.Equal(ogkrText, resultText);
27+
OgkrTextComparer.AssertOgkrEqual(ogkrText, resultText);
28+
}
29+
}
30+
31+
/// <summary>
32+
/// 对两份 ogkr 文本进行“分段、逐行”的比较。
33+
///
34+
/// 段落与比较规则:
35+
/// 1. [HEADER]:忽略 T_ 开头的统计量与 TUTORIAL,其余逐行严格比较。
36+
/// 2. [B_PALETTE]:不直接逐行比较;但要求 actual 内不存在两行“实质相同”
37+
/// (除去 ID 之外其余字段全部相同)。
38+
/// 3. [COMPOSITION] / [LANE] / [LANE_BLOCK] / [BEAM] / [FLICK] / [NOTES]:逐行严格比较。
39+
/// 4. [BULLET] / [BELL]:逐行比较,但其中引用 B_PALETTE ID 的字段,比较的是
40+
/// 所引用的 BPL 行的实质内容(去 ID 后的其余字段)是否相同,而非 ID 字面相等。
41+
///
42+
/// 比较失败时,会打印差异所在 expected 中的行号(1-based)。
43+
/// </summary>
44+
internal static class OgkrTextComparer
45+
{
46+
public sealed record LineEntry(int LineNumber, string Content);
47+
48+
// 在 BLT/BEL 行中,引用 B_PALETTE strID 的字段位置(按 \t 分割后的索引)。
49+
// BLT: BLT strId tUnit tGrid xUnit BulletType → 索引 1
50+
// BEL: BEL tUnit tGrid xUnit bulletPallete → 索引 4
51+
private const int BltPaletteIndex = 1;
52+
private const int BelPaletteIndex = 4;
53+
54+
public static void AssertOgkrEqual(string expectedText, string actualText)
55+
{
56+
var expectedSections = ParseSections(expectedText);
57+
var actualSections = ParseSections(actualText);
58+
59+
CompareHeaderSection(expectedSections, actualSections);
60+
61+
AssertNoSubstantiallyEqualBpalettes(actualSections);
62+
63+
var expectedBplSubstance = BuildBplSubstanceMap(expectedSections);
64+
var actualBplSubstance = BuildBplSubstanceMap(actualSections);
65+
66+
CompareSimpleSection("COMPOSITION", expectedSections, actualSections);
67+
CompareSimpleSection("LANE", expectedSections, actualSections);
68+
CompareSimpleSection("LANE_BLOCK", expectedSections, actualSections);
69+
CompareSimpleSection("BEAM", expectedSections, actualSections);
70+
CompareSimpleSection("FLICK", expectedSections, actualSections);
71+
CompareSimpleSection("NOTES", expectedSections, actualSections);
72+
73+
CompareBulletOrBellSection("BULLET", BltPaletteIndex,
74+
expectedSections, actualSections, expectedBplSubstance, actualBplSubstance);
75+
CompareBulletOrBellSection("BELL", BelPaletteIndex,
76+
expectedSections, actualSections, expectedBplSubstance, actualBplSubstance);
77+
}
78+
79+
private static Dictionary<string, List<LineEntry>> ParseSections(string text)
80+
{
81+
var result = new Dictionary<string, List<LineEntry>>();
82+
var lines = text.Replace("\r\n", "\n").Split('\n');
83+
string? current = null;
84+
for (var i = 0; i < lines.Length; i++)
85+
{
86+
var raw = lines[i];
87+
var trimmed = raw.Trim();
88+
if (trimmed.StartsWith('[') && trimmed.EndsWith(']'))
89+
{
90+
current = trimmed[1..^1];
91+
if (!result.ContainsKey(current))
92+
result[current] = [];
93+
continue;
94+
}
95+
96+
if (current == null) continue;
97+
if (string.IsNullOrWhiteSpace(raw)) continue;
98+
result[current].Add(new LineEntry(i + 1, raw.TrimEnd()));
99+
}
100+
101+
return result;
102+
}
103+
104+
private static void CompareHeaderSection(
105+
Dictionary<string, List<LineEntry>> expectedSections,
106+
Dictionary<string, List<LineEntry>> actualSections)
107+
{
108+
var expected = (expectedSections.TryGetValue("HEADER", out var le) ? le : [])
109+
.Where(l => !ShouldSkipHeaderLine(l.Content)).ToList();
110+
var actual = (actualSections.TryGetValue("HEADER", out var la) ? la : [])
111+
.Where(l => !ShouldSkipHeaderLine(l.Content)).ToList();
112+
113+
CompareLinesStrict("HEADER", expected, actual);
114+
}
115+
116+
private static bool ShouldSkipHeaderLine(string content)
117+
{
118+
var first = content.Split('\t', 2)[0].Trim();
119+
return first.StartsWith("T_") || first == "TUTORIAL";
120+
}
121+
122+
private static void CompareSimpleSection(string section,
123+
Dictionary<string, List<LineEntry>> expectedSections,
124+
Dictionary<string, List<LineEntry>> actualSections)
125+
{
126+
var expected = expectedSections.TryGetValue(section, out var le) ? le : [];
127+
var actual = actualSections.TryGetValue(section, out var la) ? la : [];
128+
CompareLinesStrict(section, expected, actual);
129+
}
130+
131+
private static void CompareLinesStrict(string section, List<LineEntry> expected, List<LineEntry> actual)
132+
{
133+
var max = Math.Max(expected.Count, actual.Count);
134+
for (var i = 0; i < max; i++)
135+
{
136+
var eOk = i < expected.Count;
137+
var aOk = i < actual.Count;
138+
var eStr = eOk ? expected[i].Content : "<EOF>";
139+
var aStr = aOk ? actual[i].Content : "<EOF>";
140+
if (eOk && aOk && eStr == aStr) continue;
141+
var lineNumDesc = eOk ? $"line {expected[i].LineNumber}" : "<EOF>";
142+
143+
Assert.Fail(
144+
$"[{section}] section mismatch (at {lineNumDesc}):{Environment.NewLine}" +
145+
$" EXPECTED: {eStr}{Environment.NewLine}" +
146+
$" ACTUAL : {aStr}");
147+
}
148+
}
149+
150+
/// <summary>
151+
/// 计算一行 B_PALETTE 条目(BPL 行)的“实质内容”:去掉位于索引 1 的 strID 字段后,
152+
/// 其余所有以制表符分隔的字段拼接而成的字符串。
153+
/// </summary>
154+
private static string ExtractBplSubstance(string line)
155+
{
156+
var tokens = line.Split('\t');
157+
if (tokens.Length < 2) return line;
158+
var sub = new List<string>(tokens.Length - 1);
159+
for (var i = 0; i < tokens.Length; i++)
160+
if (i != 1) sub.Add(tokens[i]);
161+
return string.Join('\t', sub);
162+
}
163+
164+
private static void AssertNoSubstantiallyEqualBpalettes(Dictionary<string, List<LineEntry>> sections)
165+
{
166+
if (!sections.TryGetValue("B_PALETTE", out var lines) || lines.Count == 0) return;
167+
168+
var seen = new Dictionary<string, LineEntry>();
169+
foreach (var line in lines)
170+
{
171+
var substance = ExtractBplSubstance(line.Content);
172+
if (seen.TryGetValue(substance, out var prev))
173+
{
174+
Assert.Fail(
175+
$"[B_PALETTE] actual 中存在实质相同的两行(除 ID 外其余字段完全一致):{Environment.NewLine}" +
176+
$" line {prev.LineNumber}: {prev.Content}{Environment.NewLine}" +
177+
$" line {line.LineNumber}: {line.Content}");
178+
}
179+
180+
seen[substance] = line;
181+
}
182+
}
183+
184+
/// <summary>
185+
/// 构建 B_PALETTE 中 strID → 实质内容 的映射,用于 BLT/BEL 行做引用解析比较。
186+
/// </summary>
187+
private static Dictionary<string, string> BuildBplSubstanceMap(
188+
Dictionary<string, List<LineEntry>> sections)
189+
{
190+
var map = new Dictionary<string, string>();
191+
if (!sections.TryGetValue("B_PALETTE", out var lines)) return map;
192+
foreach (var line in lines)
193+
{
194+
var tokens = line.Content.Split('\t');
195+
if (tokens.Length < 2) continue;
196+
var id = tokens[1].Trim();
197+
if (id.Length == 0) continue;
198+
map[id] = ExtractBplSubstance(line.Content);
199+
}
200+
return map;
201+
}
202+
203+
private static void CompareBulletOrBellSection(string section, int idPosition,
204+
Dictionary<string, List<LineEntry>> expectedSections,
205+
Dictionary<string, List<LineEntry>> actualSections,
206+
Dictionary<string, string> expectedBplSubstance,
207+
Dictionary<string, string> actualBplSubstance)
208+
{
209+
var expected = expectedSections.TryGetValue(section, out var le) ? le : [];
210+
var actual = actualSections.TryGetValue(section, out var la) ? la : [];
211+
212+
var max = Math.Max(expected.Count, actual.Count);
213+
for (var i = 0; i < max; i++)
214+
{
215+
var eOk = i < expected.Count;
216+
var aOk = i < actual.Count;
217+
var eStr = eOk ? expected[i].Content : "<EOF>";
218+
var aStr = aOk ? actual[i].Content : "<EOF>";
219+
var lineNumDesc = eOk ? $"line {expected[i].LineNumber}" : "<EOF>";
220+
221+
string MakeFailMsg(string? extra = null) =>
222+
$"[{section}] section mismatch (at {lineNumDesc}):{Environment.NewLine}" +
223+
$" EXPECTED: {eStr}{Environment.NewLine}" +
224+
$" ACTUAL : {aStr}" +
225+
(extra is null ? "" : $"{Environment.NewLine} {extra}");
226+
227+
if (!eOk || !aOk)
228+
{
229+
Assert.Fail(MakeFailMsg());
230+
continue;
231+
}
232+
233+
var eTokens = expected[i].Content.Split('\t');
234+
var aTokens = actual[i].Content.Split('\t');
235+
236+
if (eTokens.Length != aTokens.Length)
237+
{
238+
Assert.Fail(MakeFailMsg("(token count differs)"));
239+
continue;
240+
}
241+
242+
for (var k = 0; k < eTokens.Length; k++)
243+
{
244+
if (k == idPosition)
245+
{
246+
if (!ReferencesEquivalent(eTokens[k], aTokens[k],
247+
expectedBplSubstance, actualBplSubstance))
248+
{
249+
Assert.Fail(MakeFailMsg(
250+
$"(B_PALETTE reference mismatch: expected '{eTokens[k]}' vs actual '{aTokens[k]}')"));
251+
break;
252+
}
253+
}
254+
else if (eTokens[k] != aTokens[k])
255+
{
256+
Assert.Fail(MakeFailMsg($"(token #{k} differs: '{eTokens[k]}' vs '{aTokens[k]}')"));
257+
break;
258+
}
259+
}
260+
}
261+
}
262+
263+
/// <summary>
264+
/// 比较 BLT/BEL 行中位于“调色板引用”字段的两个 ID 是否等价。
265+
/// 若两边的 ID 都在各自的 B_PALETTE 中能查到,则以其实质内容(去 ID 后的字段)相等为准;
266+
/// 若两边都不是有效引用(如 BEL 行的 "--"),则按字面值比较;
267+
/// 否则视为不等价。
268+
/// </summary>
269+
private static bool ReferencesEquivalent(string eId, string aId,
270+
Dictionary<string, string> expectedBplSubstance,
271+
Dictionary<string, string> actualBplSubstance)
272+
{
273+
var eHas = expectedBplSubstance.TryGetValue(eId, out var eSub);
274+
var aHas = actualBplSubstance.TryGetValue(aId, out var aSub);
275+
if (eHas && aHas) return eSub == aSub;
276+
if (!eHas && !aHas) return eId == aId;
277+
return false;
28278
}
29279
}

0 commit comments

Comments
 (0)