@@ -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