Skip to content

Commit 38fde3e

Browse files
committed
[+] 为Statistics添加测试,和修复测出来的bug
1 parent dc3494c commit 38fde3e

4 files changed

Lines changed: 169 additions & 15 deletions

File tree

chart/Statistics.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,17 @@ internal Statistics(Chart chart)
4646
public int Total => _data.Values.Sum();
4747

4848
// 返回按音符类型分组的数量。所返回的字典中会包含的key:TAP,STR,HLD,SLD,TTP,THO
49-
public Dictionary<string, int> ByNoteType =>
50-
_data.GroupBy(x => x.Key[2..5]).ToDictionary(x => x.Key, x => x.Sum(v => v.Value));
49+
public Dictionary<string, int> ByNoteType => _data.GroupBy(x => x.Key[2..5])
50+
.ToDictionary(x => x.Key, x => x.Sum(v => v.Value))
51+
.EnsureKeys(["TAP", "STR", "HLD", "SLD", "TTP", "THO"]);
5152

5253
// 返回按音符的修饰符分组的数量。所返回的字典中会包含的key:NM,BR,EX,BX
53-
public Dictionary<string, int> ByModifiers =>
54-
_data.GroupBy(x => x.Key[..2]).ToDictionary(x => x.Key, x => x.Sum(v => v.Value));
54+
public Dictionary<string, int> ByModifiers => _data.GroupBy(x => x.Key[..2])
55+
.ToDictionary(x => x.Key, x => x.Sum(v => v.Value))
56+
.EnsureKeys(["NM", "BR", "EX", "BX"]);
5557

5658
// 返回按游戏结算画面上屏判定表分类的数量。所返回的字典中会包含的key:TAP,HOLD,SLIDE,TOUCH,BREAK
57-
public Dictionary<string, int> ByScoring =>
58-
_data.GroupBy(x =>
59+
public Dictionary<string, int> ByScoring => _data.GroupBy(x =>
5960
{
6061
if (x.Key[0] == 'B') return "BREAK";
6162
var type = x.Key[2..5];
@@ -64,7 +65,9 @@ internal Statistics(Chart chart)
6465
else if (type == "TTP") return "TOUCH";
6566
else if (type is "TAP" or "STR") return "TAP";
6667
else throw Utils.Fail();
67-
}).ToDictionary(x => x.Key, x => x.Sum(v => v.Value));
68+
})
69+
.ToDictionary(x => x.Key, x => x.Sum(v => v.Value))
70+
.EnsureKeys(["TAP","HOLD","SLIDE","TOUCH","BREAK"]);
6871

6972
// 绝赞总数
7073
public int Break => _data.Where(x=>x.Key[0] == 'B').Sum(x=>x.Value);

generator/MA2Generator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ private void AddTap(Tap tap, int bar, int tick)
206206
var stats = chart.Statistics;
207207
foreach (var (k, v) in statsNameConversion())
208208
{
209-
result.AppendLine($"T_REC_{k}\t{stats.Data[v]}");
209+
result.AppendLine($"T_REC_{k}\t{stats.Data.GetValueOrDefault(v)}");
210210
}
211211
var totalNum = stats.Total;
212212
result.AppendLine($"T_REC_ALL\t{totalNum}");

