use bms_table::{BmsTable, BmsTableData, BmsTableHeader, ChartItem, CourseGroup, CourseInfo};
use serde_json::json;
fn hdr(name: &str, symbol: &str, data_url: &str) -> BmsTableHeader {
BmsTableHeader::new(name.into(), symbol.into(), data_url.into())
}
fn hdr_with_levels(name: &str, symbol: &str, data_url: &str, levels: &[&str]) -> BmsTableHeader {
let mut h = BmsTableHeader::new(name.into(), symbol.into(), data_url.into());
h.level_order = levels.iter().map(|&s| s.to_string()).collect();
h
}
fn data_empty() -> BmsTableData {
BmsTableData::new(Vec::new())
}
fn table(h: BmsTableHeader, d: BmsTableData) -> BmsTable {
BmsTable::new(h, d)
}
fn sample_chart_json() -> serde_json::Value {
json!([
{
"level": "1",
"id": 1,
"md5": "test_md5_1",
"sha256": "test_sha256_1",
"title": "Test Song",
"artist": "Test Artist",
"url": "https://example.com/test.bms",
"url_diff": "https://example.com/test_diff.bms",
"custom_field": "custom_value",
"rating": 5.0
}
])
}
fn extract_course_from_nested(group: &CourseGroup) -> &CourseInfo {
let CourseGroup::SubGroups(groups) = group else {
panic!("expected SubGroups, got {group:?}");
};
let [sub] = groups.as_slice() else {
panic!("expected one subgroup, got {}: {groups:?}", groups.len());
};
let CourseGroup::Courses(courses) = sub else {
panic!("expected Courses, got {sub:?}");
};
let [course] = courses.as_slice() else {
panic!("expected one course, got {}: {courses:?}", courses.len());
};
course
}
#[test]
fn build_bms_table_from_json_parses_all_fields() {
let header_json = json!({
"name": "Test Table",
"symbol": "test",
"data_url": "charts.json",
"course": [
[
{
"name": "Test Course",
"constraint": ["grade_mirror"],
"trophy": [
{
"name": "goldmedal",
"missrate": 1.0,
"scorerate": 90.0
}
],
"md5": ["test_md5_1", "test_md5_2"]
}
]
],
"level_order": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, "!i"],
"extra_field": "extra_value",
"another_field": 123
});
let data_json = sample_chart_json();
let header: BmsTableHeader = serde_json::from_value(header_json).unwrap();
let data: BmsTableData = serde_json::from_value(data_json).unwrap();
let bms_table = BmsTable::new(header, data);
assert_eq!(bms_table.header.name, "Test Table");
assert_eq!(bms_table.header.symbol, "test");
assert_eq!(bms_table.header.data_url, "charts.json");
assert_eq!(bms_table.data.len(), 1);
let course = extract_course_from_nested(&bms_table.header.course);
assert_eq!(course.name, "Test Course");
assert_eq!(course.constraint, vec!["grade_mirror"]);
assert_eq!(course.trophy.len(), 1);
let [trophy0] = course.trophy.as_slice() else {
panic!(
"expected one trophy, got {}: {:?}",
course.trophy.len(),
course.trophy
);
};
assert_eq!(trophy0.name, "goldmedal");
assert!((trophy0.missrate - 1.0).abs() <= 1e-12);
assert!((trophy0.scorerate - 90.0).abs() <= 1e-12);
assert_eq!(course.md5, vec!["test_md5_1", "test_md5_2"]);
let [score] = bms_table.data.as_slice() else {
panic!(
"expected one chart, got {}: {:?}",
bms_table.data.len(),
bms_table.data.0
);
};
assert_eq!(score.level, "1");
assert_eq!(score.md5, Some("test_md5_1".to_string()));
assert_eq!(score.sha256, Some("test_sha256_1".to_string()));
assert_eq!(score.title, Some("Test Song".to_string()));
assert_eq!(score.artist, Some("Test Artist".to_string()));
assert_eq!(score.url, Some("https://example.com/test.bms".to_string()));
assert_eq!(
score.url_diff,
Some("https://example.com/test_diff.bms".to_string())
);
assert_eq!(
bms_table.header.extra.get("extra_field"),
Some(&json!("extra_value"))
);
assert_eq!(
bms_table.header.extra.get("another_field"),
Some(&json!(123))
);
assert!(!bms_table.header.extra.contains_key("name"));
assert_eq!(
score.extra.get("custom_field"),
Some(&json!("custom_value"))
);
assert_eq!(score.extra.get("rating"), Some(&json!(5.0)));
assert!(!score.extra.contains_key("level"));
assert_eq!(bms_table.header.level_order.len(), 22);
let [first, .., penultimate, last] = bms_table.header.level_order.as_slice() else {
panic!(
"expected at least two level_order entries, got {}: {:?}",
bms_table.header.level_order.len(),
bms_table.header.level_order
);
};
assert_eq!(first.as_str(), "0");
assert_eq!(penultimate.as_str(), "20");
assert_eq!(last.as_str(), "!i");
assert!(!bms_table.header.extra.contains_key("level_order"));
}
#[test]
fn build_bms_table_with_empty_fields_keeps_empty_strings() {
let header_json = json!({
"name": "Test Table",
"symbol": "test",
"data_url": "charts.json",
"course": []
});
let data_json = json!([
{
"level": "1",
"id": 1,
"md5": "",
"sha256": "",
"title": "",
"artist": "",
"url": "",
"url_diff": ""
}
]);
let header: BmsTableHeader = serde_json::from_value(header_json).unwrap();
let data: BmsTableData = serde_json::from_value(data_json).unwrap();
let bms_table = BmsTable::new(header, data);
let [score] = bms_table.data.as_slice() else {
panic!(
"expected one chart, got {}: {:?}",
bms_table.data.len(),
bms_table.data.0
);
};
assert_eq!(score.level, "1");
assert_eq!(score.md5, Some(String::new()));
assert_eq!(score.sha256, Some(String::new()));
assert_eq!(score.title, Some(String::new()));
assert_eq!(score.artist, Some(String::new()));
assert_eq!(score.url, Some(String::new()));
assert_eq!(score.url_diff, Some(String::new()));
}
#[test]
fn bms_table_creation_sets_fields_correctly() {
let mut header = hdr("Test Table", "test", "https://example.com/charts.json");
header.level_order = vec!["0".to_string(), "1".to_string()];
let bms_table = table(header, data_empty());
assert_eq!(bms_table.header.name, "Test Table");
assert_eq!(bms_table.header.symbol, "test");
assert!(bms_table.header.course.flatten().is_empty());
assert_eq!(bms_table.data.len(), 0);
assert_eq!(bms_table.header.level_order.len(), 2);
}
#[test]
fn bms_table_partial_eq_matches_identical_tables() {
let mut header1 = hdr("Test Table", "test", "https://example.com/charts.json");
header1.level_order = vec!["0".to_string(), "1".to_string()];
let table1 = table(header1.clone(), data_empty());
let table2 = table(header1, data_empty());
assert_eq!(table1, table2);
}
#[test]
fn chart_item_numeric_fields_convert_correctly() {
let data_json = json!([
{
"level": 0,
"id": 1,
"md5": "12345",
"sha256": "67890",
"title": "987",
"artist": "654",
"url": "321",
"url_diff": "111"
}
]);
let data: BmsTableData = serde_json::from_value(data_json).unwrap();
let [score] = data.as_slice() else {
panic!("expected one chart, got {}: {:?}", data.len(), data.0);
};
assert_eq!(score.level, "0");
assert_eq!(score.md5, Some("12345".to_string()));
assert_eq!(score.sha256, Some("67890".to_string()));
assert_eq!(score.title, Some("987".to_string()));
assert_eq!(score.artist, Some("654".to_string()));
assert_eq!(score.url, Some("321".to_string()));
assert_eq!(score.url_diff, Some("111".to_string()));
}
#[test]
fn build_bms_table_extra_fields_ignored() {
let header_json = json!({
"name": "Test Table",
"symbol": "test",
"data_url": "charts.json"
});
let data_json = json!([
{
"level": "1",
"id": 1
}
]);
let header: BmsTableHeader = serde_json::from_value(header_json).unwrap();
let data: BmsTableData = serde_json::from_value(data_json).unwrap();
let _ = BmsTable::new(header, data);
}
#[test]
fn course_info_flat_list_deserialized_correctly() {
let json_data = r#"{
"name": "Test Table",
"symbol": "test",
"data_url": "score.json",
"course": [
{
"name": "Course 1",
"constraint": ["grade_mirror"],
"trophy": [
{
"name": "goldmedal",
"missrate": 5.0,
"scorerate": 70.0
}
],
"md5": ["abc123", "def456"]
}
]
}"#;
let result: serde_json::Result<BmsTableHeader> = serde_json::from_str(json_data);
assert!(result.is_ok());
let header = result.unwrap();
assert_eq!(header.name, "Test Table");
assert_eq!(header.symbol, "test");
assert_eq!(header.data_url, "score.json");
let course = match &header.course {
CourseGroup::Courses(courses) => {
let [course] = courses.as_slice() else {
panic!("expected one course, got {}: {:?}", courses.len(), courses);
};
course
}
other @ CourseGroup::SubGroups(_) => panic!("expected Courses, got {other:?}"),
_ => unreachable!("unexpected CourseGroup variant"),
};
assert_eq!(course.name.as_str(), "Course 1");
assert_eq!(course.md5, vec!["abc123", "def456"]);
}
#[test]
fn course_info_nested_lists_deserialized_correctly() {
let json_data = r#"{
"name": "Test Table",
"symbol": "test",
"data_url": "score.json",
"course": [
[
{
"name": "Course 1",
"constraint": ["grade_mirror"],
"trophy": [
{
"name": "goldmedal",
"missrate": 5.0,
"scorerate": 70.0
}
],
"md5": ["abc123", "def456"]
}
],
[
{
"name": "Course 2",
"constraint": ["ln"],
"trophy": [
{
"name": "silvermedal",
"missrate": 10.0,
"scorerate": 60.0
}
],
"md5": ["ghi789"]
}
]
]
}"#;
let result: serde_json::Result<BmsTableHeader> = serde_json::from_str(json_data);
assert!(result.is_ok());
let header = result.unwrap();
assert_eq!(header.name, "Test Table");
assert_eq!(header.symbol, "test");
assert_eq!(header.data_url, "score.json");
let (course1, course2) = match &header.course {
CourseGroup::SubGroups(groups) => {
let [g1, g2] = groups.as_slice() else {
panic!("expected two groups, got {}: {:?}", groups.len(), groups);
};
let info1 = match g1 {
CourseGroup::Courses(courses) => {
let [c] = courses.as_slice() else {
panic!("expected one course, got {}: {:?}", courses.len(), courses);
};
c
}
other @ CourseGroup::SubGroups(_) => panic!("expected Courses, got {other:?}"),
_ => unreachable!("unexpected CourseGroup variant"),
};
let info2 = match g2 {
CourseGroup::Courses(courses) => {
let [c] = courses.as_slice() else {
panic!("expected one course, got {}: {:?}", courses.len(), courses);
};
c
}
other @ CourseGroup::SubGroups(_) => panic!("expected Courses, got {other:?}"),
_ => unreachable!("unexpected CourseGroup variant"),
};
(info1, info2)
}
other @ CourseGroup::Courses(_) => panic!("expected SubGroups, got {other:?}"),
_ => unreachable!("unexpected CourseGroup variant"),
};
assert_eq!(course1.name.as_str(), "Course 1");
assert_eq!(course2.name.as_str(), "Course 2");
assert_eq!(course1.md5, vec!["abc123", "def456"]);
assert_eq!(course2.md5, vec!["ghi789"]);
}
#[test]
fn course_info_charts_missing_level_defaults_to_zero() {
let json_data = r#"{
"name": "Test Course",
"constraint": ["grade_mirror"],
"trophy": [
{
"name": "goldmedal",
"missrate": 5.0,
"scorerate": 70.0
}
],
"charts": [
{
"title": "Test Song",
"artist": "Test Artist",
"url": "https://example.com/test.bms"
},
{
"level": "1",
"title": "Test Song 2",
"artist": "Test Artist 2",
"url": "https://example.com/test2.bms"
}
]
}"#;
let result: serde_json::Result<CourseInfo> = serde_json::from_str(json_data);
assert!(result.is_ok());
let course_info = result.unwrap();
assert_eq!(course_info.name, "Test Course");
assert_eq!(course_info.constraint, vec!["grade_mirror"]);
assert_eq!(course_info.trophy.len(), 1);
assert_eq!(course_info.charts.len(), 2);
let [first_chart, second_chart] = course_info.charts.as_slice() else {
panic!(
"expected two charts, got {}: {:?}",
course_info.charts.len(),
course_info.charts
);
};
assert_eq!(first_chart.level, "0");
assert_eq!(first_chart.title, Some("Test Song".to_string()));
assert_eq!(first_chart.artist, Some("Test Artist".to_string()));
assert_eq!(second_chart.level, "1");
assert_eq!(second_chart.title, Some("Test Song 2".to_string()));
assert_eq!(second_chart.artist, Some("Test Artist 2".to_string()));
}
#[test]
fn course_info_sha256_list_converts_to_charts() {
let json_data = r#"{
"name": "Test Course",
"constraint": ["grade_mirror"],
"trophy": [
{
"name": "goldmedal",
"missrate": 5.0,
"scorerate": 70.0
}
],
"sha256": ["sha256_hash_1", "sha256_hash_2"]
}"#;
let result: serde_json::Result<CourseInfo> = serde_json::from_str(json_data);
assert!(result.is_ok());
let course_info = result.unwrap();
assert_eq!(course_info.name, "Test Course");
assert_eq!(course_info.constraint, vec!["grade_mirror"]);
assert_eq!(course_info.trophy.len(), 1);
assert_eq!(course_info.sha256, vec!["sha256_hash_1", "sha256_hash_2"]);
}
#[test]
fn course_info_md5_sha256_both_convert_to_charts() {
let json_data = r#"{
"name": "Test Course",
"constraint": ["grade_mirror"],
"trophy": [
{
"name": "goldmedal",
"missrate": 5.0,
"scorerate": 70.0
}
],
"md5": ["md5_hash_1"],
"sha256": ["sha256_hash_1"],
"charts": [
{
"level": "2",
"title": "Existing Chart",
"artist": "Test Artist"
}
]
}"#;
let result: serde_json::Result<CourseInfo> = serde_json::from_str(json_data);
assert!(result.is_ok());
let course_info = result.unwrap();
assert_eq!(course_info.name, "Test Course");
assert_eq!(course_info.constraint, vec!["grade_mirror"]);
assert_eq!(course_info.trophy.len(), 1);
assert_eq!(course_info.md5, vec!["md5_hash_1"]);
assert_eq!(course_info.sha256, vec!["sha256_hash_1"]);
assert_eq!(course_info.charts.len(), 1);
let [existing] = course_info.charts.as_slice() else {
panic!(
"expected one chart, got {}: {:?}",
course_info.charts.len(),
course_info.charts
);
};
assert_eq!(existing.level.as_str(), "2");
assert_eq!(existing.title.as_deref(), Some("Existing Chart"));
assert_eq!(existing.artist.as_deref(), Some("Test Artist"));
}
#[test]
fn header_json_roundtrip_preserves_fields() {
let mut header = hdr("Test Table", "test", "charts.json");
header.level_order = vec!["0".to_string(), "1".to_string(), "!i".to_string()];
let json = serde_json::to_string(&header).unwrap();
let parsed: bms_table::BmsTableHeader = serde_json::from_str(&json).unwrap();
assert_eq!(header, parsed);
assert!(parsed.course.flatten().is_empty());
}
fn make_course_info(name: &str) -> serde_json::Value {
json!({
"name": name,
"constraint": [],
"trophy": [],
})
}
#[test]
fn course_empty_flat_roundtrips_correctly() {
let raw = json!({"name":"T","symbol":"t","data_url":"c.json","course":[],"level_order":[]});
let h: BmsTableHeader = serde_json::from_value(raw).unwrap();
assert!(matches!(&h.course, CourseGroup::Courses(v) if v.is_empty()));
let out = serde_json::to_value(&h).unwrap();
assert_eq!(out.get("course").unwrap(), &json!([]));
}
#[test]
fn course_single_flat_roundtrips_correctly() {
let course = json!([make_course_info("C1")]);
let raw = json!({"name":"T","symbol":"t","data_url":"c.json","course":course,"level_order":[]});
let h: BmsTableHeader = serde_json::from_value(raw).unwrap();
assert!(matches!(&h.course, CourseGroup::Courses(v) if v.len() == 1));
let out = serde_json::to_value(&h).unwrap();
assert_eq!(out.get("course").unwrap(), &course);
}
#[test]
fn course_multi_flat_roundtrips_correctly() {
let course = json!([make_course_info("C1"), make_course_info("C2")]);
let raw = json!({"name":"T","symbol":"t","data_url":"c.json","course":course,"level_order":[]});
let h: BmsTableHeader = serde_json::from_value(raw).unwrap();
assert!(matches!(&h.course, CourseGroup::Courses(v) if v.len() == 2));
let out = serde_json::to_value(&h).unwrap();
assert_eq!(out.get("course").unwrap(), &course);
}
#[test]
fn course_single_nested_roundtrips_correctly() {
let course = json!([[make_course_info("C1")]]);
let raw = json!({"name":"T","symbol":"t","data_url":"c.json","course":course,"level_order":[]});
let h: BmsTableHeader = serde_json::from_value(raw).unwrap();
assert!(matches!(&h.course, CourseGroup::SubGroups(g) if g.len() == 1));
let out = serde_json::to_value(&h).unwrap();
assert_eq!(out.get("course").unwrap(), &course);
}
#[test]
fn course_multi_nested_roundtrips_correctly() {
let course = json!([[make_course_info("C1")], [make_course_info("C2")]]);
let raw = json!({"name":"T","symbol":"t","data_url":"c.json","course":course,"level_order":[]});
let h: BmsTableHeader = serde_json::from_value(raw).unwrap();
assert!(matches!(&h.course, CourseGroup::SubGroups(g) if g.len() == 2));
let out = serde_json::to_value(&h).unwrap();
assert_eq!(out.get("course").unwrap(), &course);
}
#[test]
fn course_empty_nested_roundtrips_correctly() {
let course = json!([[]]);
let raw = json!({"name":"T","symbol":"t","data_url":"c.json","course":course,"level_order":[]});
let h: BmsTableHeader = serde_json::from_value(raw).unwrap();
let CourseGroup::SubGroups(g) = &h.course else {
panic!("expected SubGroups");
};
assert!(matches!(&g.first().unwrap(), CourseGroup::Courses(v) if v.is_empty()));
let out = serde_json::to_value(&h).unwrap();
assert_eq!(out.get("course").unwrap(), &course);
}
#[test]
fn course_deeply_nested_roundtrips_correctly() {
let course = json!([[[make_course_info("C1")]]]);
let raw = json!({"name":"T","symbol":"t","data_url":"c.json","course":course,"level_order":[]});
let h: BmsTableHeader = serde_json::from_value(raw).unwrap();
assert!(matches!(&h.course, CourseGroup::SubGroups(g) if g.len() == 1));
let out = serde_json::to_value(&h).unwrap();
assert_eq!(out.get("course").unwrap(), &course);
}
#[test]
fn roundtrip_course_missing_defaults_to_empty() {
let raw = json!({"name":"T","symbol":"t","data_url":"c.json","level_order":[]});
let h: BmsTableHeader = serde_json::from_value(raw).unwrap();
assert!(h.course.flatten().is_empty());
assert!(matches!(&h.course, CourseGroup::Courses(v) if v.is_empty()));
}
#[test]
fn level_index_returns_correct_index() {
let header = hdr_with_levels("Test", "t", "c.json", &["1", "2", "3", "11+"]);
assert_eq!(header.level_index("1"), Some(0));
assert_eq!(header.level_index("2"), Some(1));
assert_eq!(header.level_index("3"), Some(2));
assert_eq!(header.level_index("11+"), Some(3));
}
#[test]
fn level_index_missing_level_returns_none() {
let header = hdr_with_levels("Test", "t", "c.json", &["1", "2"]);
assert_eq!(header.level_index("3"), None);
assert_eq!(header.level_index("0"), None);
}
#[test]
fn level_index_empty_level_order_returns_none() {
let header = hdr_with_levels("Test", "t", "c.json", &[]);
assert_eq!(header.level_index("1"), None);
}
#[test]
fn effective_tag_returns_tag_when_present() {
let mut header = hdr("Test", "t", "c.json");
header.tag = Some("★".into());
assert_eq!(header.effective_tag(), "★");
}
#[test]
fn effective_tag_falls_back_to_symbol_when_absent() {
let header = hdr("Test", "sl", "c.json");
assert_eq!(header.effective_tag(), "sl");
}
#[test]
fn chart_item_new_fields_land_in_extra() {
let data_json = json!([
{
"level": "1",
"md5": "abc",
"comment": "hard chart",
"url_pack": "https://example.com/pack.zip",
"name_pack": "Example Pack",
"org_md5": "def123",
"mode": "7K",
"custom_extra": "still in extra"
}
]);
let data: BmsTableData = serde_json::from_value(data_json).unwrap();
let [chart] = data.as_slice() else {
panic!("expected one chart, got {}", data.len());
};
assert_eq!(chart.comment.as_deref(), Some("hard chart"));
assert_eq!(
chart.extra.get("url_pack"),
Some(&json!("https://example.com/pack.zip"))
);
assert_eq!(chart.extra.get("name_pack"), Some(&json!("Example Pack")));
assert_eq!(chart.extra.get("org_md5"), Some(&json!("def123")));
assert_eq!(chart.extra.get("mode"), Some(&json!("7K")));
assert_eq!(
chart.extra.get("custom_extra"),
Some(&json!("still in extra"))
);
}
#[test]
fn chart_item_new_fields_default_to_none() {
let data_json = json!([
{
"level": "1",
"md5": "abc"
}
]);
let data: BmsTableData = serde_json::from_value(data_json).unwrap();
let [chart] = data.as_slice() else {
panic!("expected one chart, got {}", data.len());
};
assert!(chart.comment.is_none());
assert!(!chart.extra.contains_key("url_pack"));
assert!(!chart.extra.contains_key("name_pack"));
assert!(!chart.extra.contains_key("org_md5"));
assert!(!chart.extra.contains_key("mode"));
}
#[test]
fn course_group_flat_into_flattened_returns_courses() {
let group = CourseGroup::Courses(vec![CourseInfo::new("C1".into())]);
let flat = group.into_flattened();
assert_eq!(flat.len(), 1);
assert_eq!(flat.first().unwrap().name, "C1");
}
#[test]
fn course_group_nested_into_flattened_returns_courses() {
let group = CourseGroup::SubGroups(vec![
CourseGroup::Courses(vec![CourseInfo::new("C1".into())]),
CourseGroup::Courses(vec![CourseInfo::new("C2".into())]),
]);
let flat = group.into_flattened();
assert_eq!(flat.len(), 2);
assert_eq!(flat.first().unwrap().name, "C1");
assert_eq!(flat.get(1).unwrap().name, "C2");
}
#[test]
fn course_group_empty_into_flattened_returns_empty_vec() {
let group = CourseGroup::Courses(vec![]);
assert!(group.into_flattened().is_empty());
}
#[test]
fn course_group_deeply_nested_into_flattened_returns_courses() {
let group = CourseGroup::SubGroups(vec![CourseGroup::SubGroups(vec![CourseGroup::Courses(
vec![CourseInfo::new("C1".into())],
)])]);
let flat = group.into_flattened();
assert_eq!(flat.len(), 1);
assert_eq!(flat.first().unwrap().name, "C1");
}
#[test]
fn chart_data_empty_array_has_no_charts() {
let data: BmsTableData = serde_json::from_value(json!([])).unwrap();
assert!(data.is_empty());
}
#[test]
fn chart_item_level_null_defaults_to_zero() {
let data: BmsTableData =
serde_json::from_value(json!([{ "level": null, "md5": "abc" }])).unwrap();
let [chart] = data.as_slice() else {
panic!("expected one chart, got {}", data.len());
};
assert_eq!(chart.level, "0");
assert_eq!(chart.md5.as_deref(), Some("abc"));
}
#[test]
fn chart_item_level_zero_converts_to_string() {
let data: BmsTableData = serde_json::from_value(json!([{ "level": 0, "md5": "abc" }])).unwrap();
let [chart] = data.as_slice() else {
panic!("expected one chart, got {}", data.len());
};
assert_eq!(chart.level, "0");
}
#[test]
fn course_chart_level_null_defaults_to_zero() {
let json_data = r#"{
"name": "Test Course",
"constraint": [],
"trophy": [],
"charts": [
{ "level": null, "title": "Null Level Chart" },
{ "title": "Missing Level Chart" }
]
}"#;
let course: CourseInfo = serde_json::from_str(json_data).unwrap();
let [c0, c1] = course.charts.as_slice() else {
panic!("expected two charts, got {}", course.charts.len());
};
assert_eq!(c0.level, "0", "null level should default to \"0\"");
assert_eq!(c1.level, "0", "missing level should default to \"0\"");
}
#[test]
fn course_very_deeply_nested_roundtrips_correctly() {
let course = json!([[[[make_course_info("C1")]]]]);
let raw = json!({"name":"T","symbol":"t","data_url":"c.json","course":course,"level_order":[]});
let h: BmsTableHeader = serde_json::from_value(raw).unwrap();
assert!(matches!(&h.course, CourseGroup::SubGroups(g) if g.len() == 1));
let out = serde_json::to_value(&h).unwrap();
assert_eq!(out.get("course").unwrap(), &course);
}
#[test]
fn course_group_default_serializes_as_empty_array() {
let header = hdr("T", "t", "c.json");
let value = serde_json::to_value(&header).unwrap();
assert_eq!(value.get("course").unwrap(), &json!([]));
}
#[test]
fn level_order_with_boolean_reports_error() {
let raw = json!({"name":"T","symbol":"t","data_url":"c.json","level_order":["1", true, "3"]});
let result: Result<BmsTableHeader, _> = serde_json::from_value(raw);
assert!(result.is_err());
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("level_order") || msg.contains("expected string or number"),
"msg: {msg}"
);
}
#[test]
fn level_order_omitted_defaults_to_empty() {
let raw = json!({"name":"T","symbol":"t","data_url":"c.json"});
let h: BmsTableHeader = serde_json::from_value(raw).unwrap();
assert!(h.level_order.is_empty());
}
#[test]
fn level_order_with_null_value_reports_error() {
let raw = json!({"name":"T","symbol":"t","data_url":"c.json","level_order":["1", null]});
let result: Result<BmsTableHeader, _> = serde_json::from_value(raw);
assert!(result.is_err());
}
#[test]
fn course_group_from_course_info_converts_correctly() {
let group: CourseGroup = CourseInfo::new("C1".into()).into();
let CourseGroup::Courses(v) = group else {
panic!("expected Courses variant");
};
assert_eq!(v.len(), 1);
assert_eq!(v.first().unwrap().name, "C1");
}
#[test]
fn level_field_boolean_reports_error() {
let raw = json!([{ "level": true, "md5": "abc" }]);
let result: Result<BmsTableData, _> = serde_json::from_value(raw);
assert!(result.is_err());
}
#[test]
fn course_info_all_charts_merges_three_sources() {
let mut chart = ChartItem::new("1".into());
chart.md5 = Some("chart_md5".into());
let mut course = CourseInfo::new("C1".into());
course.charts = vec![chart];
course.md5 = vec!["md5_1".into(), "md5_2".into()];
course.sha256 = vec!["sha_1".into()];
let all = course.all_charts();
assert_eq!(all.len(), 4);
let [chart, md5_1, md5_2, sha_1] = all.as_slice() else {
panic!("expected four charts, got {}: {:?}", all.len(), all);
};
assert_eq!(chart.md5.as_deref(), Some("chart_md5"));
assert_eq!(chart.level, "1");
assert_eq!(md5_1.md5.as_deref(), Some("md5_1"));
assert_eq!(md5_1.level, "0");
assert_eq!(md5_2.md5.as_deref(), Some("md5_2"));
assert_eq!(md5_2.level, "0");
assert_eq!(sha_1.sha256.as_deref(), Some("sha_1"));
assert_eq!(sha_1.level, "0");
}
#[test]
fn course_info_extra_fields_preserved() {
let json_data = r#"{
"name": "C1",
"constraint": [],
"trophy": [],
"custom_field": "custom_value",
"another": 42
}"#;
let course: CourseInfo = serde_json::from_str(json_data).unwrap();
assert_eq!(
course.extra.get("custom_field"),
Some(&json!("custom_value"))
);
assert_eq!(course.extra.get("another"), Some(&json!(42)));
}
#[test]
fn course_info_md5_sha256_roundtrip_preserved() {
let mut course = CourseInfo::new("C1".into());
course.md5 = vec!["hash1".into()];
course.sha256 = vec!["hash2".into()];
let json_str = serde_json::to_string(&course).unwrap();
let parsed: CourseInfo = serde_json::from_str(&json_str).unwrap();
assert_eq!(course, parsed);
}