tests/Statistics测试.cs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System.Text;
2+
using MuConvert.generator;
3+
using MuConvert.parser;
4+
using Xunit.Abstractions;
5+
using static MuConvert.Tests.TestUtils;
6+
7+
namespace MuConvert.Tests;
8+
9+
/// <summary>
10+
/// 官谱 MA2 → Chart(<see cref="MA2Parser"/>)→ MA2(<see cref="MA2Generator"/>)轮回合后,
11+
/// 与原版 MA2 末尾统计段(<c>T_REC_*</c> 起至文件结束)逐项一致。
12+
/// </summary>
13+
/// <remarks>
14+
/// 下列键与官机/导出工具的规则尚未在 <see cref="MA2Generator"/> 内完全复现,测试中跳过比对:
15+
/// <list type="bullet">
16+
/// <item><description><c>TTM_EACHPAIRS</c>:官机 EACH 对计数与谱面对象模型对应关系仍待考证。</description></item>
17+
/// <item><description><c>T_JUDGE_HLD</c> / <c>T_JUDGE_ALL</c>:Hold 在判定统计中的折算与官机不一致(生成器侧 HLD 仍为 TODO)。</description></item>
18+
/// </list>
19+
/// </remarks>
20+
public class Statistics测试
21+
{
22+
private readonly ITestOutputHelper _output;
23+
24+
public Statistics测试(ITestOutputHelper output) => _output = output;
25+
26+
/// <summary>8 张官谱各取 maidata 等级 5(对应 <c>*03.ma2</c>)。</summary>
27+
public static IEnumerable<object[]> OfficialLevel5()
28+
{
29+
const int levelId = 5;
30+
var repoRoot = FindRepoRoot();
31+
var testsetRoot = Path.Combine(repoRoot.FullName, "tests", "testset", "官谱");
32+
if (!Directory.Exists(testsetRoot))
33+
throw new DirectoryNotFoundException($"Testset root not found: {testsetRoot}");
34+
35+
foreach (var maidataPath in Directory.EnumerateFiles(testsetRoot, "maidata.txt", SearchOption.AllDirectories)
36+
.OrderBy(p => p, StringComparer.Ordinal))
37+
yield return [new TestInput(maidataPath, levelId)];
38+
}
39+
40+
[Theory]
41+
[MemberData(nameof(OfficialLevel5))]
42+
public void 统计段与原版一致(TestInput input)
43+
{
44+
var ma2Original = File.ReadAllText(input.MA2, Encoding.UTF8);
45+
var (chart, parseAlerts) = new MA2Parser().Parse(ma2Original);
46+
Assert.Empty(parseAlerts);
47+
48+
var (ma2RoundTrip, genAlerts) = new MA2Generator().Generate(chart);
49+
Assert.Empty(genAlerts);
50+
51+
var expected = Ma2StatisticsSection.Parse(ma2Original);
52+
Assert.NotEmpty(expected);
53+
54+
var actual = Ma2StatisticsSection.Parse(ma2RoundTrip);
55+
Ma2StatisticsSection.AssertEqual(expected, actual, input.ToString(), _output);
56+
}
57+
}
58+
59+
internal static class Ma2StatisticsSection
60+
{
61+
/// <summary>与 <see cref="MA2Generator"/> 当前实现尚未对齐的统计键,不参与等价断言。</summary>
62+
public static readonly HashSet<string> SkippedKeys =
63+
[
64+
"TTM_EACHPAIRS",
65+
"T_JUDGE_HLD",
66+
"T_JUDGE_ALL",
67+
];
68+
69+
/// <summary>从首个 <c>T_REC_</c> 行起解析到文件末尾,得到 <c>键 → 值</c>(不含键前缀)。</summary>
70+
public static Dictionary<string, string> Parse(string ma2Text)
71+
{
72+
var dict = new Dictionary<string, string>(StringComparer.Ordinal);
73+
var started = false;
74+
foreach (var raw in ma2Text.EnumerateLines())
75+
{
76+
var line = raw.ToString().TrimEnd('\r');
77+
if (!started)
78+
{
79+
if (line.StartsWith("T_REC_", StringComparison.Ordinal))
80+
started = true;
81+
else
82+
continue;
83+
}
84+
85+
if (string.IsNullOrWhiteSpace(line))
86+
continue;
87+
88+
var tab = line.IndexOf('\t');
89+
if (tab <= 0)
90+
continue;
91+
92+
dict[line[..tab]] = line[(tab + 1)..];
93+
}
94+
95+
return dict;
96+
}
97+
98+
public static void AssertEqual(
99+
IReadOnlyDictionary<string, string> expected,
100+
IReadOnlyDictionary<string, string> actual,
101+
string context,
102+
ITestOutputHelper? output)
103+
{
104+
foreach (var kv in expected.OrderBy(k => k.Key, StringComparer.Ordinal))
105+
{
106+
if (SkippedKeys.Contains(kv.Key))
107+
continue;
108+
109+
if (!actual.TryGetValue(kv.Key, out var actVal))
110+
{
111+
Assert.Fail($"{context}: 缺少统计键 {kv.Key}(expected {kv.Value})");
112+
}
113+
114+
if (!ValuesEqual(kv.Key, kv.Value, actVal))
115+
{
116+
Assert.Fail($"{context}: 统计键 {kv.Key} 不一致:expected {kv.Value},actual {actVal}");
117+
}
118+
}
119+
120+
foreach (var kv in actual)
121+
{
122+
if (SkippedKeys.Contains(kv.Key))
123+
continue;
124+
if (expected.ContainsKey(kv.Key))
125+
continue;
126+
if (IsZeroStatValue(kv.Value))
127+
continue;
128+
Assert.Fail($"{context}: 多出原版没有的统计键 {kv.Key}={kv.Value}");
129+
}
130+
}
131+
132+
private static bool IsZeroStatValue(string v) =>
133+
v == "0" || v == "0.0" || v == "0.00" || v == "0.000";
134+
135+
private static bool ValuesEqual(string key, string expected, string actual)
136+
{
137+
return string.Equals(expected, actual, StringComparison.Ordinal);
138+
}
139+
}

utils/Utils.cs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,6 @@ internal static Exception Fail(string msg = "")
2121

2222
public static void SetLocale(CultureInfo culture) => Locale.Culture = culture;
2323

24-
internal static void Add<K, V>(this Dictionary<K, List<V>> dict, K key, V value) where K : notnull
25-
{
26-
if (!dict.ContainsKey(key)) dict[key] = new();
27-
dict[key].Add(value);
28-
}
29-
3024
public static BigInteger LCM(BigInteger a, BigInteger b) => a / BigInteger.GreatestCommonDivisor(a, b) * b;
3125

3226
public static BigInteger LCM(IEnumerable<BigInteger> values) => values.Aggregate(LCM);
@@ -35,4 +29,22 @@ internal static void Add<K, V>(this Dictionary<K, List<V>> dict, K key, V value)
3529
public static Rational Ceil(Rational r) => r.WholePart + (r.FractionPart == 0 ? 0 : 1);
3630

3731
public static BigInteger Max(BigInteger a, BigInteger b) => a > b ? a : b;
38-
}
32+
}
33+
34+
internal static class ExtensionUtils
35+
{
36+
internal static void Add<K, V>(this Dictionary<K, List<V>> dict, K key, V value) where K : notnull
37+
{
38+
if (!dict.ContainsKey(key)) dict[key] = [];
39+
dict[key].Add(value);
40+
}
41+
42+
internal static Dictionary<K, V> EnsureKeys<K, V>(
43+
this Dictionary<K, V> dict,
44+
IEnumerable<K> requiredKeys,
45+
V defaultValue = default!) where K : notnull
46+
{
47+
foreach (var key in requiredKeys) dict.TryAdd(key, defaultValue);
48+
return dict;
49+
}
50+
}

0 commit comments

Comments
 (0)